ein Projekt von goloroden.de

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);