Рефлексия позволяет программе исследовать типы во время работы: какие свойства есть у класса, каковы их значения, какие существуют методы, какие атрибуты прикреплены. Атрибуты это вторая половина: декларативные метки в квадратных скобках вроде [Obsolete] или [JsonPropertyName("id")], которые сами по себе ничего не значат, пока их не прочитает компилятор или какой-то код через рефлексию. Так работают сериализаторы, ORM, библиотеки проверки, фреймворки тестирования и маршрутизация ASP.NET.
Большинству прикладного кода рефлексия напрямую нужна редко, но понимание её работы объясняет многое в поведении фреймворков.
Объекты Type: typeof и GetType
Всё начинается с System.Type. Получить его можно двумя способами:
Вывод:
Employee
Manager
Employee
False
True
True
System.Int32
False
Name
Разница, которую нужно запомнить: typeof(X) называет тип, известный при написании кода, а obj.GetType() спрашивает объект, чем он является на самом деле. Сравнение GetType() == typeof(Employee) это точное совпадение, которое не срабатывает для производных классов, а обычно нужно не это; is и IsAssignableFrom учитывают наследование. nameof выглядит похоже, но это вообще не рефлексия: компилятор заменяет его строковой константой.
GetType() у ссылки null выбрасывает NullReferenceException, поскольку спрашивать некого. У упакованного типа значения, допускающего null, он возвращает базовый тип: ((int?)5).GetType() это System.Int32.
Чтение и запись свойств
GetProperties() перечисляет открытые свойства типа в виде объектов PropertyInfo, каждый из которых может читать и записывать значение у заданного экземпляра:
Вывод:
Name String = Mug
Price Decimal = 8.50
Stock Int32 = 12
7.90
True
SUP-77
Здесь видны три вещи:
GetValueвозвращаетobject, поэтому типы значений возвращаются упакованными, и для использования их нужно привести.GetPropertyс несуществующим именем возвращаетnull, и следующий вызов на нём выбрасываетNullReferenceException. Проверяйте перед использованием.BindingFlags.NonPublic | BindingFlags.Instanceдобирается до закрытых членов. Это законно в инструментах и тестах, но обходит инкапсуляцию и молча ломается при рефакторинге класса.
Этот цикл по сути и есть то, как работает экспорт в CSV или сериализатор объектов в JSON: пройти по свойствам, прочитать каждое значение, отформатировать его.
Вызов методов и создание объектов по имени
GetMethod находит метод, а Invoke вызывает его с массивом аргументов. Activator.CreateInstance создаёт объект по Type, и так системы плагинов и контейнеры внедрения зависимостей строят типы, выбранные во время выполнения:
Вывод:
60.00
Decimal WithTax(1 parameters)
String Describe(0 parameters)
True
DeclaredOnly ограничивает список членами, объявленными в самом классе; без него GetMethods возвращает также ToString, Equals, GetHashCode и GetType из object. Type.GetType("Name") требует имени с пространством имён, а для типов из других сборок ещё и имени сборки ("MyApp.Plugins.Csv, MyApp.Plugins").
Если вызванный метод выбрасывает исключение, Invoke оборачивает его в TargetInvocationException; исходное находится в его InnerException.
Атрибуты: метки, которые читают компилятор и фреймворки
Атрибут записывается в квадратных скобках перед тем, что он описывает. Фреймворк определяет множество атрибутов; вот несколько, которые меняют поведение компилятора:
public class OrderService
{
[Obsolete("Use PlaceOrderAsync instead.")]
public void PlaceOrder(Order order) { }
// Every call site: warning CS0618: 'OrderService.PlaceOrder(Order)' is obsolete: 'Use PlaceOrderAsync instead.'
// [Obsolete("...", true)] makes it error CS0619 instead.
[Conditional("DEBUG")]
public void Trace(string message) => Console.WriteLine(message);
// Calls to Trace are removed entirely from builds without the DEBUG symbol.
}
[Flags] enum Channels { None = 0, Email = 1, Sms = 2 } // changes how ToString formats combinations
[Serializable] class Snapshot { } // marks a type for legacy binary serialization
Другие читают библиотеки во время выполнения: [JsonPropertyName] и [JsonIgnore] читает System.Text.Json, [Required] и [MaxLength] читают проверка моделей ASP.NET Core и Entity Framework, [HttpGet("orders/{id}")] читает маршрутизация ASP.NET, [Fact] и [Test] читают средства запуска тестов. Сам атрибут ничего не делает; делает код, который его ищет.
Имя ObsoleteAttribute при применении сокращается до [Obsolete]: по соглашению имя каждого класса атрибута оканчивается на Attribute, и C# позволяет опускать этот суффикс.
Объявление и чтение собственного атрибута
Собственный атрибут это класс, наследующийся от Attribute. [AttributeUsage] говорит, к чему его можно применять. Параметры конструктора становятся позиционными аргументами, а открытые задаваемые свойства именованными аргументами:
Вывод:
Username must be at most 20 characters
Email is required
Keep the city code short
0
Это миниатюрная версия того, что проверка моделей ASP.NET Core делает с System.ComponentModel.DataAnnotations. Аргументы атрибутов должны быть константами времени компиляции (числа, строки, typeof(...), значения перечислений или массивы из них), потому что они хранятся в метаданных сборки. GetCustomAttribute<T>() это метод расширения из System.Reflection; есть также IsDefined(typeof(T)), когда нужно лишь узнать, присутствует ли атрибут.
Цена рефлексии
Рефлексия обменивает скорость и безопасность на гибкость:
- Скорость. Поиск члена по имени и вызов через
InvokeилиGetValueнамного медленнее прямого вызова и упаковывает типы значений. Для повторного использования найдитеPropertyInfoилиMethodInfoодин раз и сохраните его или превратите в делегат черезDelegate.CreateDelegateилиMethodInfo.CreateDelegateи вызывайте его. - Безопасность. Опечатка в имени или изменённая сигнатура нормально компилируются и падают во время выполнения. Везде, где можно, предпочитайте
nameof(Product.Price)строке"Price", чтобы переименования ловились. - Обрезка и AOT. Обрезанные приложения и приложения Native AOT удаляют члены, которые, как кажется, никто не использует, а рефлексия скрывает использование от этого анализа. Современные библиотеки (
System.Text.Json,GeneratedRegex, логирование) переходят на генераторы исходного кода, которые делают ту же работу при компиляции.
Используйте рефлексию для тех частей программы, которые действительно заранее не знают своих типов: плагины, универсальные инструменты, сериализаторы, вспомогательные средства тестов. Когда типы известны, обычный код, обобщения или интерфейсы быстрее и проверяются компилятором.
Частые ошибки
GetType() == typeof(Base)для проверки базового класса. Не срабатывает для производных типов. ИспользуйтеisилиIsAssignableFrom.- Нет проверки на
null.GetProperty,GetMethodиType.GetTypeвозвращаютnull, когда ничего не найдено. - Рефлексия в горячем цикле без кэширования. Кэшируйте
MemberInfoили скомпилируйте делегат. - Перехват не того исключения из
Invoke. Настоящее исключение этоInnerExceptionуTargetInvocationException. - Магические строки для имён членов. Используйте
nameof.
Часто задаваемые вопросы
Что такое рефлексия в C#?
Рефлексия это способность программы исследовать типы во время выполнения: перечислять свойства и методы класса, читать и задавать значения по имени, вызывать методы, создавать экземпляры и читать атрибуты. Она находится в System.Reflection и начинается с объекта Type. На ней построены сериализаторы, ORM, контейнеры внедрения зависимостей и фреймворки тестирования.
Чем typeof отличается от GetType в C#?
typeof(Customer) разрешается при компиляции по имени типа, и объект ему не нужен. obj.GetType() вызывается у экземпляра во время выполнения и возвращает фактический тип объекта, который может быть более производным, чем объявленный тип переменной: для Animal a = new Dog(); вызов a.GetType() даёт Dog. GetType() у ссылки null выбрасывает NullReferenceException.
Как получить значение свойства по имени в C#?
obj.GetType().GetProperty("Price") возвращает PropertyInfo (или null, если такого открытого свойства нет), а .GetValue(obj) читает его как object. .SetValue(obj, value) записывает значение. Если делаете это в цикле, кэшируйте PropertyInfo, потому что дорогая часть это поиск.
Как создать собственный атрибут в C#?
Объявите класс, наследующийся от System.Attribute, назовите его с суффиксом Attribute и укажите, где его можно применять, через [AttributeUsage]: [AttributeUsage(AttributeTargets.Property)] class MaxLengthAttribute : Attribute { public int Length { get; } public MaxLengthAttribute(int length) { Length = length; } }. Применяйте его как [MaxLength(50)] и читайте через property.GetCustomAttribute<MaxLengthAttribute>().
Что делает атрибут Obsolete в C#?
[Obsolete("Use PlaceOrderAsync instead")] на члене заставляет компилятор выдавать предупреждение CS0618 с вашим сообщением в каждом месте вызова. [Obsolete("...", true)] превращает предупреждение в ошибку CS0619. Так библиотеки выводят API из употребления, не ломая вызывающий код в одночасье.
Медленная ли рефлексия в C#?
По сравнению с прямым вызовом да: поиск члена по имени и вызов через MethodInfo.Invoke или PropertyInfo.GetValue обычно в десятки или сотни раз медленнее и упаковывает типы значений. Для запуска, конфигурации и редкого использования это нормально. Для горячих путей кэшируйте MemberInfo, один раз постройте делегат или используйте вместо этого обобщения или генератор исходного кода.