Refleksja (reflection) pozwala programowi badać typy w trakcie działania: jakie właściwości ma klasa, jakie mają wartości, jakie metody istnieją, jakie atrybuty są dołączone. Atrybuty to druga połowa: deklaratywne znaczniki w nawiasach kwadratowych, takie jak [Obsolete] czy [JsonPropertyName("id")], które same w sobie nic nie znaczą, dopóki kompilator albo jakiś kod nie odczyta ich przez refleksję. Tak działają serializatory, ORM-y, biblioteki walidacji, frameworki testowe i routing w ASP.NET.
Większość kodu aplikacji rzadko potrzebuje refleksji bezpośrednio, ale wiedza o tym, jak działa, wyjaśnia wiele zachowań frameworków.
Obiekty Type: typeof i GetType
Wszystko zaczyna się od System.Type. Są dwa sposoby, żeby go zdobyć:
Wynik:
Employee
Manager
Employee
False
True
True
System.Int32
False
Name
Różnica do zapamiętania: typeof(X) nazywa typ, który znasz w chwili pisania kodu, a obj.GetType() pyta obiekt, czym naprawdę jest. Porównanie GetType() == typeof(Employee) to dokładne dopasowanie, które zawodzi dla klas pochodnych, a zwykle nie o to chodzi; is i IsAssignableFrom respektują dziedziczenie. nameof wygląda podobnie, ale w ogóle nie jest refleksją: kompilator zastępuje je stałą tekstową.
GetType() na referencji null rzuca NullReferenceException, bo nie ma obiektu, którego można by zapytać. Na opakowanym typie wartościowym nullable zwraca typ bazowy: ((int?)5).GetType() to System.Int32.
Odczyt i ustawianie właściwości
GetProperties() wypisuje publiczne właściwości typu jako obiekty PropertyInfo, z których każdy potrafi odczytać i zapisać wartość na danej instancji:
Wynik:
Name String = Mug
Price Decimal = 8.50
Stock Int32 = 12
7.90
True
SUP-77
Ten przykład pokazuje trzy rzeczy:
GetValuezwracaobject, więc typy wartościowe wracają opakowane i żeby ich użyć, trzeba je rzutować.GetPropertyz nazwą, która nie istnieje, zwracanull, a następne wywołanie na tym wyniku rzucaNullReferenceException. Sprawdzaj przed użyciem.BindingFlags.NonPublic | BindingFlags.Instancesięga do prywatnych składowych. W narzędziach i testach jest to uzasadnione, ale omija hermetyzację i po cichu psuje się przy refaktoryzacji klasy.
Ta pętla to w zasadzie sposób działania eksportera CSV albo serializatora obiektów do JSON: przejdź po właściwościach, odczytaj każdą wartość i ją sformatuj.
Wywoływanie metod i tworzenie obiektów po nazwie
GetMethod znajduje metodę, a Invoke wywołuje ją z tablicą argumentów. Activator.CreateInstance tworzy obiekt na podstawie Type; tak systemy wtyczek i kontenery DI budują typy wybrane w czasie działania:
Wynik:
60.00
Decimal WithTax(1 parameters)
String Describe(0 parameters)
True
DeclaredOnly ogranicza listę do składowych zadeklarowanych w samej klasie; bez tego GetMethods zwraca też ToString, Equals, GetHashCode i GetType z object. Type.GetType("Name") wymaga nazwy kwalifikowanej przestrzenią nazw, a dla typów z innych assembly także nazwy assembly ("MyApp.Plugins.Csv, MyApp.Plugins").
Jeśli wywołana metoda rzuci wyjątek, Invoke opakowuje go w TargetInvocationException; oryginał znajduje się w jego InnerException.
Atrybuty: znaczniki czytane przez kompilator i frameworki
Atrybut zapisuje się w nawiasach kwadratowych przed elementem, który opisuje. Framework definiuje ich wiele; oto kilka, które zmieniają działanie kompilatora:
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
Inne są odczytywane przez biblioteki w czasie działania: [JsonPropertyName] i [JsonIgnore] przez System.Text.Json, [Required] i [MaxLength] przez walidację modeli w ASP.NET Core i Entity Framework, [HttpGet("orders/{id}")] przez routing ASP.NET, [Fact] i [Test] przez narzędzia uruchamiające testy. Sam atrybut nic nie robi; robi to kod, który go szuka.
Nazwa ObsoleteAttribute skraca się przy zastosowaniu do [Obsolete]: zgodnie z konwencją nazwa każdej klasy atrybutu kończy się na Attribute, a C# pozwala pominąć ten przyrostek.
Deklarowanie i odczyt własnego atrybutu
Własny atrybut to klasa dziedzicząca po Attribute. [AttributeUsage] określa, do czego można go zastosować. Parametry konstruktora stają się argumentami pozycyjnymi, a publiczne właściwości z setterem argumentami nazwanymi:
Wynik:
Username must be at most 20 characters
Email is required
Keep the city code short
0
To miniaturowa wersja tego, co robi walidacja modeli w ASP.NET Core z System.ComponentModel.DataAnnotations. Argumenty atrybutów muszą być stałymi czasu kompilacji (liczbami, stringami, typeof(...), wartościami enumów albo ich tablicami), bo są przechowywane w metadanych assembly. GetCustomAttribute<T>() to metoda rozszerzająca z System.Reflection; jest też IsDefined(typeof(T)), gdy chcesz tylko wiedzieć, czy atrybut jest obecny.
Koszt refleksji
Refleksja wymienia szybkość i bezpieczeństwo na elastyczność:
- Szybkość. Wyszukanie składowej po nazwie i wywołanie jej przez
InvokealboGetValuejest dużo wolniejsze od bezpośredniego wywołania i opakowuje typy wartościowe. Przy wielokrotnym użyciu wyszukajPropertyInfoalboMethodInforaz i go zachowaj, albo zamień go w delegata za pomocąDelegate.CreateDelegatelubMethodInfo.CreateDelegatei wywołuj właśnie jego. - Bezpieczeństwo. Literówka w nazwie albo zmieniona sygnatura kompilują się bez problemu i zawodzą w czasie działania. Gdzie tylko się da, wybieraj
nameof(Product.Price)zamiast stringa"Price", żeby zmiany nazw zostały wyłapane. - Trimming i AOT. Aplikacje przycinane (trimmed) i Native AOT usuwają składowe, których pozornie nic nie używa, a refleksja ukrywa użycie przed tą analizą. Nowoczesne biblioteki (
System.Text.Json,GeneratedRegex, logowanie) przechodzą na generatory źródeł, które wykonują tę samą pracę w czasie kompilacji.
Używaj refleksji w tych częściach programu, które naprawdę nie znają swoich typów z góry: we wtyczkach, ogólnych narzędziach, serializatorach i pomocnikach testowych. Gdy typy są znane, zwykły kod, typy generyczne albo interfejsy są szybsze i sprawdzane przez kompilator.
Typowe błędy
GetType() == typeof(Base)do sprawdzania klasy bazowej. Zawodzi dla typów pochodnych. UżyjisalboIsAssignableFrom.- Brak sprawdzenia
null.GetProperty,GetMethodiType.GetTypezwracająnull, gdy nic nie pasuje. - Refleksja w gorącej pętli bez zapamiętywania. Zapamiętaj
MemberInfoalbo skompiluj delegata. - Przechwytywanie niewłaściwego wyjątku z
Invoke. Prawdziwy wyjątek toInnerExceptionwyjątkuTargetInvocationException. - Magiczne stringi jako nazwy składowych. Używaj
nameof.
Najczęściej zadawane pytania
Czym jest refleksja w C#?
Refleksja (reflection) to zdolność programu do badania typów w czasie działania: wypisania właściwości i metod klasy, odczytu i ustawiania wartości po nazwie, wywoływania metod, tworzenia instancji i odczytu atrybutów. Znajduje się w System.Reflection i zaczyna od obiektu Type. Zbudowane są na niej serializatory, ORM-y, kontenery wstrzykiwania zależności i frameworki testowe.
Jaka jest różnica między typeof a GetType w C#?
typeof(Customer) jest rozstrzygane w czasie kompilacji na podstawie nazwy typu i nie potrzebuje obiektu. obj.GetType() jest wywoływane na instancji w czasie działania i zwraca rzeczywisty typ obiektu, który może być bardziej pochodny niż zadeklarowany typ zmiennej: dla Animal a = new Dog(); wywołanie a.GetType() daje Dog. GetType() na referencji null rzuca NullReferenceException.
Jak pobrać wartość właściwości po nazwie w C#?
obj.GetType().GetProperty("Price") zwraca PropertyInfo (albo null, jeśli nie ma takiej publicznej właściwości), a .GetValue(obj) odczytuje ją jako object. .SetValue(obj, value) ją zapisuje. Zapamiętaj PropertyInfo, jeśli robisz to w pętli, bo kosztowne jest właśnie wyszukiwanie.
Jak utworzyć własny atrybut w C#?
Zadeklaruj klasę dziedziczącą po System.Attribute, nadaj jej nazwę z przyrostkiem Attribute i oznacz, gdzie można jej użyć, za pomocą [AttributeUsage]: [AttributeUsage(AttributeTargets.Property)] class MaxLengthAttribute : Attribute { public int Length { get; } public MaxLengthAttribute(int length) { Length = length; } }. Zastosuj go jako [MaxLength(50)] i odczytaj przez property.GetCustomAttribute<MaxLengthAttribute>().
Co robi atrybut Obsolete w C#?
[Obsolete("Use PlaceOrderAsync instead")] na składowej sprawia, że kompilator w każdym miejscu wywołania zgłasza ostrzeżenie CS0618 z twoim komunikatem. [Obsolete("...", true)] zamienia ostrzeżenie w błąd CS0619. Tak biblioteki wycofują API, nie psując wywołujących z dnia na dzień.
Czy refleksja jest wolna w C#?
W porównaniu z bezpośrednim wywołaniem tak: znalezienie składowej po nazwie i wywołanie jej przez MethodInfo.Invoke albo PropertyInfo.GetValue jest zwykle od kilkudziesięciu do kilkuset razy wolniejsze i opakowuje typy wartościowe (boxing). Przy starcie, konfiguracji i okazjonalnym użyciu to nie problem. W gorących ścieżkach zapamiętaj MemberInfo, zbuduj raz delegata albo użyj typów generycznych lub generatora źródeł.