40-783: Podstawy Typów Generycznych

Podobnie jak w Javie, C# posiada typy generyczne. Język ten nie zmusza nas jednak do parametryzowania tylko z użyciem reference types – możemy użyć też value types. Drugą znaczną różnicą jest to, że o ile w Javie nie mamy (powiedzmy) informacji o typie generycznym w runtimie – o tyle w C# taka informacja jest do wyciągniecia przez mechanizmy refleksji.

O samej refleksji pomówimy sobie jednak kiedy indziej. Dzisiaj “czysta” generyczność.

Składnia

Parametryzować możemy klasę (składnia taka sama jak w Javie):

public class MyGenericType<T>
{
	// …
}

lub metodę (składnia trochę inna niż w Javie):

public void MyGenericMethod<T>(T arg)
{
	// ...
}

T w tej notacji nazywamy “type parameter”. Nie ma problemu, żeby overloadować metodę tylko po przez różne type parameters.

Możemy wywołać generyczną metodę nie parametryzując jej. Kompilator powinien być w stanie samemu wywnioskować typ z przekazanych argumentów (type inference) – nie zawsze jest to możliwe, np. przy generycznej metodzie bezparametrowej.

Type parameter constraints

W momencie kiedy nasz type parameter będzie miał postać T, to możemy go dowolnie parametryzować. Moglibyśmy jednak chcieć zawęzić możliwe do wykorzystania typy (podobnie jak to się robi w Javie wykorzystując extends/super). Czyli, inaczej mówiąc, moglibyśmy chcieć nałożyć constraint na type parameter. Główna zaleta jest taka, że możemy używać szerszego api. Przykładowo, jeżeli zażyczymy sobie, żeby T implementowało System.IComparable to T będzie mógło użyć metody CompareTo.

Constraitns w C# ma ogólną postać:

where T : … 

i zapisujemy to na końcu klasy, np:

public class MyGenericType<T> where T : OtherType
{
	// …
}

A w przypadku metody:

public void MyGenericMethod<T>(T arg) where T : OtherType
{
	// ...
}

Oto możliwe do wykorzystania constraintsy:

where T : struct // tylko value types
where T : class // tylko reference types. 
where T : new() // T musi posiadać domyślny konstruktor 
where T : SomeClass // T musi dziedziczyć po SomeClass
where T : SomeInterface // T musi implementować SomeInterface
where T : U // T musi wywodzić się z type parameter U 

Dwie ważne uwagi:

  • Constraint new() – zawsze musi być ostatnim na liście.
  • Constraint class – zawsze dla podanego typu będą użyte nieprzeciążone wersje operatorów == i != nawet jeżeli będziemy parametryzować typem, który takie operatory przeciąża.

Jeżeli już jesteśmy przy rzeczach na które warto zwrócić uwagę:

public class MyGenericType<T>
{
	public void MyGenericMethod&amp<T>(T arg) 
	{ 
			//… 
	}
}

Zauważmy, że i klasa i metodą jest parametryzowana typem T – i to nie jest ten sam typ. W tym momencie T z metody przesłania T z klasy (warrning podczas kompilacji). Jeżeli chodziło nam o to, żeby argument przekazywany do metody był parametryzowany na poziomie klasy, to oczywiście nie ma potrzeby wtedy parametryzować na poziomie metody. Jeżeli jednak chcieliśmy faktycznie użyć innego type parameter dla metody to po prostu wybierajmy inną literkę.

Typy generyczne i tablice

Trochę mindfuck i nie do końca na temat, ale…

Jednowymiarowa tablica w C# implementuje interfejs IList<T>. Implementuje z pewnymi zastrzeżeniami, ale o tym za chwilę. Ze względu na ten fakt, wszędzie tam gdzie mamy generyczną metodę która akceptuje Ilist <T>- możemy przekazać tablicę typu T. Ale uwaga – tablica widoczna przez interfejs IList jest tylko do odczytu. Nie możemy dodawać i usuwać z niej po przez metody z interfejsu listy – próba wywołania pozdrowi nas wyjątkiem. Wewnątrz naszej metody możemy się bronić przed taką niedozwoloną akcją sprawdzając wcześniej co nam zwróci metoda IsReadOnly

Przed chwilą napisałem, że jednowymiarowa tablica będzie implementować interejs listy, ale nie jest to do końca ścisła definicja. Jest jeszcze druga zasada jaka musi być spełniona, żeby nastąpiła auto-implementacja – cytując z MSDN:

“Single-dimensional arrays that have a lower bound of zero automatically implement IList.”

Może to być zaskakujące dla kogoś ze świata Javy ale istnieje możliwość stworzenia tablicy, której indeksowanie NIE zaczyna się od 0. Z tego co się zorientowałem jest to raczej nietrywialne w C# i wyobrażam sobie, że zastosowanie takiej konstrukcji zdarzy nam się z raz w życiu – więc zostawiam to jako ciekawostkę.

Słowo kluczowe default

Rozpatrzmy pewną problematyczną sytuację – mianowicie mamy generyczną metodę:

public void TestMethod<T>()
{
	// …
}

Chcielibyśmy mieć nową instancję typu T w jej wnętrzu. Ale jak to zrobić? Przecież nie wiemy jak dokładnie będzie sparametryzowana nasza metoda. Nie wiadomo czy pod T przypisać null’a, zero, czy cokolwiek. Moglibyśmy użyć constraintsa new(), ale to nie jest do końca to samo. Z pomocą przychodzi nam następująca konstrukcja:

T t = default(T);

W runtimie taka składnia zachowa się w natępujący sposób:

  • Dla reference type przypisze nulla.
  • Dla value types, które są wartościami liczbowymi przypisze 0.
  • Dla struktur stworzy strukturę, której wszystkie pola są zainicjalizowane tak jak dla powyższych dwóch sytuacji.

Typy generyczne a runtime.

Jak wiadomo w Javie w runtimie tracimy (powiedzmy) informacje na temat generyków. W C# jest inaczej – informacja o typie generycznym jest dostępna w czasie działania aplikacji. Dzięki temu możliwe są konstrukcje, które w Javie są niedostępne. Przykładowo nasza klasa może kilkakrotnie zaimplementować ten sam generyczny interfejs z innymi parametrami. Możemy też pobierać informacje o generykach za pomocą refleksji ale tak jak wspomniałem na samym początku – o refleksji kiedy indziej.

Kod źródłowy

Oto projekt na githubie gdzie można znaleźć przykłady, rzeczy które zostały omówione w artykule.

https://github.com/mprzybylak/minefields-csharp/tree/master/typesystem

Źródła

  1. T. Covaci – MCSD Certification Toolkit (Exam 70-483): Programming in C# – rodział 3: „Working with the Type System”
  2. Introduction to Generics (C# Programming Guide)
  3. Constraints on Type Parameters (C# Programming Guide)
  4. Generics and Arrays (C# Programming Guide)
  5. Generics and Arrays (C# Programming Guide)
  6. .Net arrays with lower bound > 0
  7. Why doesn’t VB support non-zero lower bounds for arrays?
  8. default Keyword in Generic Code (C# Programming Guide)
  9. Generics in the Run Time (C# Programming Guide)
  10. Generics and Reflection (C# Programming Guide)