C# 6.0 - kurz & gut

von: Joseph Albahari, Ben Albahari

O'Reilly Verlag, 2016

ISBN: 9783960100461 , 232 Seiten

4. Auflage

Format: PDF, ePUB, OL

Kopierschutz: Wasserzeichen

Windows PC,Mac OSX für alle DRM-fähigen eReader Apple iPad, Android Tablet PC's Apple iPod touch, iPhone und Android Smartphones Online-Lesen für: Windows PC,Mac OSX,Linux

Preis: 14,90 EUR

Mehr zum Inhalt

C# 6.0 - kurz & gut


 

Generics


C# verfügt über zwei separate Mechanismen, um Code zu schreiben, der mit verschiedenen Typen verwendbar ist: Vererbung und Generics. Während bei der Vererbung die Wiederverwendbarkeit durch einen Basistyp ausgedrückt wird, geschieht dies bei Generics durch ein »Template«, das Typen als »Platzhalter« enthält. Generics können im Vergleich zur Vererbung die Typsicherheit erhöhen und für weniger Casting und Boxing sorgen.

Generische Typen

Ein generischer Typ deklariert Typparameter – Platzhaltertypen, die vom Anwender des generischen Typs gefüllt werden, indem er die Typargumente bereitstellt. Hier wurde ein generischer Typ Stack entworfen, der Instanzen vom Typ T auf einem Stack verwalten soll. Stack deklariert einen einzelnen Typparameter T:

public class Stack
{
  int position;
  T[] data = new T[100];
  public void Push (T obj) => data[position++] = obj;
  public T Pop()           => data[--position];
}

Wir können Stack so nutzen:

var stack = new Stack();
stack.Push(5);
stack.Push(10);
int x = stack.Pop();        // x ist 10
int y = stack.Pop();        // y ist 5

Beachten Sie, dass in den beiden letzten Zeilen keine Downcasts erforderlich sind, was das Risiko eines Laufzeitfehlers und den Overhead der Boxing/Unboxing-Operationen vermeidet. Das macht unseren generischen Stack einem nichtgenerischen Stack überlegen, der object anstelle von T nutzt (ein Beispiel finden Sie unter »Der Typ object« auf Seite 87).

Stack gibt für den Typparameter T das Typargument int vor, wodurch implizit ein Typ erstellt wird (die Synthese geschieht zur Laufzeit). Stack hat im Endeffekt folgende Definition (die Ersetzungen sind hervorgehoben und der Klassenname wurde durch ### ersetzt, um Verwirrung zu vermeiden):

public class ###
{
  int position;
  int[] data;
  public void Push (int obj) => data[position++] = obj;
  public int Pop()           => data[--position];
}

Technisch ausgedrückt, ist Stack ein offener Typ, Stack dagegen ein geschlossener Typ. Zur Laufzeit sind alle Instanzen generischer Typen geschlossen – ihre Typplatzhalter sind gefüllt.

Generische Methoden

Eine generische Methode deklariert Typparameter innerhalb ihrer Signatur. Mit generischen Methoden können viele grundlegende Algorithmen sehr allgemein implementiert werden. Hier sehen Sie eine generische Methode, die zwei Werte eines beliebigen Typs T vertauscht:

static void Swap (ref T a, ref T b)
{
  T temp = a; a = b; b = temp;
}

Swap kann wie folgt verwendet werden:

int x = 5, y = 10;
Swap (ref x, ref y);

Im Allgemeinen ist es nicht notwendig, Typargumente an eine generische Methode zu übergeben, da der Compiler den Typ implizit ermitteln kann. Würde das zu einer Mehrdeutigkeit führen, können generische Methoden mit dem Typargument aufgerufen werden:

Swap (ref x, ref y);

Innerhalb eines generischen Typs ist eine Methode so lange nichtgenerisch, wie sie nicht selbst Typparameter einführt (mit den spitzen Klammern). In unserem generischen Stack nutzt die Methode Pop nur den schon im Typ bestehenden Typparameter T und ist daher nicht als generische Methode klassifiziert.

Methoden und Typen sind die einzigen Konstrukte, die Typparameter einführen können. Eigenschaften, Indexer, Events, Felder, Operatoren und so weiter können keine Typparameter deklarieren, auch wenn sie die Typparameter verwenden können, die vom umhüllenden Typ deklariert wurden. In unserem Beispiel mit dem generischen Stack könnten wir zum Beispiel einen Indexer schreiben, der ein generisches Element zurückgibt:

public T this [int index] { get { return data[index]; } }

Auch Konstruktoren können nur die vorhandenen Typparameter nutzt, aber nicht selbst welche einführen.

Deklarieren generischer Parameter

Typparameter können bei der Deklaration von Klassen, Structs, Interfaces, Delegates (siehe »Delegates« auf Seite 110) und Methoden eingeführt werden. Ein generischer Typ bzw. eine generische Methode kann mehrere Typparameter haben:

class Dictionary {...}

Die Instanziierung erfolgt folgendermaßen:

var myDic = new Dictionary();

Generische Typnamen und Methodennamen können überladen werden, solange sich die Anzahl der Typparameter unterscheidet. So kommen zum Beispiel die folgenden drei Typnamen nicht miteinander in Konflikt:

class A {}
class A {}
class A {}

Es ist üblich, bei generischen Typen und Methoden mit einem einzelnen Typparameter diesen Parameter T zu nennen, solange der Sinn des Parameters klar ist. Bei mehreren Typparametern beginnt jeder Parameter mit T, hat aber einen stärker beschreibenden Namen.

typeof und ungebundene generische Typen

Zur Laufzeit gibt es keine offenen generischen Typen: Offene generische Typen werden bei der Kompilation geschlossen. Aber es kann zur Laufzeit ungebundene generische Typen geben – nur als Type-Objekt. Einen ungebundenen generischen Typ können Sie in C# nur mit dem typeof-Operator angeben:

class A {}
class A {}
...

Type a1 = typeof (A<>);   // Ungebundener Typ
Type a2 = typeof (A<,>);  // Zeigt 2 Typargumente an
Console.Write (a2.GetGenericArguments().Count());  // 2

Sie können den typeof-Operator nutzen, um einen geschlossenen Typ anzugeben

Type a3 = typeof (A);

oder einen offenen Typ (der zur Laufzeit geschlossen ist):

class B { void X() { Type t = typeof (T); } }

Der generische Wert default

Das Schlüsselwort default kann genutzt werden, um den Standardwert für einen angegebenen generischen Typparameter zu erhalten. Der Standardwert für einen Referenztyp ist null, der für Werttypen ist das Ergebnis eines bitweisen Löschens der Felder des Typs:

static void Zap (T[] array)
{
  for (int i = 0; i < array.Length; i++)
    array[i] = default(T);
}

Generische Constraints

Standardmäßig kann ein Typparameter durch jeden beliebigen Typ ersetzt werden. Constraints können auf einen Typparameter angewandt werden, um spezifischere Typargumente zu verlangen. Es gibt sechs Arten von Constraints:

where T : Basisklasse   // Basisklassen-Constraint
where T : Interface     // Interface-Constraint
where T : class         // Referenztyp-Constraint
where T : struct        // Werttyp-Constraint
where T : new()         // Parameterloser Konstruktor
                        // Constraint
where U : T             // Typ-Constraint

Im folgenden Beispiel verlangt GenericClass, dass T von SomeClass abgeleitet ist (oder der Klasse selbst entspricht) und Interface1 implementiert, und verlangt außerdem, dass U einen parameterlosen Konstruktor anbietet:

class     SomeClass {}
interface Interface1 {}

class GenericClass where T : SomeClass, Interface1
                        where U : new()
{ ... }

Constraints können überall dort angewendet werden, wo generische Parameter definiert sind, sowohl in Methoden als auch in Typdefinitionen.

Ein Basisklassen-Constraint verlangt, dass der Typparameter eine Subklasse einer bestimmten Klasse sein muss (oder die Klasse selbst); ein Interface-Constraint fordert, dass der Typparameter dieses Interface implementieren muss. Diese Constraints gestatten es, dass Instanzen des Typparameters implizit in diese Klasse oder dieses Interface umgewandelt werden.

Der class-Constraint und der struct-Constraint erfordern, dass T ein Referenztyp beziehungsweise ein (nicht nullbarer) Werttyp ist. Der parameterlose Konstruktor-Constraint erfordert, dass T einen...