Generika
Was sind Generika?
Die Schnittstelle IMemento, die zum Speichern und Wiederherstellen von Daten dient, verfügt
über einen eklatanten Nachteil: In der bislang verwendeten Form ist sie auf Daten vom Typ
float beschränkt.
| C# |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
using System;
namespace GoloRoden.GuideToCSharp
{
/// <summary>
/// Provides methods for memento classes.
/// </summary>
public interface IMemento
{
/// <summary>
/// Stores the specified value using the specified
/// key.
/// </summary>
/// <param name="key">The key.</param>
/// <param name="value">The value.</param>
void Store(string key, float value);
/// <summary>
/// Restores the value stored with the specified
/// key.
/// </summary>
/// <param name="key">The key.</param>
/// <returns>The value.</returns>
float Restore(string key);
}
}
|
Bei der Verwendung der Schnittstelle mit der Klasse ComplexNumber hat sich diese
Einschränkung nicht ausgewirkt, da dort nur Daten vom Typ
float verwendet werden.
Falls die Schnittstelle jedoch mehr Datentypen unterstützen soll, was spätestens
dann benötigt wird, wenn die Schnittstelle allgemeingültig für zahlreiche
verschiedene Klassen eingesetzt werden soll, macht sich diese Einschränkung
deutlich bemerkbar.
Die einfachste Variante, die Schnittstelle um die benötigten Datentypen zu erweitern,
liegt darin, die entsprechenden Methoden zu ergänzen. Bei der Methode Store bedeutet
dies zwar einigen Aufwand, prinzipiell ist es aber überhaupt möglich, da sich die
einzelnen überladenen Methoden im Typ des zweiten Parameters unterscheiden. Im
folgenden Code wurde die Schnittstelle um eine Methode zum Speichern von Zeichenketten
erweitert.
| C# |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
using System;
namespace GoloRoden.GuideToCSharp
{
/// <summary>
/// Provides methods for memento classes.
/// </summary>
public interface IMemento
{
/// <summary>
/// Stores the specified value using the specified
/// key.
/// </summary>
/// <param name="key">The key.</param>
/// <param name="value">The value.</param>
void Store(string key, float value);
/// <summary>
/// Stores the specified value using the specified
/// key.
/// </summary>
/// <param name="key">The key.</param>
/// <param name="value">The value.</param>
void Store(string key, string value);
/// <summary>
/// Restores the value stored with the specified
/// key.
/// </summary>
/// <param name="key">The key.</param>
/// <returns>The value.</returns>
float Restore(string key);
}
}
|
Abgesehen von dem notwendigen Aufwand, eine prinzipiell immer gleiche Methode zu
definieren, funktioniert dieser Ansatz bei der Methode Restore nicht: Da als Parameter
immer ein
string übergeben wird und sich die Methoden nur durch den Typ des Rückgabewertes
unterscheiden würden, ist ein Überladen nicht möglich. Als Ausweg bietet es sich an, den
Typ des Rückgabewertes in den Methodennamen aufzunehmen, um die Methoden unterscheidbar
zu machen.
| C# |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
using System;
namespace GoloRoden.GuideToCSharp
{
/// <summary>
/// Provides methods for memento classes.
/// </summary>
public interface IMemento
{
/// <summary>
/// Stores the specified value using the specified
/// key.
/// </summary>
/// <param name="key">The key.</param>
/// <param name="value">The value.</param>
void Store(string key, float value);
/// <summary>
/// Stores the specified value using the specified
/// key.
/// </summary>
/// <param name="key">The key.</param>
/// <param name="value">The value.</param>
void Store(string key, string value);
/// <summary>
/// Restores the value stored with the specified
/// key.
/// </summary>
/// <param name="key">The key.</param>
/// <returns>The value.</returns>
float RestoreAsFloat(string key);
/// <summary>
/// Restores the value stored with the specified
/// key.
/// </summary>
/// <param name="key">The key.</param>
/// <returns>The value.</returns>
string RestoreAsString(string key);
}
}
|
Auch wenn dieser Ansatz funktioniert, ist dies nicht sonderlich elegant. Seit C# 2.0 gibt
es für derartige Probleme eine Lösung, nämlich die sogenannten generischen Datentypen, die
kurz auch als Generika bezeichnet werden. Generika stellen immer dann eine gangbare elegante
Lösung dar, wenn der gleiche Algorithmus oder die gleiche Datenstruktur mehrfach
implementiert werden muss, wobei sich die einzelnen Varianten nur durch den Typ der zu
verarbeitenden Daten unterscheiden.
In einem solchen Fall ermöglichen es Generika, den Algorithmus oder die Datenstruktur nur ein
einziges Mal implementieren zu müssen, ohne von vornherein einen konkreten Typ festzulegen.
Statt dessen wird der Typ abstrahiert und an seiner Stelle ein Platzhalter eingefügt, der erst
von dem Compiler durch den tatsächlichen Typ ersetzt wird. Da der Compiler den tatsächlichen
Typ in den MSIL-Code schreibt, sind generische Datentypen trotz ihres abstrakten Ansatzes
typsicher.
Der Platzhalter kann so wohl bei Klassen und Schnittstellen wie auch bei beliebigen Elementen
wie Feldern, Eigenschaften oder Methoden eingesetzt werden und wird durch ein paar Spitzklammern
begrenzt. Als Name wird üblicherweise der Buchstabe T, der als Kürzel für Type steht, verwendet.
Falls mehr als ein Platzhalter benötigt wird, wird jeder einzelne Typparameter mit dem Buchstaben
T als Suffix und einem folgenden Substantiv in Pascal Case benannt, wobei die zusätzlichen
Platzhalter durch Kommata getrennt innerhalb der Spitzklammern aufgelistet werden.
Um also die Schnittstelle IMemento als generischen Datentyp zur Verfügung zu stellen, muss
ihre Definition um den Platzhalter für den tatsächlich zu verarbeitenden Typ ergänzt werden.
Innerhalb der Schnittstelle kann an Stelle der Typangabe dann der Platzhalter T verwendet
werden. Der Typparameter wird dabei im XML-Kommentar mit Hilfe des Elementes typeparam
beschrieben.
| C# |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
using System;
namespace GoloRoden.GuideToCSharp
{
/// <summary>
/// Provides methods for memento classes.
/// </summary>
/// <typeparam name="T">The type.</typeparam>
public interface IMemento<T>
{
/// <summary>
/// Stores the specified value using the specified
/// key.
/// </summary>
/// <param name="key">The key.</param>
/// <param name="value">The value.</param>
void Store(string key, T value);
/// <summary>
/// Restores the value stored with the specified
/// key.
/// </summary>
/// <param name="key">The key.</param>
/// <returns>The value.</returns>
T Restore(string key);
}
}
|
Die Schnittstelle IMemento kann nun für beliebige Typen eingesetzt werden, indem sie
über ihren Namen ergänzt um einen konkreten Typ angesprochen wird. Statt IMemento muss
in der Schnittstelle IPersistable nun IMemento<
float> angegeben werden.
| C# |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
using System;
namespace GoloRoden.GuideToCSharp
{
/// <summary>
/// Provides methods for persisting an object.
/// </summary>
public interface IPersistable
{
/// <summary>
/// Stores the current instance to the specified
/// memento.
/// </summary>
/// <param name="memento">The memento.</param>
void Store(IMemento<float> memento);
/// <summary>
/// Restores the current instance to the specified
/// memento.
/// </summary>
/// <param name="memento">The memento.</param>
void Restore(IMemento<float> memento);
}
}
|
Nachteilig an dieser Variante ist allerdings, dass nun für jeden einzelnen Datentyp eine
eigene Schnittstelle IMemento mit dem jeweiligen Typ definiert werden muss. Daher kann der
Typ auch nur für eine Methode angegeben werden, so dass die Schnittstelle IMemento nach wie
vor allgemein gültig bleibt, ihre Methoden aber unter Angabe eines Typs aufgerufen werden
müssen.
| C# |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
|
using System;
namespace GoloRoden.GuideToCSharp
{
/// <summary>
/// Provides methods for memento classes.
/// </summary>
public interface IMemento
{
/// <summary>
/// Stores the specified value using the specified
/// key.
/// </summary>
/// <typeparam name="T">The type.</typeparam>
/// <param name="key">The key.</param>
/// <param name="value">The value.</param>
void Store<T>(string key, T value);
/// <summary>
/// Restores the value stored with the specified
/// key.
/// </summary>
/// <typeparam name="T">The type.</typeparam>
/// <param name="key">The key.</param>
/// <returns>The value.</returns>
T Restore<T>(string key);
}
/// <summary>
/// Provides methods for persisting an object.
/// </summary>
public interface IPersistable
{
/// <summary>
/// Stores the current instance to the specified
/// memento.
/// </summary>
/// <param name="memento">The memento.</param>
void Store(IMemento<float> memento);
/// <summary>
/// Restores the current instance to the specified
/// memento.
/// </summary>
/// <param name="memento">The memento.</param>
void Restore(IMemento<float> memento);
}
}
|
Typparameter
Allen bisher verwendeten generischen Typparametern ist gemein, dass es keine Einschränkungen
gibt, welche Typen an Stelle des Platzhalters eingesetzt werden können. Solche Typparameter
werden daher auch als nicht gebundene oder ungebundene Typparameter bezeichnet. Allerdings
verfügen ungebundene Typparameter - eben weil es keine Einschränkung der potenziellen Typen
gibt - ihrerseits über einige Einschränkungen.
Unabhängig davon, dass der Typ bei ungebundenen Typparametern unbekannt ist, lassen sich
auch keinerlei Annahmen über die Art des Typs machen: Es ist unbekannt, welche Schnittstellen
dieser Typ implementiert, es ist unbekannt, ob der Typ von einer bestimmten Basisklasse
ableitet, es ist nicht einmal bekannt, ob es sich bei dem Typ um einen Verweis- oder einen
Wertetyp handelt.
In einigen Fällen kann es erforderlich sein, die potenziellen Typen einzuschränken. Dazu
dient in C# das Schlüsselwort
where, mit dem zusätzliche Angaben
zu einem Typ gemacht werden können. Typparameter, die mit diesem Schlüsselwort näher spezifiziert
wurden, werden als gebundene Typparameter bezeichnet.
Sofern mehr als ein Typparameter verwendet wird, muss für jeden dieser Typparameter, der
gebunden werden soll, ein eigenes
where angegeben werden.
Die einfachste Variante einer Typeinschränkung gibt an, ob es sich bei dem Typparameter um
einen Verweis- oder einen Wertetyp handelt. Für Wertetypen wird als Basisklasse des
Typparameters das Schlüsselwort
struct angegeben, für
Verweistypen
class.
| C# |
1
2
3
|
public void Foo<T> where T : class
{
}
|
Ebenso kann an Stelle des Schlüsselwortes
class auch eine
konkrete Klasse oder Schnittstelle angegeben werden, welcher der Typparameter
entsprechen muss. Wie bei der Vererbung von Klassen können mehrere Schnittstellen
angegeben werden, zudem können sie mit der Angabe einer Klasse kombiniert werden.
In diesem Fall werden die einzelnen Angaben durch Kommata getrennt.
| C# |
1
2
3
|
public void Foo<T> where T : Bar, IBar1, IBar2
{
}
|
Schließlich kann der Ausdruck new() angegeben werden, um zu definieren, dass der Typparameter
über einen öffentlichen parameterlosen Konstruktor verfügen muss. Falls dieser Ausdruck
angegeben wird, muss er als letzter angegeben werden.
| C# |
1
2
3
|
public void Foo<T> where T : class, new()
{
}
|
Als Spezialfall gibt es des weiteren noch Typparameter, die wiederum durch einen Typparameter
eingeschränkt werden, indem dieser weitere Typparameter beispielsweise als notwendige
Basisklasse angegeben wird. Solche Typeinschränkungen werden als naked bezeichnet.
| C# |
1
2
3
|
public void Foo<TDerived, TBase> where TDerived : TBase
{
}
|
Da bei einem Typparameter nicht notwendigerweise bekannt ist, ob es sich um einen Verweis-
oder einen Wertetyp handelt, ist es nicht möglich, ihn mit dem Standardwert zu initialisieren.
Um einen Typparameter dennoch mit dem Standardwert seines Typs initialisieren zu können, gibt
es das Schlüsselwort
default, das wie eine Methode verwendet
wird, und dem als Parameter der entsprechende Typ übergeben werden muss.
| C# |
1
2
3
4
|
public T Foo<T>()
{
return default(T);
}
|
Neben Schnittstellen und Methoden können auch Klassen, Strukturen und Delegaten mit
Typparametern versehen werden.
Lambdaausdrücke
Generika eignen sich jedoch nicht nur dazu, Typen mit Hilfe von Typparametern flexibel
gestalten zu können, sie ermöglichen auch die Definition von Lambdaausdrücken während
der Ausführung. Dazu bietet C# seit der Version 3.0 den vorgefertigten Delegaten Func
im Namensraum System an, dem als Typparameter die Typen der Parameter und des Rückgabewertes
des zu erzeugenden Lambdaausdrucks übergeben werden.
Soll beispielsweise ein Lambdaausdruck definiert werden, der eine komplexe Zahl in ihren
Absolutbetrag überführt, so ist dies mit Hilfe dieses Delegaten möglich. Als Typparameter
werden in diesem Fall die Klasse ComplexNumber sowie
float als
Typ des Absolutbetrags angegeben.
| C# |
1
2
|
Func<ComplexNumber, float> GetAbsoluteValue =
(c => c.AbsoluteValue);
|
Die auf diese Art erzeugte Delegatinstanz kann im weiteren Verlauf wie jeder andere
Delegat aufgerufen werden. Sollen nicht nur ein, sondern mehrere Parameter angegeben
werden, müssen diese zum einen dem Delegaten Func wie auch innerhalb des Lambdaausdrucks
kommasepariert innerhalb von runden Klammern angegeben werden.
| C# |
1
2
|
Func<ComplexNumber, ComplexNumber, ComplexNumber> Add =
((c1, c2) => c1 + c2);
|