Masz obiekt typu object i chcesz wiedzieć, z jaką klasą naprawdę pracujesz? A może budujesz mechanizm oparty na refleksji, generykach lub rejestracji zależności? Operator typeof w C# zwraca obiekt System.Type, ale jego działanie łatwo pomylić z GetType() albo operatorem is. Poniżej pokazuję różnice, praktyczne przykłady, obsługę typów generycznych oraz pułapki, które często pojawiają się w kodzie .NET.
Najważniejsze informacje o typeof w C#
-
typeofzwraca obiektSystem.Typereprezentujący wskazany typ. - Operator przyjmuje nazwę typu lub parametr generyczny, a nie dowolne wyrażenie.
-
GetType()sprawdza rzeczywisty typ obiektu w czasie działania programu. -
isbada zgodność typu, także w przypadku dziedziczenia. - W generykach można użyć
typeof(T), a także odwołać się do otwartych typów generycznych.
Co dokładnie zwraca operator typeof
Najprostszy przykład wygląda tak:
Type typLiczby = typeof(int);
Console.WriteLine(typLiczby.Name); // Int32
Console.WriteLine(typLiczby.FullName); // System.Int32Wynikiem nie jest wartość int, lecz obiekt klasy System.Type. Ten obiekt przechowuje informacje o typie, między innymi jego nazwę, przestrzeń nazw, właściwości, metody, konstruktory oraz informacje o dziedziczeniu. To właśnie dlatego typeof jest często pierwszym krokiem do refleksji.
Operator działa zarówno dla typów wbudowanych, jak i własnych klas:
Type typTekstu = typeof(string);
Type typDaty = typeof(DateTime);
Type typUzytkownika = typeof(Uzytkownik);
public class Uzytkownik
{
public string Email { get; set; } = string.Empty;
}Nie tworzę tutaj żadnego obiektu. Wyrażenie typeof(Uzytkownik) nie wywołuje konstruktora i nie alokuje instancji klasy. Otrzymuję jedynie opis typu, który mogę później wykorzystać na przykład do odczytania właściwości albo znalezienia odpowiedniego konstruktora.
Typ można też porównywać bezpośrednio:
if (typLiczby == typeof(int))
{
Console.WriteLine("To jest int.");
}Takie porównanie jest jednoznaczne i odporne na zmianę nazwy typu. Zwykle jest lepszym wyborem niż sprawdzanie tekstu zwróconego przez Name, ponieważ nazwa może być niepełna albo niejednoznaczna.
typeof, GetType i is rozwiązują różne problemy
Najczęstsze nieporozumienie polega na traktowaniu tych konstrukcji jako zamienników. Nie są nimi. typeof opisuje typ znany w kodzie, GetType() odczytuje typ konkretnego obiektu, a is sprawdza, czy obiekt jest zgodny z określonym typem.
| Konstrukcja | Co sprawdza | Przykład |
|---|---|---|
typeof |
Typ podany bezpośrednio w kodzie | typeof(Klient) |
GetType() |
Rzeczywisty typ konkretnej instancji | obiekt.GetType() |
is |
Zgodność obiektu z typem, także przez dziedziczenie | obiekt is Klient |
Różnicę dobrze widać na przykładzie dziedziczenia:
class Zwierze
{
}
class Pies : Zwierze
{
}
object obiekt = new Pies();
Console.WriteLine(obiekt is Zwierze); // True
Console.WriteLine(obiekt.GetType() == typeof(Zwierze)); // False
Console.WriteLine(obiekt.GetType() == typeof(Pies)); // TrueOperator is zwraca true, ponieważ pies jest zwierzęciem. Porównanie GetType() == typeof(Zwierze) zwraca jednak false, bo rzeczywisty typ instancji to Pies. Ta różnica ma znaczenie w obsłudze polimorfizmu, czyli sytuacji, gdy zmienna bazowego typu przechowuje obiekt klasy pochodnej.
Jeżeli interesuje mnie dokładny typ obiektu, wybieram GetType(). Jeżeli chcę sprawdzić, czy obiekt może być używany jako określony typ, częściej właściwy będzie is:
if (obiekt is Zwierze zwierze)
{
Console.WriteLine("Zwierzę: " + zwierze.GetType().Name);
}Trzeba też pamiętać o wartości null. Wywołanie GetType() na pustej referencji kończy się wyjątkiem NullReferenceException, natomiast null is JakiśTyp zwraca false. Samo typeof(JakiśTyp) jest bezpieczne, bo nie operuje na instancji.
typeof w generykach i dla typów złożonych
Jednym z najlepszych zastosowań operatora jest kod generyczny. Parametr typu może być nieznany w momencie pisania metody, ale kompilator nadal pozwala użyć konstrukcji typeof(T):
static void WyswietlTyp(T wartosc)
{
Console.WriteLine(typeof(T).FullName);
}
WyswietlTyp(42); // System.Int32
WyswietlTyp("tekst"); // System.String W tym przykładzie typeof(T) opisuje parametr typu użyty przy wywołaniu metody. To przydatne między innymi w generycznych loggerach, serializerach, fabrykach oraz rejestrach usług.
Można również odwołać się do zamkniętego typu generycznego, czyli takiego, któremu podano konkretne argumenty:
Type listaTekstow = typeof(List);
Type slownik = typeof(Dictionary);
Console.WriteLine(listaTekstow);
Console.WriteLine(slownik); Istnieje także forma typu generycznego bez określonych argumentów:
Type definicjaListy = typeof(List<>);
Type definicjaSlownika = typeof(Dictionary<,>);Zapis List<> oznacza typ generyczny z jednym parametrem, a Dictionary<,> typ z dwoma parametrami. Liczba przecinków musi odpowiadać liczbie parametrów pomniejszonej o jeden. Taka forma jest szczególnie przydatna przy analizowaniu generyków za pomocą refleksji.
Warto odróżnić definicję typu od typu skonstruowanego. typeof(List<>) opisuje ogólną definicję listy, natomiast typeof(List opisuje konkretną listę przechowującą napisy. W kodzie refleksyjnym ta różnica często decyduje o tym, czy uda się poprawnie utworzyć instancję lub odczytać argumenty generyczne.
Praktyczne zastosowania w refleksji i .NET
Sam obiekt Type nie daje jeszcze gotowej funkcjonalności, ale otwiera dostęp do metadanych typu. Można sprawdzić jego właściwości, metody albo atrybuty:
Type typ = typeof(Uzytkownik);
foreach (var wlasciwosc in typ.GetProperties())
{
Console.WriteLine($"{wlasciwosc.Name}: {wlasciwosc.PropertyType.Name}");
}Tak działają między innymi proste mechanizmy mapowania, walidacji i serializacji. W praktyce używam Type jako klucza do decyzji podejmowanych dynamicznie, na przykład do wyboru właściwego konwertera dla konkretnej klasy.
Przykładowy rejestr obsługi typów może wyglądać tak:
var obslugi = new Dictionary
{
[typeof(string)] = "Konwerter tekstu",
[typeof(int)] = "Konwerter liczby",
[typeof(DateTime)] = "Konwerter daty"
};
Type typDanych = typeof(int);
if (obslugi.TryGetValue(typDanych, out var nazwaObslugi))
{
Console.WriteLine(nazwaObslugi);
} To podejście jest czytelniejsze niż porównywanie nazw klas zapisanych jako tekst. Nie polecam jednak używania refleksji wszędzie, gdzie wystarczy zwykły kod statyczny. Refleksja daje elastyczność, ale zwiększa złożoność, utrudnia analizę przez kompilator i może być wolniejsza w bardzo często wykonywanych fragmentach.
Jeżeli typ jest wykorzystywany w pętli lub podczas obsługi wielu żądań, można przechować wynik w pamięci. Sam operator typeof jest tani, ale późniejsze operacje refleksyjne, takie jak wyszukiwanie właściwości czy tworzenie obiektów, mogą już uzasadniać cache.
Typowe błędy przy używaniu typeof
Przekazanie obiektu zamiast typu
Do operatora nie można przekazać dowolnego wyrażenia:
var tekst = "hello";
// Błąd kompilacji
// Type typ = typeof(tekst);Jeśli chcę poznać typ zmiennej w czasie działania, używam tekst.GetType(). Jeśli typ jest znany z góry, zapisuję jego nazwę bezpośrednio, na przykład typeof(string).
Pomylenie typów nullable
Typ wartościowy z możliwością wartości null jest prawidłowym argumentem:
Type liczbaNullable = typeof(int?);
Console.WriteLine(liczbaNullable);
Console.WriteLine(Nullable.GetUnderlyingType(liczbaNullable));Inaczej wygląda sytuacja z nullable reference types:
// Błąd kompilacji
// Type typ = typeof(string?);Adnotacja string? pomaga kompilatorowi analizować możliwość wystąpienia wartości null, ale nie tworzy osobnego typu w metadanych środowiska uruchomieniowego. Dlatego w takim przypadku używa się typeof(string).
Przeczytaj również: Ile miejsca zajmuje int w C#? 4 bajty, zakres i wybór typu
Dynamic, var i typeof
Nie można użyć typeof(var) ani typeof(dynamic). var jest jedynie skrótem, który kompilator rozwiązuje na etapie kompilacji, a dynamic zmienia sposób wiązania wywołań, lecz nie reprezentuje osobnego typu CLR.
var liczba = 10;
// Poprawnie
Type typ = typeof(int);
// Niepoprawnie
// Type typ1 = typeof(var);
// Type typ2 = typeof(dynamic);Gdy potrzebuję informacji o wartości przechowywanej w zmiennej typu dynamic, mogę użyć GetType(), o ile zmienna nie ma wartości null. To drobna różnica składniowa, ale często wyjaśnia błędy pojawiające się podczas pracy z kodem dynamicznym.
Jak wybrać właściwą konstrukcję w codziennym kodzie
Moja praktyczna reguła jest prosta. Gdy znam typ w kodzie, wybieram typeof. Gdy mam konkretną instancję i interesuje mnie jej rzeczywisty typ, używam GetType(). Gdy sprawdzam możliwość użycia obiektu jako typu bazowego lub interfejsu, korzystam z is.
-
typeof(Klient)stosuj do uzyskania obiektuTypedla znanego typu. -
obiekt.GetType()stosuj do odczytu rzeczywistego typu instancji. -
obiekt is IKlientstosuj do sprawdzenia zgodności z interfejsem lub klasą bazową. -
typeof(T)stosuj w metodach i klasach generycznych.
Największy błąd nie polega na nieznajomości składni, lecz na użyciu poprawnej konstrukcji do niewłaściwego celu. typeof nie bada obiektu i nie zastępuje operatora is. Zwraca opis typu, który później można wykorzystać w refleksji, porównaniu lub logice generycznej.
Jeżeli zapamiętasz tę różnicę, większość przypadków stanie się oczywista: typ znany w kodzie oznacza typeof, typ konkretnej instancji oznacza GetType(), a sprawdzanie zgodności oznacza is. To niewielki fragment składni C#, ale fundament wielu mechanizmów działających wewnątrz .NET.
