Why reflection when generics

Why reflection when generics

StepOne

В C# очень мощный инструмент обобщённого программирования. Мощнее, чем в Java. Всю информацию о типовых параметрах, переданных в generic среду можно получить в runtime, и это нереально круто. Плюс к этому типобезопасность, строгие ругательства за несоблюдение выстроенных ограничений ещё в compile time. Но почему при всех имеющихся преимуществах могут прибегать к рефлексии? Давайте разберёмся.

Для начала, кратко, что такое рефлексия. Рефлексия - это механизм, который предоставляет информацию о типах, сборках, модулях приложения во время его выполнения. Например, можно информацию о типе любого объекта можно получить с помощью метода GetType:

var i = 42;
var type = i.GetType();
Console.WriteLine(type); // System.Int32

В основном область применения этого инструмента касается тех ситуаций, когда мы работаем с некой метаинформацией, недоступной на этапе компиляции (атрибуты, конструирование новых типов, позднее связывание). Однако, обобщённые типы это точно такие же обычные типы, поэтому с ними рефлексия тоже работает. Например, мы можем создать инстанс объекта двумя способами:

public class Factory<T> where T : new()
{
  public T GetObject() => new();

  public T GetObject(params object[] args) =>
    (T)Activator.CreateInstance(typeof(T), args)!;
}

В первом случае мы воспользовались непосредственно generic механизмом, который зашит на уровне кода в программу. Во втором же мы прибегли к рефлексии на тот случай, если у конструктора создаваемого объекта есть параметры.

Оказывается дженерики не такие уж и всемогущие. Вот ещё пример.

Есть некий интерфейс:

interface ISome<T> {}

И мы хотим создать некую логику для обработки типов, являющихся типовыми параметрами интерфейса. Получается такой расклад:

class NeedSome<ISome<T>> {} // нельзя
class NeedSome<TSome, T> where TSome : ISome<T> {} // можно

Приходится вводить дополнительный типовой параметр, что создаёт лишнюю когнитивную нагрузку для разработчика. В таких случаях рефлексия может помочь. Приведу пример.

Вот хотим регистрировать реализации нашего интерфейса в DI контейнере. Можно написать что-то вроде:

public static void RegisterSome<TSome, TUsed>(this Container container)
  where TSome : class, ISome<TUsed>
{
  container.RegisterSingleton<ISome<TUsed>, TSome>();
}

Вроде ок, у нас есть ограничение, не-реализацию интерфейса мы не подкинем, все типы известны. НО! Мы поменяли одну строчку кода на другую, наплодив расширение и создав плохую семантику с передачей лишней информации. То есть, семантически ожидается, что если я использую это расширение, то подразумевается, что класс реализует каким-то образом интерфейс. Это можно обойти с помощью рефлексии следующим образом:

public static void RegisterSome<TSome>(this Container container)
  where TSome : class
{
  var iSomeType = typeof(ISome<>);
  var someType = typeof(TSome);
  var someTypeInterface = someType.GetInterfaces()
    .FirstOrDefault(x =>
      x.IsGenericType &&
      x.GetGenericTypeDefinition() == iSomeType
    );
  if (someType.IsClass && someTypeInterface != null)
  {
    var usedType = someTypeInterface
      .GetGenericArguments().First();
    container.RegisterSingleton(
      iSomeType.MakeGenericType(usedType),
      someType
    );
  }
}

Для полноты картины можно прилепить какой-нибудь маркер-интерфейс выше обобщённого по иерархии и добавить его к ограничениям типового параметра, если есть сомнения в недостаточности типобезопасности.

© Канал StepOne

Report Page