1. Вступ
Рефлексія (від англ. Reflection) — це здатність вашої програми досліджувати власну структуру під час виконання. Уявіть, що програма може запитати себе: «Які в мене є методи? Які властивості має цей тип?» — і отримати відповідь під час виконання.
Приклади застосування рефлексії у реальному житті
- Створення плагінів і модулів: вам не потрібно заздалегідь знати, з яким класом ви працюватимете — достатньо завантажити DLL і визначити його методи під час виконання.
- Автоматизація тестування: тестові фреймворки, наприклад, xUnit і NUnit, знаходять тестові методи через рефлексію.
- Серіалізація: за допомогою рефлексії можна автоматично перетворювати будь-який об’єкт у JSON/XML без ручного перебору властивостей.
- Валідація даних за атрибутами: скажімо, ви додали атрибут [Required] до властивості — і хочете автоматично перевірити всі такі властивості.
Навіщо це знати на співбесідах
Про вміння працювати з рефлексією часто запитують на співбесідах, адже це демонструє ваше розуміння внутрішнього влаштування .NET і вміння розв’язувати задачі динамічного характеру. Якщо ви плануєте писати власні фреймворки або розширення, рефлексія незамінна.
2. Простір імен і базові класи рефлексії
Увесь функціонал рефлексії в C# доступний через простір імен System.Reflection. На початку файлу не забудьте:
using System.Reflection;
Основні типи рефлексії
| Клас/Тип | Опис |
|---|---|
|
Представляє опис типу в .NET (наприклад, клас, інтерфейс, enum тощо) |
|
Властивість типу |
|
Метод типу |
|
Поле типу |
|
Конструктор типу |
|
Представляє збірку (EXE або DLL) |
Основні методи класу Type
| Метод | Опис |
|---|---|
|
Повертає усі публічні властивості |
|
Повертає усі публічні поля |
|
Повертає усі публічні методи |
|
Повертає усі публічні конструктори |
|
Повертає всі інтерфейси, які реалізує тип |
|
Повертає всі атрибути, застосовані до типу |
Часто цим методам можна передати BindingFlags для доступу до приватних або статичних членів, але поки зосередимося на базовому сценарії.
3. Перший крок: отримання об’єкта Type
Уся робота з рефлексією базується на об’єкті типу Type.
Є кілька способів отримати цей об’єкт. Розгляньмо кожен із них:
Спосіб 1: через об’єкт
string text = "Hello, Reflection!";
Type type1 = text.GetType();
Console.WriteLine(type1.Name); // String
Спосіб 2: через ім’я типу
Type type2 = typeof(int);
Console.WriteLine(type2.FullName); // System.Int32
Спосіб 3: за рядком (динамічно)
Type type3 = Type.GetType("System.Double");
Console.WriteLine(type3); // System.Double
Будьте уважні: для користувацьких класів з інших збірок слід указувати повне імʼя типу разом із назвою збірки.
4. Досліджуємо структуру типу: властивості, методи, поля й конструктори
Ключовий момент: ми отримали об’єкт Type — і тепер можемо дізнатися все, що потрібно.
Отримання списку властивостей
Type type = typeof(DateTime);
PropertyInfo[] properties = type.GetProperties();
foreach (var prop in properties)
{
Console.WriteLine($"{prop.PropertyType.Name} {prop.Name}");
}
Виведення:
Int32 Day
Int32 Month
Int32 Year
DayOfWeek DayOfWeek
...
Отримання списку методів
MethodInfo[] methods = type.GetMethods();
foreach (var method in methods)
{
Console.WriteLine($"{method.ReturnType.Name} {method.Name}()");
}
У великих типів методів може бути дуже багато. Часто вам потрібні не всі, а лише потрібні саме вам (наприклад, без успадкованих методів із object). До цього повернемося згодом.
Отримання списку полів
FieldInfo[] fields = type.GetFields();
foreach (var field in fields)
{
Console.WriteLine($"{field.FieldType.Name} {field.Name}");
}
Однак більшість прикладних класів не мають публічних полів, адже інкапсуляція — один із ключових принципів.
Отримання конструкторів
ConstructorInfo[] constructors = type.GetConstructors();
foreach (var ctor in constructors)
{
Console.WriteLine($"Конструктор: {ctor}");
}
5. Застосовуємо рефлексію на практиці: аналіз нашого застосунку
Нагадаємо, що в нашому навчальному проєкті є клас TaskItem, у якому ми зберігаємо завдання. Дослідмо його за допомогою рефлексії.
public class TaskItem
{
public int Id { get; set; }
public string Title { get; set; }
public DateTime DueDate { get; set; }
public bool IsCompleted;
}
Ось як можна отримати повний перелік членів нашого типу під час виконання:
Type taskType = typeof(TaskItem);
Console.WriteLine("Властивості:");
foreach (var prop in taskType.GetProperties())
Console.WriteLine($" {prop.PropertyType.Name} {prop.Name}");
Console.WriteLine("Поля:");
foreach (var field in taskType.GetFields())
Console.WriteLine($" {field.FieldType.Name} {field.Name}");
Console.WriteLine("Методи:");
foreach (var method in taskType.GetMethods())
Console.WriteLine($" {method.ReturnType.Name} {method.Name}()");
Спробуйте додати до класу нові властивості та подивіться, як рефлексія їх побачить. Так працюють універсальні серіалізатори та інспектори об’єктів.
6. Візуалізація: як влаштований об’єкт Type (схема)
+-------------------------+
| Type |
+-------------------------+
| .Name |
| .FullName |
| .Namespace |
| .Assembly |
+-------------------------+
| .GetProperties() |
| .GetFields() |
| .GetMethods() |
| .GetConstructors() |
| .GetInterfaces() |
| .GetCustomAttributes() |
+-------------------------+
Так виглядає «портрет типу»: він має імʼя, збірку, набір членів і атрибути.
Отримання базової інформації про тип
Об’єкт Type може розповісти, що це за тип:
Type t = typeof(double);
Console.WriteLine(t.Name); // Double
Console.WriteLine(t.FullName); // System.Double
Console.WriteLine(t.Namespace); // System
Console.WriteLine(t.IsClass); // False
Console.WriteLine(t.IsValueType); // True
Console.WriteLine(t.IsEnum); // False
Console.WriteLine(t.IsPrimitive); // True
7. Пошук конкретної властивості, методу або поля за ім’ям
Властивість
var prop = taskType.GetProperty("Title");
if (prop != null)
{
Console.WriteLine($"Тип властивості 'Title': {prop.PropertyType}");
}
Метод
var method = taskType.GetMethod("ToString");
if (method != null)
{
Console.WriteLine($"Метод 'ToString' повертає: {method.ReturnType}");
}
Поле
var field = taskType.GetField("IsCompleted");
if (field != null)
{
Console.WriteLine($"Тип поля 'IsCompleted': {field.FieldType}");
}
8. Використовуємо рефлексію для динамічного доступу до значень
Тепер ми не лише переглядаємо, а й змінюємо значення.
Читання значення властивості
TaskItem item = new TaskItem { Id = 1, Title = "Протестувати рефлексію", DueDate = DateTime.Today };
PropertyInfo titleProp = item.GetType().GetProperty("Title");
if (titleProp != null)
{
object value = titleProp.GetValue(item);
Console.WriteLine($"Заголовок: {value}");
}
Встановлення значення властивості
titleProp.SetValue(item, "Змінений заголовок");
Console.WriteLine(item.Title);
Робота з полями
FieldInfo completedField = item.GetType().GetField("IsCompleted");
completedField.SetValue(item, true);
Console.WriteLine(item.IsCompleted); // true
Для приватних полів і властивостей потрібні спеціальні прапори, наприклад, BindingFlags.NonPublic. Про це поговоримо пізніше.
9. Виклик методів через рефлексію
Методи можна викликати, навіть не знаючи їх заздалегідь під час написання коду.
TaskItem task = new TaskItem { Title = "Рефлексія — це зручно!" };
MethodInfo method = task.GetType().GetMethod("ToString");
if (method != null)
{
object result = method.Invoke(task, null);
Console.WriteLine(result);
}
Якщо метод із параметрами, їх потрібно передавати як масив об’єктів:
public class Calculator
{
public int Add(int a, int b) => a + b;
}
var calc = new Calculator();
var addMethod = typeof(Calculator).GetMethod("Add");
object[] parameters = { 5, 3 };
object sumResult = addMethod.Invoke(calc, parameters);
Console.WriteLine(sumResult); // 8
10. Динамічне створення об’єктів через конструктор
Рефлексія дозволяє створювати об’єкти навіть без оператора new!
Type taskType = typeof(TaskItem);
object newTask = Activator.CreateInstance(taskType);
// Це той самий TaskItem (але тип — object)
Console.WriteLine(newTask.GetType().Name); // TaskItem
Можна передати параметри конструктора:
public class User
{
public string Name { get; }
public User(string name) { Name = name; }
}
Type userType = typeof(User);
object user = Activator.CreateInstance(userType, "Еліс");
Console.WriteLine(((User)user).Name); // Еліс
11. Типові помилки під час роботи з System.Reflection
Помилка № 1: звернення до приватних членів без BindingFlags.
За замовчуванням рефлексія бачить лише публічні члени. Без BindingFlags.NonPublic доступ до приватних полів або методів неможливий.
Помилка № 2: ігнорування перевірки на null.
Методи GetProperty, GetMethod та інші можуть повернути null, якщо член не знайдено. Без перевірки це призведе до NullReferenceException.
Помилка № 3: неправильний вибір перевантаження методу.
За наявності кількох перевантажень метод GetMethod може повернути не той варіант. Використовуйте GetMethod із зазначенням типів параметрів.
Помилка № 4: використання рефлексії у високопродуктивних ділянках коду.
Рефлексія повільніша за прямі виклики. Для високопродуктивного коду використовуйте кешування метаданих або делегати.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ