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