Menu

Refleksja i atrybuty w C#: typeof, GetType i własne atrybuty

Jak działa refleksja w C#: obiekty Type z typeof i GetType, odczyt i ustawianie właściwości, wywoływanie metod po nazwie, tworzenie instancji oraz atrybuty: wbudowane, takie jak Obsolete, deklarowanie własnych i odczytywanie ich w czasie działania. Do tego koszt refleksji.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

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:

  • GetValue zwraca object, więc typy wartościowe wracają opakowane i żeby ich użyć, trzeba je rzutować.
  • GetProperty z nazwą, która nie istnieje, zwraca null, a następne wywołanie na tym wyniku rzuca NullReferenceException. Sprawdzaj przed użyciem.
  • BindingFlags.NonPublic | BindingFlags.Instance się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 Invoke albo GetValue jest dużo wolniejsze od bezpośredniego wywołania i opakowuje typy wartościowe. Przy wielokrotnym użyciu wyszukaj PropertyInfo albo MethodInfo raz i go zachowaj, albo zamień go w delegata za pomocą Delegate.CreateDelegate lub MethodInfo.CreateDelegate i 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żyj is albo IsAssignableFrom.
  • Brak sprawdzenia null. GetProperty, GetMethod i Type.GetType zwracają null, gdy nic nie pasuje.
  • Refleksja w gorącej pętli bez zapamiętywania. Zapamiętaj MemberInfo albo skompiluj delegata.
  • Przechwytywanie niewłaściwego wyjątku z Invoke. Prawdziwy wyjątek to InnerException wyjątku TargetInvocationException.
  • 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ł.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ