Generische Schnittstellen (C#-Programmierhandbuch)

Es ist häufig sinnvoll, Schnittstellen entweder für generische Auflistungsklassen zu definieren oder für die generischen Klassen, die Elemente in der Auflistung darstellen. Um Boxing- und Unboxing-Operationen für Werttypen zu vermeiden, ist es besser, generische Schnittstellen wie IComparable<T> für generische Klassen zu verwenden. Durch die .NET-Klassenbibliothek werden einige generische Schnittstellen definiert, die zusammen mit den Sammlungsklassen im Namespace System.Collections.Generic verwendet werden können. Weitere Informationen zu diesen Schnittstellen finden Sie unter Generische Schnittstellen.

Wenn als Einschränkung für einen Typparameter eine Schnittstelle angegeben ist, können nur Typen verwendet werden, die diese Schnittstelle implementieren. Der folgende Code zeigt eine Klasse SortedList<T>, die von der Klasse GenericList<T> abgeleitet wird. Weitere Informationen finden Sie unter Introduction to Generics (Einführung in Generika). SortedList<T> fügt die Einschränkung where T : IComparable<T> hinzu. Durch diese Einschränkung kann die Methode BubbleSort in SortedList<T> die generische Methode CompareTo für Listenelemente verwenden. In diesem Beispiel sind Listenelemente eine einfache Klasse, Person, die IComparable<Person> implementiert.

//Type parameter T in angle brackets.
public class GenericList<T> : System.Collections.Generic.IEnumerable<T>
{
    protected Node head;
    protected Node current = null;

    // Nested class is also generic on T
    protected class Node
    {
        public Node next;
        private T data;  //T as private member datatype

        public Node(T t)  //T used in non-generic constructor
        {
            next = null;
            data = t;
        }

        public Node Next
        {
            get { return next; }
            set { next = value; }
        }

        public T Data  //T as return type of property
        {
            get { return data; }
            set { data = value; }
        }
    }

    public GenericList()  //constructor
    {
        head = null;
    }

    public void AddHead(T t)  //T as method parameter type
    {
        Node n = new Node(t);
        n.Next = head;
        head = n;
    }

    // Implementation of the iterator
    public System.Collections.Generic.IEnumerator<T> GetEnumerator()
    {
        Node current = head;
        while (current != null)
        {
            yield return current.Data;
            current = current.Next;
        }
    }

    // IEnumerable<T> inherits from IEnumerable, therefore this class
    // must implement both the generic and non-generic versions of
    // GetEnumerator. In most cases, the non-generic method can
    // simply call the generic method.
    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}

public class SortedList<T> : GenericList<T> where T : System.IComparable<T>
{
    // A simple, unoptimized sort algorithm that
    // orders list elements from lowest to highest:

    public void BubbleSort()
    {
        if (null == head || null == head.Next)
        {
            return;
        }
        bool swapped;

        do
        {
            Node previous = null;
            Node current = head;
            swapped = false;

            while (current.next != null)
            {
                //  Because we need to call this method, the SortedList
                //  class is constrained on IComparable<T>
                if (current.Data.CompareTo(current.next.Data) > 0)
                {
                    Node tmp = current.next;
                    current.next = current.next.next;
                    tmp.next = current;

                    if (previous == null)
                    {
                        head = tmp;
                    }
                    else
                    {
                        previous.next = tmp;
                    }
                    previous = tmp;
                    swapped = true;
                }
                else
                {
                    previous = current;
                    current = current.next;
                }
            }
        } while (swapped);
    }
}

// A simple class that implements IComparable<T> using itself as the
// type argument. This is a common design pattern in objects that
// are stored in generic lists.
public class Person : System.IComparable<Person>
{
    string name;
    int age;

    public Person(string s, int i)
    {
        name = s;
        age = i;
    }

    // This will cause list elements to be sorted on age values.
    public int CompareTo(Person p)
    {
        return age - p.age;
    }

    public override string ToString()
    {
        return name + ":" + age;
    }

    // Must implement Equals.
    public bool Equals(Person p)
    {
        return (this.age == p.age);
    }
}

public class Program
{
    public static void Main()
    {
        //Declare and instantiate a new generic SortedList class.
        //Person is the type argument.
        SortedList<Person> list = new SortedList<Person>();

        //Create name and age values to initialize Person objects.
        string[] names =
        [
            "Franscoise",
            "Bill",
            "Li",
            "Sandra",
            "Gunnar",
            "Alok",
            "Hiroyuki",
            "Maria",
            "Alessandro",
            "Raul"
        ];

        int[] ages = [45, 19, 28, 23, 18, 9, 108, 72, 30, 35];

        //Populate the list.
        for (int x = 0; x < 10; x++)
        {
            list.AddHead(new Person(names[x], ages[x]));
        }

        //Print out unsorted list.
        foreach (Person p in list)
        {
            System.Console.WriteLine(p.ToString());
        }
        System.Console.WriteLine("Done with unsorted list");

        //Sort the list.
        list.BubbleSort();

        //Print out sorted list.
        foreach (Person p in list)
        {
            System.Console.WriteLine(p.ToString());
        }
        System.Console.WriteLine("Done with sorted list");
    }
}

Mehrere Schnittstellen können als Einschränkungen für einen einzelnen Typ wie folgt angegeben werden:

class Stack<T> where T : System.IComparable<T>, IEnumerable<T>
{
}

Eine Schnittstelle kann mehr als einen Typparameter wie folgt definieren:

interface IDictionary<K, V>
{
}

Für Klassen gelten die gleichen Vererbungsregeln wie für Schnittstellen:

interface IMonth<T> { }

interface IJanuary : IMonth<int> { }  //No error
interface IFebruary<T> : IMonth<int> { }  //No error
interface IMarch<T> : IMonth<T> { }    //No error
                                       //interface IApril<T>  : IMonth<T, U> {}  //Error

Generische Schnittstellen können von nicht generischen Schnittstellen erben, sofern die generische Schnittstelle kovariant ist, also die generische Schnittstelle ausschließlich den eigenen Typparameter als Rückgabewert verwendet. In der Klassenbibliothek von .NET Framework erbt IEnumerable<T> von IEnumerable, da IEnumerable<T> nur T im Rückgabewert von GetEnumerator und im Eigenschaftengetter Current verwendet.

Konkrete Klassen können geschlossene konstruierte Schnittstellen wie folgt implementieren:

interface IBaseInterface<T> { }

class SampleClass : IBaseInterface<string> { }

Generische Klassen können generische Schnittstellen oder geschlossene konstruierte Schnittstellen implementieren, sofern die Klassenparameterliste wie nachfolgend gezeigt sämtliche Argumente bereitstellt, die von der Schnittstelle benötigt werden:

interface IBaseInterface1<T> { }
interface IBaseInterface2<T, U> { }

class SampleClass1<T> : IBaseInterface1<T> { }          //No error
class SampleClass2<T> : IBaseInterface2<T, string> { }  //No error

Die Regeln, die das Überladen von Methoden steuern, sind für die Methoden innerhalb von generischen Klassen, generischen Strukturen oder generischen Schnittstellen gleich. Weitere Informationen finden Sie unter Generic Methods (Generische Methoden).

Ab C# 11 können Schnittstellen oder static abstract- oder static virtual-Member deklarieren. Schnittstellen, die entweder static abstract- oder static virtual-Member deklarieren, sind fast immer generische Schnittstellen. Der Compiler muss Aufrufe von static virtual- und static abstract-Methoden zur Kompilierzeit auflösen. static virtual- und static abstract-Methoden, die in Schnittstellen deklariert werden, verfügen nicht über einen Laufzeitverteilungsmechanismus, der den in Klassen deklarierten virtual- oder abstract-Methoden entspricht. Stattdessen verwendet der Compiler Typinformationen, die zur Kompilierzeit verfügbar sind. Diese Member werden in der Regel in generischen Schnittstellen deklariert. Darüber hinaus deklarieren die meisten Schnittstellen, die oder static virtual- oder static abstract-Methoden deklarieren, dass einer der Typparameter die deklarierte Schnittstelle implementieren muss. Der Compiler verwendet dann die angegebenen Typargumente, um den Typ des deklarierten Members aufzulösen.

Weitere Informationen