ConstrainsCzęść NR.18

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.

uzskanie dostępu

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.

dziedziczenie typu generycznego

Oto prawidłowy kod:

class MyClass<T> where T : new()
{

}

class MyChild<T> : MyClass<T>
where T : new()
{ }