1. Wprowadzenie
Refleksja (od ang. Reflection) — to możliwość twojego programu, żeby badać własną strukturę w czasie wykonywania. Wyobraź sobie, że program może zapytać sam siebie: 'Jakie mam metody? Jakie właściwości ma ten typ?' i otrzymać odpowiedź w runtime.
Przykłady zastosowania refleksji w praktyce
- Tworzenie pluginów i modułów: Nie musisz z góry wiedzieć, z jaką klasą będziesz pracować — wystarczy załadować DLL i poznać jej metody w locie.
- Automatyzacja testów: Frameworki testowe, np. xUnit i NUnit, znajdują metody testowe przez refleksję.
- Serializacja: Dzięki refleksji możesz automatycznie zamienić dowolny obiekt na JSON/XML bez ręcznego iterowania po właściwościach.
- Walidacja danych przez atrybuty: Na przykład dodałeś atrybut [Required] do właściwości — i chcesz automatycznie sprawdzić wszystkie takie właściwości.
Dlaczego warto to znać na rozmowach kwalifikacyjnych
Umiejętność pracy z refleksją często pojawia się na rozmowach, bo pokazuje twoje zrozumienie wnętrza .NET i zdolność rozwiązywania zadań o charakterze dynamicznym. Jeśli chcesz pisać własne frameworki lub rozszerzenia, refleksja jest nieoceniona.
2. Przestrzeń nazw i podstawowe klasy refleksji
Cała funkcjonalność refleksji w C# jest dostępna przez przestrzeń nazw System.Reflection. Na początku pliku nie zapomnij:
using System.Reflection;
Główne typy refleksji
| Klasa/Typ | Opis |
|---|---|
|
Reprezentuje opis typu w .NET (np. klasa, interfejs, enum itd.) |
|
Właściwość typu |
|
Metoda typu |
|
Pole typu |
|
Konstruktor typu |
|
Reprezentuje assembly (EXE lub DLL) |
Podstawowe metody klasy Type
| Metoda | Opis |
|---|---|
|
Zwraca wszystkie publiczne właściwości |
|
Zwraca wszystkie publiczne pola |
|
Zwraca wszystkie publiczne metody |
|
Zwraca wszystkie publiczne konstruktory |
|
Zwraca wszystkie interfejsy, które implementuje typ |
|
Zwraca wszystkie atrybuty zastosowane do typu |
Często do tych metod możesz przekazać BindingFlags by uzyskać dostęp do prywatnych lub statycznych członków, ale na razie ograniczymy się do podstawowego scenariusza.
3. Pierwszy krok: pobranie obiektu Type
Wszystkie techniki refleksji opierają się na pracy z obiektem typu Type.
Istnieje kilka sposobów, żeby dostać ten obiekt, przejrzyjmy każdy:
Sposób 1: Przez obiekt
string text = "Hello, Reflection!";
Type type1 = text.GetType();
Console.WriteLine(type1.Name); // String
Sposób 2: Przez nazwę typu
Type type2 = typeof(int);
Console.WriteLine(type2.FullName); // System.Int32
Sposób 3: Po stringu (dynamicznie)
Type type3 = Type.GetType("System.Double");
Console.WriteLine(type3); // System.Double
Uważaj! Dla własnych klas z innych assembly trzeba podać pełną nazwę typu wraz z assembly.
4. Badamy strukturę typu: właściwości, metody, pola i konstruktory
Nadszedł moment prawdy: dostaliśmy obiekt Type — i teraz możemy dowiedzieć się wszystkiego, co chcesz.
Pobieranie listy właściwości
Type type = typeof(DateTime);
PropertyInfo[] properties = type.GetProperties();
foreach (var prop in properties)
{
Console.WriteLine($"{prop.PropertyType.Name} {prop.Name}");
}
Wyjście:
Int32 Day
Int32 Month
Int32 Year
DayOfWeek DayOfWeek
...
Pobieranie listy metod
MethodInfo[] methods = type.GetMethods();
foreach (var method in methods)
{
Console.WriteLine($"{method.ReturnType.Name} {method.Name}()");
}
Duże typy mogą mieć wiele metod! Często potrzebujesz nie wszystkich, a tylko "swoich" (np. z wyłączeniem metod odziedziczonych z object). Zajmiemy się tym później.
Pobieranie listy pól
FieldInfo[] fields = type.GetFields();
foreach (var field in fields)
{
Console.WriteLine($"{field.FieldType.Name} {field.Name}");
}
Ale większość zwykłych klas nie ma publicznych pól, bo enkapsulacja to nasze wszystko :)
Pobieranie konstruktorów
ConstructorInfo[] constructors = type.GetConstructors();
foreach (var ctor in constructors)
{
Console.WriteLine($"Konstruktor: {ctor}");
}
5. Stosujemy refleksję w praktyce: analiza naszej aplikacji
Przypomnę, że w naszym projekcie edukacyjnym jest klasa TaskItem, w której przechowujemy zadania. Zbadajmy ją przy pomocy refleksji.
public class TaskItem
{
public int Id { get; set; }
public string Title { get; set; }
public DateTime DueDate { get; set; }
public bool IsCompleted;
}
Tak można poznać całą "zawartość" naszego typu w runtime:
Type taskType = typeof(TaskItem);
Console.WriteLine("Właściwości:");
foreach (var prop in taskType.GetProperties())
Console.WriteLine($" {prop.PropertyType.Name} {prop.Name}");
Console.WriteLine("Pola:");
foreach (var field in taskType.GetFields())
Console.WriteLine($" {field.FieldType.Name} {field.Name}");
Console.WriteLine("Metody:");
foreach (var method in taskType.GetMethods())
Console.WriteLine($" {method.ReturnType.Name} {method.Name}()");
Spróbuj dodać do klasy nowe właściwości i zobaczyć, jak refleksja je wykryje. Tak działają uniwersalne serializery i inspektory obiektów.
6. Wizualizacja: jak zbudowany jest obiekt Type (schemat)
+-------------------------+
| Type |
+-------------------------+
| .Name |
| .FullName |
| .Namespace |
| .Assembly |
+-------------------------+
| .GetProperties() |
| .GetFields() |
| .GetMethods() |
| .GetConstructors() |
| .GetInterfaces() |
| .GetCustomAttributes() |
+-------------------------+
Tak wygląda nasz "portret typu" — ma nazwę, assembly, zestaw członków i atrybuty.
Pobieranie podstawowych informacji o typie
Obiekt Type może powiedzieć, czym jest dany typ:
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. Wyszukiwanie konkretnej właściwości, metody lub pola po nazwie
Właściwość
var prop = taskType.GetProperty("Title");
if (prop != null)
{
Console.WriteLine($"Typ właściwości 'Title': {prop.PropertyType}");
}
Metoda
var method = taskType.GetMethod("ToString");
if (method != null)
{
Console.WriteLine($"Metoda 'ToString' zwraca: {method.ReturnType}");
}
Pole
var field = taskType.GetField("IsCompleted");
if (field != null)
{
Console.WriteLine($"Typ pola 'IsCompleted': {field.FieldType}");
}
8. Używamy refleksji do dynamicznego dostępu do wartości
Teraz nie tylko patrzymy, ale i dotykamy wartości!
Odczyt wartości właściwości
TaskItem item = new TaskItem { Id = 1, Title = "Przetestować refleksję", DueDate = DateTime.Today };
PropertyInfo titleProp = item.GetType().GetProperty("Title");
if (titleProp != null)
{
object value = titleProp.GetValue(item);
Console.WriteLine($"Tytuł: {value}");
}
Ustawienie wartości właściwości
titleProp.SetValue(item, "Zmodyfikowany tytuł");
Console.WriteLine(item.Title);
Praca z polami
FieldInfo completedField = item.GetType().GetField("IsCompleted");
completedField.SetValue(item, true);
Console.WriteLine(item.IsCompleted); // true
Dla prywatnych pól i właściwości potrzebne są specjalne flagi, np. BindingFlags.NonPublic. O tym opowiem później.
9. Wywoływanie metod przez refleksję
Możesz wywoływać metody, nawet nie znając ich w czasie pisania kodu!
TaskItem task = new TaskItem { Title = "Refleksja — super!" };
MethodInfo method = task.GetType().GetMethod("ToString");
if (method != null)
{
object result = method.Invoke(task, null);
Console.WriteLine(result);
}
Jeśli metoda ma parametry, trzeba je przekazać jako tablicę obiektów:
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. Dynamiczne tworzenie obiektów przez konstruktor
Refleksja pozwala tworzyć obiekty nawet bez operatora new!
Type taskType = typeof(TaskItem);
object newTask = Activator.CreateInstance(taskType);
// To ten sam TaskItem (ale typ — object)
Console.WriteLine(newTask.GetType().Name); // TaskItem
Można przekazać parametry konstruktora:
public class User
{
public string Name { get; }
public User(string name) { Name = name; }
}
Type userType = typeof(User);
object user = Activator.CreateInstance(userType, "Elis");
Console.WriteLine(((User)user).Name); // Elis
11. Typowe błędy przy pracy z System.Reflection
Błąd nr 1: Odwoływanie się do prywatnych członków bez BindingFlags.
Domyślnie refleksja widzi tylko publiczne członki. Bez BindingFlags.NonPublic dostęp do prywatnych pól lub metod jest niemożliwy.
Błąd nr 2: Ignorowanie sprawdzenia na null.
Metody GetProperty, GetMethod i inne mogą zwrócić null, jeśli członek nie zostanie znaleziony. Bez sprawdzenia to doprowadzi do NullReferenceException.
Błąd nr 3: Nieprawidłowy wybór przeciążenia metody.
Przy wielu przeciążeniach metoda GetMethod może zwrócić niewłaściwą wersję. Używaj GetMethod z podaniem typów parametrów.
Błąd nr 4: Używanie refleksji w kodzie krytycznym wydajnościowo.
Refleksja jest wolniejsza niż bezpośrednie wywołania. Dla kodu wysokowydajnego używaj cache'owania metadanych lub delegatów.
GO TO FULL VERSION