System Internals Concurrency

Reflection API: System.Type та Метадані

Глибоке занурення у механізми рефлексії C#: System.Type, інспекція метаданих, динамічний виклик членів, та практичне застосування.

Reflection API: System.Type та Метадані

Вступ: Коли Код Вивчає Сам Себе

Уявіть собі програму, яка не знає наперед, з якими типами вона буде працювати. Це може бути:

  • ORM (Object-Relational Mapping), який відображає довільні класи на таблиці бази даних.
  • Dependency Injection контейнер, що створює екземпляри класів, про які він дізнається лише під час виконання.
  • Серіалізатор JSON, який перетворює будь-який об'єкт у текст.
  • Фреймворк для тестування, що знаходить і викликає методи з атрибутом [Test].

У традиційному статично типізованому коді ми маємо писати new MyClass() — компілятор знає про MyClass на етапі компіляції. Але що, якщо нам потрібно створити екземпляр класу, ім'я якого зберігається в рядку?

Саме тут виходить на сцену Reflection (Рефлексія) — здатність програми інспектувати та маніпулювати своєю власною структурою під час виконання.

Reflection — це механізм часу виконання (runtime), який дозволяє коду отримувати інформацію про типи (metadata) та взаємодіяти з ними динамічно, без явної компіляції проти конкретного типу.

Реальний Сценарій

Припустимо, ви розробляєте плагін-систему. Користувач кладе DLL-файл у директорію plugins, а ваша програма має:

  1. Завантажити цю збірку (assembly).
  2. Знайти в ній усі класи, що реалізують інтерфейс IPlugin.
  3. Створити їх екземпляри та викликати метод Execute().

Ви не можете написати new SomePlugin(), бо не знаєте назву класу до моменту запуску. Reflection робить це можливим.

Застереження про продуктивність: Reflection у 10-100 разів повільніший за прямий виклик. Використовуйте його там, де гнучкість важливіша за швидкість, або кешуйте результати (наприклад, MethodInfo).

Фундаментальні Концепції

System.Type: Серце Рефлексії

Центральний клас у рефлексії — System.Type. Кожен тип у .NET (клас, структура, інтерфейс, enum) представлений екземпляром Type.

Три способи отримати Type:

Type stringType = typeof(string);
Type listType = typeof(List<int>);

Ключові відмінності:

СпосібЧас виконанняВикористання
typeof(T)Compile-timeКоли тип відомий на етапі компіляції
obj.GetType()RuntimeОтримання реального типу об'єкта
Type.GetType(name)Runtime (slow)Коли ім'я типу зберігається в рядку/конфігурації
Loading diagram...
graph TD
    A[Compile Time] -->|typeof| B[Type Object]
    C[Runtime Object] -->|GetType| B
    D[String Name] -->|Type.GetType| B
    B --> E[Metadata Access]
    E --> F[Properties]
    E --> G[Methods]
    E --> H[Constructors]
    E --> I[Custom Attributes]

    style B fill:#3b82f6,stroke:#1d4ed8,color:#ffffff
    style E fill:#f59e0b,stroke:#b45309,color:#ffffff
    style A fill:#64748b,stroke:#334155,color:#ffffff
    style C fill:#64748b,stroke:#334155,color:#ffffff
    style D fill:#64748b,stroke:#334155,color:#ffffff

Type як Metadata Container

Об'єкт Type не містить даних екземпляра — він описує структуру типу. Подумайте про нього як про "інструкцію з експлуатації" класу.

Type userType = typeof(User);

// Властивості Type
Console.WriteLine(userType.Name);          // "User"
Console.WriteLine(userType.FullName);      // "MyApp.Models.User"
Console.WriteLine(userType.Namespace);     // "MyApp.Models"
Console.WriteLine(userType.IsClass);       // True
Console.WriteLine(userType.IsValueType);   // False
Console.WriteLine(userType.IsSealed);      // False
Console.WriteLine(userType.BaseType);      // System.Object

Пояснення рядків:

  • Рядок 4: Name — ім'я типу без namespace.
  • Рядок 5: FullName — повне ім'я включно з namespace (важливо для унікальності).
  • Рядок 7: IsClass vs IsValueType — розрізнення reference types та value types.
  • Рядок 10: BaseType — базовий клас (null для інтерфейсів).

Архітектура Reflection API

###Ієрархія Класів Reflection

.NET надає окремі класи для кожного виду членів типу:

Loading diagram...
classDiagram
    class Type {
        +Name: string
        +FullName: string
        +GetMembers() MemberInfo[]
        +GetMethod(name) MethodInfo
        +GetProperty(name) PropertyInfo
        +GetConstructors() ConstructorInfo[]
    }

    class MemberInfo {
        <<abstract>>
        +Name: string
        +DeclaringType: Type
        +GetCustomAttributes()
    }

    class MethodInfo {
        +ReturnType: Type
        +Invoke(obj, params)
        +IsStatic: bool
    }

    class PropertyInfo {
        +PropertyType: Type
        +GetValue(obj)
        +SetValue(obj, value)
    }

    class FieldInfo {
        +FieldType: Type
        +GetValue(obj)
        +SetValue(obj, value)
    }

    class ConstructorInfo {
        +Invoke(params) object
    }

    Type --> MemberInfo : describes
    MemberInfo <|-- MethodInfo
    MemberInfo <|-- PropertyInfo
    MemberInfo <|-- FieldInfo
    MemberInfo <|-- ConstructorInfo

    style Type fill:#3b82f6,stroke:#1d4ed8,color:#ffffff
    style MemberInfo fill:#f59e0b,stroke:#b45309,color:#ffffff

Under the Hood: Metadata Tables

Коли ви компілюєте C# код, компілятор генерує не тільки IL (Intermediate Language), але й metadata tables. Ці таблі зберігаються в збірці (DLL/EXE) і містять інформацію про:

  • TypeDef: Визначення типів.
  • MethodDef: Методи з сигнатурами.
  • FieldDef: Поля з типами.
  • CustomAttribute: Атрибути.

Коли ви викликаєте typeof(User), CLR читає ці таблиці й конструює об'єкт Type.

Інструмент ILSpy або dnSpy дозволяє переглянути metadata будь-якої збірки. Спробуйте відкрити вашу DLL там — ви побачите, як CLR "бачить" ваш код.

Інспекція Метаданих: Практичні Приклади

1. Отримання Списку Всіх Властивостей

using System.Reflection;

public class User
{
    public int Id { get; set; }
    public string Name { get; set; } = "";
    private string _password = "secret";
}

Type userType = typeof(User);

// Отримати всі публічні властивості
PropertyInfo[] publicProps = userType.GetProperties();

foreach (PropertyInfo prop in publicProps)
{
    Console.WriteLine($"{prop.Name} ({prop.PropertyType.Name})");
    // Output:
    // Id (Int32)
    // Name (String)
}

// Отримати приватні поля (flags: BindingFlags)
FieldInfo[] privateFields = userType.GetFields(
    BindingFlags.NonPublic | BindingFlags.Instance
);

foreach (FieldInfo field in privateFields)
{
    Console.WriteLine($"{field.Name} ({field.FieldType.Name})");
    // Output: _password (String)
}

Важливо про BindingFlags:

Прапорець (Flag)Опис
PublicПублічні члени
NonPublicПриватні/protected
InstanceЧлени екземпляра
StaticСтатичні члени
DeclaredOnlyТільки теперішній тип (не від base)
FlattenHierarchyВключити статичні члени з базових класів
Використовуйте комбінацію flags через оператор | (OR). За замовчуванням GetProperties() повертає тільки Public | Instance.

2. Динамічний Виклик Методу

public class Calculator
{
    public int Add(int a, int b) => a + b;
    private int Multiply(int a, int b) => a * b;
}

Calculator calc = new();
Type calcType = typeof(Calculator);

// Публічний метод
MethodInfo? addMethod = calcType.GetMethod("Add");
if (addMethod != null)
{
    object? result = addMethod.Invoke(calc, new object[] { 5, 3 });
    Console.WriteLine(result); // 8
}

// Приватний метод (потрібен BindingFlags.NonPublic)
MethodInfo? multiplyMethod = calcType.GetMethod("Multiply",
    BindingFlags.NonPublic | BindingFlags.Instance);

if (multiplyMethod != null)
{
    object? result = multiplyMethod.Invoke(calc, new object[] { 4, 7 });
    Console.WriteLine(result); // 28
}

Пояснення рядка 14:

  • Invoke(object target, object[] parameters):
    • target — екземпляр об'єкта (null для статичних методів).
    • parameters — масив аргументів (boxing для value types).
Boxing Penalty: Параметри передаються як object[], що призводить до boxing примітивних типів. Для критичних за продуктивністю сценаріїв використовуйте compiled expressions або source generators.

3. Читання та Запис Властивостей

User user = new() { Id = 1, Name = "Alice" };
Type userType = typeof(User);

PropertyInfo? nameProp = userType.GetProperty("Name");

if (nameProp != null)
{
    // Читання
    string? currentName = nameProp.GetValue(user) as string;
    Console.WriteLine(currentName); // "Alice"

    // Запис
    nameProp.SetValue(user, "Bob");
    Console.WriteLine(user.Name); // "Bob"
}

Альтернатива з Generic:

string? name = nameProp?.GetValue(user) as string;

Створення Екземплярів: Activator та ConstructorInfo

Спосіб 1: Activator.CreateInstance

Найпростіший спосіб створити об'єкт з Type:

Type listType = typeof(List<int>);
object? instance = Activator.CreateInstance(listType);

if (instance is List<int> list)
{
    list.Add(42);
    Console.WriteLine(list[0]); // 42
}

// З параметрами конструктора
Type userType = typeof(User);
User? user = Activator.CreateInstance(userType, new object[] { 123, "John" }) as User;
Activator.CreateInstance повільний і не безпечний для типів (повертає object?). Для багаторазового створення використовуйте кешовані делегати.

Спосіб 2: ConstructorInfo (більш контролю)

Type userType = typeof(User);

// Знайти конструктор з параметрами
ConstructorInfo? ctor = userType.GetConstructor(new[] { typeof(int), typeof(string) });

if (ctor != null)
{
    User user = (User)ctor.Invoke(new object[] { 99, "Alice" });
    Console.WriteLine(user.Name); // "Alice"
}

Порівняння підходів:

МетодШвидкістьТипобезпекаВикористання
Activator.CreateInstanceПовільнийНіОдноразове створення
ConstructorInfo.InvokeПовільнийНіКоли потрібен контроль параметрів
Compiled ExpressionШвидкийТакДля повторюваних операцій (DI containers)

Робота з Generic Types

Generic типи у рефлексії мають особливий синтаксис.

Приклад: List<int>

// Open generic type (без аргументів типу)
Type openListType = typeof(List<>);
Console.WriteLine(openListType.IsGenericTypeDefinition); // True

// Closed generic type (з конкретним T)
Type closedListType = typeof(List<int>);
Console.WriteLine(closedListType.IsGenericType); // True

// Отримати аргументи типу
Type[] genericArgs = closedListType.GetGenericArguments();
Console.WriteLine(genericArgs[0]); // System.Int32

// Створити закритий тип з відкритого
Type constructedType = openListType.MakeGenericType(typeof(string));
object? stringList = Activator.CreateInstance(constructedType);
// stringList тепер є List<string>

Важливо:

  • Open generic: List<> — шаблон.
  • Closed generic: List<int> — конкретна реалізація.

Практичний Кейс: Cloner Об'єктів

Напишемо функцію, яка глибоко копіює будь-який об'єкт через рефлексію.

using System.Reflection;

public static class ObjectCloner
{
    public static T? Clone<T>(T source)
    {
        if (source == null) return default;

        Type type = typeof(T);

        // Створити новий екземпляр
        T? clone = (T?)Activator.CreateInstance(type);
        if (clone == null) return default;

        // Копіювати всі публічні властивості
        foreach (PropertyInfo prop in type.GetProperties())
        {
            if (!prop.CanWrite || !prop.CanRead) continue;

            object? value = prop.GetValue(source);
            prop.SetValue(clone, value);
        }

        // Копіювати приватні поля (optional)
        foreach (FieldInfo field in type.GetFields(BindingFlags.NonPublic | BindingFlags.Instance))
        {
            object? value = field.GetValue(source);
            field.SetValue(clone, value);
        }

        return clone;
    }
}

// Використання
User original = new() { Id = 1, Name = "Alice" };
User? copy = ObjectCloner.Clone(original);
Console.WriteLine(copy?.Name); // "Alice"

Обмеження:

  • Не копіює вкладені об'єкти (shallow copy).
  • Не працює з об'єктами без конструктора за замовчуванням.
Production-ready рішення: Використовуйте бібліотеки як AutoMapper або DeepCloner, які оптимізують це через compiled expressions.

Продуктивність Рефлексії

Benchmarks

using BenchmarkDotNet.Attributes;

public class ReflectionBenchmark
{
    private readonly Calculator _calc = new();
    private readonly MethodInfo _addMethod = typeof(Calculator).GetMethod("Add")!;

    [Benchmark(Baseline = true)]
    public int DirectCall() => _calc.Add(5, 3);

    [Benchmark]
    public int ReflectionCall() => (int)_addMethod.Invoke(_calc, new object[] { 5, 3 })!;
}

Результати (приблизно):

МетодЧасШвидкість
DirectCall0.5 ns1x
ReflectionCall50 ns100x повільніше

Оптимізації

  1. Кешування MethodInfo: Не викликайте GetMethod щоразу.
  2. Compiled Expressions:
using System.Linq.Expressions;

public static Func<Calculator, int, int, int> CreateDelegate()
{
    MethodInfo method = typeof(Calculator).GetMethod("Add")!;

    ParameterExpression instance = Expression.Parameter(typeof(Calculator));
    ParameterExpression arg1 = Expression.Parameter(typeof(int));
    ParameterExpression arg2 = Expression.Parameter(typeof(int));

    MethodCallExpression call = Expression.Call(instance, method, arg1, arg2);

    return Expression.Lambda<Func<Calculator, int, int, int>>(call, instance, arg1, arg2).Compile();
}

// Використання (швидкість майже як прямий виклик)
var addDelegate = CreateDelegate();
int result = addDelegate(new Calculator(), 5, 3); // 8
Compiled expressions перетворюють рефлексію у делегат, який викликається зі швидкістю прямого виклику після одноразової компіляції.

Assembly та Module

Завантаження Збірок

using System.Reflection;

// Завантажити поточну збірку
Assembly currentAssembly = Assembly.GetExecutingAssembly();
Console.WriteLine(currentAssembly.FullName);

// Завантажити збірку з файлу
Assembly loadedAssembly = Assembly.LoadFrom("MyPlugin.dll");

// Отримати всі типи зі збірки
Type[] types = loadedAssembly.GetTypes();

foreach (Type type in types)
{
    if (type.IsClass && !type.IsAbstract && typeof(IPlugin).IsAssignableFrom(type))
    {
        // Знайдено клас, що реалізує IPlugin
        IPlugin? plugin = Activator.CreateInstance(type) as IPlugin;
        plugin?.Execute();
    }
}

Методи Assembly:

МетодОпис
GetExecutingAssembly()Поточна збірка
Load(string name)Завантаження з GAC або поточної директорії
LoadFrom(string path)Завантаження з конкретного шляху
GetTypes()Всі типи зі збірки
GetType(string name)Пошук типу за повним іменем
AssemblyLoadContext: У .NET Core/.NET 5+ використовуйте AssemblyLoadContext.Default.LoadFromAssemblyPath() для кращої ізоляції та unloading.

Reflection Emit: Генерація Коду

Reflection.Emit дозволяє генерувати IL код у пам'яті під час виконання. Це основа для ORM, serializers, proxy-генераторів.

Ця тема дуже просунута. Для практичного використання розгляньте Source Generators (.NET 5+), які генерують C# код на етапі компіляції.

Приклад створення типу динамічно:

using System.Reflection;
using System.Reflection.Emit;

AssemblyName assemblyName = new("DynamicAssembly");
AssemblyBuilder assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(
    assemblyName, AssemblyBuilderAccess.Run
);

ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule("MainModule");

TypeBuilder typeBuilder = moduleBuilder.DefineType(
    "DynamicType",
    TypeAttributes.Public
);

// Додати поле
FieldBuilder fieldBuilder = typeBuilder.DefineField(
    "_value",
    typeof(int),
    FieldAttributes.Private
);

// Створити тип
Type? dynamicType = typeBuilder.CreateType();
object? instance = Activator.CreateInstance(dynamicType!);

Практичні Завдання

Рівень 1: Inspector

Напишіть програму TypeInspector, яка:

  1. Приймає назву типу як аргумент командного рядка.
  2. Виводить:
    • Всі публічні властивості з типами.
    • Всі публічні методи з параметрами та типами повернення.
    • Базовий клас.

Приклад виводу:

Type: System.String
Base: System.Object

Properties:
  - Length: Int32

Methods:
  - ToUpper(): String
  - Substring(Int32, Int32): String

Рівень 2: Generic Factory

Створіть клас Factory<T>, який:

  1. Приймає масив параметрів конструктора.
  2. Знаходить відповідний конструктор через рефлексію.
  3. Створює екземпляр T.
Factory<User> factory = new();
User user = factory.Create(42, "Alice");

Рівень 3: Mapper

Реалізуйте AutoMapper.Map<TSource, TDest>(TSource source), який:

  1. Копіює значення властивостей з TSource у TDest.
  2. Підтримує різні імена властивостей через атрибут [MapFrom("SourcePropName")].
UserDto dto = AutoMapper.Map<User, UserDto>(user);

Резюме

System.Type Центральний клас рефлексії, що представляє метадані типу. Отримується через typeof(), GetType(), або Type.GetType().
Reflection API Набір класів (MethodInfo, PropertyInfo, FieldInfo) для інспекції та виклику членів типу під час виконання.
BindingFlags Прапорці для контролю пошуку членів (Public/NonPublic, Instance/Static, DeclaredOnly).
Activator Утиліта для створення екземплярів типів динамічно. Повільна, але зручна для одноразових операцій.
Золоте Правило: Використовуйте рефлексію рідко. Якщо код виконується в циклі — оптимізуйте через кешування або compiled expressions.

Додаткові Ресурси


Наступна тема: Attributes та Dynamic Language Runtime