Generics, Typy generyczne referują się do typów, których definicja metody, klasy i interfejs operują zależnie od tego, co podasz do niego.
Typy generyczne mają wiele zalet jedną z nich jest wykrywanie ich działania już na poziomie pisania kodu oraz to, że pozwalają na uniknięcie konwersji typów.
Ograniczanie parametrów generycznych : Java
Istnieje możliwość dodania restrykcji do typów generycznych w Javie. C# ma ich więcej, ale na razie rzućmy okiem na Jave.
Ograniczenia w Javie nazywają się “bounds” . Używając słowa kluczowego extends określamy fakt, że dany parametr generyczny musi dziedziczyć lub być daną klasą.
// T must be or inherit from superclass A class B<T extends A> {} class A {}
Alternatywnie implementować dany interfejs.
// T must be or implement interface I class C<T extends I> {} interface I {}
Wiele ograniczeń tego typu może zostać dodanych w ten sposób.
class D<T extends A & I> {}
Przecinkiem oddziela się poszczególne zasady dla poszczególnych parametrów generycznych.
class E<T extends A & I, U extends A & I> {}
Zaletą ograniczeń jest fakt, że skoro kompilator wie, że typ parametru na pewno będzie daną klasą lub będzie po niej dziedziczyć, daje to możliwość uzyskania dostępu do elementów tej klasy, mimo iż informacja o typie wciąż jest niewiadomą.
public class Vegetable { public String name; } public class VegetableBox<T extends Fruit> { private T box; public void VegetableBox(T t) { box = t; } public String get VegetableName() { // Use of Vegetable member allowed since T extends Vegetable return box.name; } }
Przejdźmy do C#.
Constraints C#
Definiując klasy generyczne lub metody można wymusić na poziomie kompilacji kodu pewne ograniczenia. Te ograniczenia w C# nazywaj się constraints.
Po definicji klasy piszemy słowo kluczowe “where”, a po nim definiujemy ograniczenie. Przykładowo możemy zastrzec, że typ T umieszczony do klasy musi być strukturą, czyli typem wartościowym
class C<T> where T : struct {} // value type
Alternatywnie w podobny sposób możemy zastrzec, że klasa generyczna jako parametr generyczny może przyjmować tylko typy referencyjne.
class D<T> where T : class {} // reference type
Możemy też zastrzec, że dany parametr generyczny musi dziedziczyć po określonej klasie.
class B { public string Some { get; set; } class E<T> where T : B // be/derive from base class { public T MyProperty { get; set; } }
Jest to dosyć użyteczne. Kompilator wie, że T na pewno będzie tą klasą, albo będzie dziedziczyć po tej klasie – dzięki temu mogę używać metody, właściwości tej klasy w parametrze.
Mogę też stworzyć ograniczenie, które wymusza dziedziczenie parametrów generycznych po sobie. Parametr T musi dziedziczyć po parametrze U.
class F<T, U> where T : U {} // be/derive from U
Nie ma też problemu ze stworzeniem ograniczenia dotyczącym implementacji danych interfejsów.
interface I {} class G<T> where T : I {} // be/implement interface
Ostatnim możliwym ograniczeniem jest posiadanie bezparametrowego konstruktora. Parametr T wiec zaakceptuje klasy, które mają bezparametrowy konstruktor.
class H<T> where T : new() {} // no parameter constructor
Oczywiście można dodać kilka ograniczeń do jednego parametru. Po przecinku definiuje się kolejne ograniczenie do tego samego parametru. Gdy mamy wiele parametrów ograniczenie do kolejnego parametru wymusza kolejne słowo kluczowe where.
class J<T, U> where T : class, I where U : I, new() {}
Dlaczego warto korzystać z ograniczeń?
Tak jak pisałem wcześniej informacji o tym, że typ parametru generycznego będzie na pewno tą klasą lub będzie dziedziczyć po niej, daje nam możliwość dostępu do elementów tej klasy.
class Person { public string name; } class PersonStorage<T> where T : Person { public string box; public void StorePersonName(T a) { box = a.name; } }
Ograniczenie new() pozwala nam tworzyć instancje parametru T.
class MyClass<T> where T : new() {}
Skoro wiemy, że typ T ma na pewno bezparametrowy konstruktor, daje nam to możliwość skorzystania z niego.
class MyClass<T> where T : new() { public MyClass() { T t = new T(); } }
Warto zaznaczyć, że jeśli klasa ma ograniczenie parametru typu, to jego klasa pochodna też musi mieć to ograniczenie. Inaczej nasza aplikacja się nie skompiluje.
Oto prawidłowy kod:
class MyClass<T> where T : new() { } class MyChild<T> : MyClass<T> where T : new() { }