JavaRush /Курси /C# SELF /Рефлексія: System.Reflecti...

Рефлексія: System.Reflection

C# SELF
Рівень 63 , Лекція 0
Відкрита

1. Вступ

Рефлексія (від англ. Reflection) — це здатність вашої програми досліджувати власну структуру під час виконання. Уявіть, що програма може запитати себе: «Які в мене є методи? Які властивості має цей тип?» — і отримати відповідь під час виконання.

Приклади застосування рефлексії у реальному житті

  • Створення плагінів і модулів: вам не потрібно заздалегідь знати, з яким класом ви працюватимете — достатньо завантажити DLL і визначити його методи під час виконання.
  • Автоматизація тестування: тестові фреймворки, наприклад, xUnit і NUnit, знаходять тестові методи через рефлексію.
  • Серіалізація: за допомогою рефлексії можна автоматично перетворювати будь-який об’єкт у JSON/XML без ручного перебору властивостей.
  • Валідація даних за атрибутами: скажімо, ви додали атрибут [Required] до властивості — і хочете автоматично перевірити всі такі властивості.

Навіщо це знати на співбесідах

Про вміння працювати з рефлексією часто запитують на співбесідах, адже це демонструє ваше розуміння внутрішнього влаштування .NET і вміння розв’язувати задачі динамічного характеру. Якщо ви плануєте писати власні фреймворки або розширення, рефлексія незамінна.

2. Простір імен і базові класи рефлексії

Увесь функціонал рефлексії в C# доступний через простір імен System.Reflection. На початку файлу не забудьте:

using System.Reflection;

Основні типи рефлексії

Клас/Тип Опис
Type
Представляє опис типу в .NET (наприклад, клас, інтерфейс, enum тощо)
PropertyInfo
Властивість типу
MethodInfo
Метод типу
FieldInfo
Поле типу
ConstructorInfo
Конструктор типу
Assembly
Представляє збірку (EXE або DLL)

Основні методи класу Type

Метод Опис
GetProperties()
Повертає усі публічні властивості
GetFields()
Повертає усі публічні поля
GetMethods()
Повертає усі публічні методи
GetConstructors()
Повертає усі публічні конструктори
GetInterfaces()
Повертає всі інтерфейси, які реалізує тип
GetCustomAttributes()
Повертає всі атрибути, застосовані до типу

Часто цим методам можна передати 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: використання рефлексії у високопродуктивних ділянках коду.
Рефлексія повільніша за прямі виклики. Для високопродуктивного коду використовуйте кешування метаданих або делегати.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ