Das .NET Framework ist umfangreich und dank seiner mittlerweile 4 (bzw. 5) Versionen auch etwas unübersichtlich geworden. Vor allem wenn es um Schlüsselworte geht kann der geneigte Nutzer schon einmal den Überblick verlieren. Aus diesem Grund habe ich mir heute drei heraus gepickt die im Umgang mit generischen Datentypen verwendet werden. Zugegebenermaßen ist dies zum Teil absolutes Grundlagenwissen, dennoch werden zumindest zwei der drei in diesem Zusammenhang gern übersehen.

Das where Schlüsselwort ist das bekannteste und erlaubt sogenannte Type Constrains. Es grenzt demnach den generischen Typ einer Klasse ein und verringert damit die Fehleranfälligkeit des Codes und verhindert lange Überprüfungsketten. Einschränkungen die damit getroffen werden bestehen auch innerhalb der Vererbungshierarchie, können jedoch weiter spezialisiert werden:

public class Foo<T>;
   where T : ICloneable
{
}
public class Bar<T, K> : Foo<T>;
       where T : FooBar, ICloneable
       where K : ICloneable
{
}

Nicht verwendet werden können in diesem Zusammenhang Enum, ValueType, delegate und object. Möchte man aber beispielsweise Referenztypen erzwingen, kann man class nutzen, während struct Werttypen verlangt. Die Einschränkung fällt demnach kaum ins Gewicht.

Eine weiteres Konstrukt das sich dank where ergibt, sind sogenannte naked type constrains. Diese beschreiben im Grunde nur die Vererbungshierarchien der verwendeten Typen. Im nachfolgenden Beispiel muss die hinzuzufügende Collection beispielweise immer auch von dem Typ sein dessen Instanz sie hinzugefügt werden soll. Die Angabe verhindert somit, dass ein IEnumerable<T> einer List<T> hinzugefügt wird, wärend List<T> durchaus einem IEnumerable<T> hinzugefügt werden kann. Über den Realismusgehalt des Beispiels lässt sich zugegebener Maßen streiten.

public class MyCollectionHandler<T>
{
   public void AddRange<U>(U range) where U : T
   {
       . . .
   }
}
public void FooClass
{
   public void UseCollectionHandler()
   {
      var listHandler = new MyCollectionHandler <IEnumerable<int>>();
      List<int> content = new List<int> { 1,2,3,4,5,6};
      listHandler.AddRange(content);
   }
}

Und um den Hattrick komplett zu machen, sei an dieser Stelle noch das recursive type constraint genannt (habe ich da etwa jemand Bingo rufen hören? 😉 ). Davon ist die Rede wenn man den Typ auf einen Datentyp einschränkt, der den Datentyp selbst enthält. Dies kann unter Umständen auch naked sein, wobei es dann sehr verwirrend wird…

public class Bar<T>
       where T : Foo<T>
{
}

Schlüsselwort Nummer 2 ist new. Mit ihm wird vorgeschrieben, dass der zu übergebende Typ einen parameterlosen Konstruktor besitzen muss. Dabei kann wirklich nur der parameterlose Konstruktor vorgeschrieben werden und keine Parameterliste.

public class Foo<T>
       where T :  IDisposable, new()
{
   protected T _instance;
   public T Bar()
   {
      if(_instance == null)
         _instance = new T();
      return _instance;
   }
}

Nur so am Rande: Der Null coalescing Operator funktioniert an dieser Stelle nicht da er nicht mit generischen Datentypen umgehen kann. Das default Schlüsselwort kennt man bereits von switch-case und ist aus meiner Sicht, das am wenigsten bekannte im Zusammenhang mit generischen Typen. Mit ihm ist es möglich eine Variable auf einen Standardwert zu initialisieren ohne ihren Typ zu kennen.

public T Initialize<T>()
{
    return default(T);
}

Bei numerischen Typen wie float, double usw. ist dies in der Regel 0. Bei DateTime wird die Ticks Property auf 0 gesetzt und somit der 01.01.0001 als Initialwert verwendet. Bei allen Referenztypen, Strings also auch (!!!), wird verständlicherweise auf null initialisiert, während bei Strukturen alle numerischen Member mit 0 und alle Referenzmember mit null initialisiert werden.

Warum sollte man es verwenden? Schreibt man int i, wird i doch auch richtig mit 0 initialisiert! Stimmt, aber bei generischen Typen funktioniert diese implizite Initialisierung nicht und muss deshalb explizit angegeben werden. Der Grund dafür ist, dass die generischen Typen erst vom Compiler aufgelöst werden und man ihm hiermit unter die Arme greift.

Aus selbigem Grund funktionieren auch Vergleiche nicht. Folgender Code würde (leider) nicht einmal kompilieren, denn da T nicht bekannt ist kann auch keine Aussage darüber getroffen werden ob es überhaupt einen Vergleichsoperator gibt:

public bool IsInitialValue<T>(T value)
{
    return default(T) == value;
}

Um dies zu ermöglichen muss der Typ von T wieder eingeschränkt werden. Nachfolgendes Beispiel funktioniert aber wie man sich denken kann nur mit Referenztypen.

public bool IsInitialValue<T>(T value) where T : class
{
    return default(T) == value;
}