Interfaces génériques (guide de programmation C#)

Il est souvent utile de définir des interfaces pour les classes de collections génériques, ou pour les classes génériques qui représentent des éléments dans la collection. Pour éviter les opérations boxing et unboxing sur les types valeurs, il est préférable d’utiliser des interfaces génériques, telles que IComparable<T>, sur des classes génériques. La bibliothèque de classes .NET définit plusieurs interfaces génériques à utiliser avec les classes de collections dans l’espace de noms System.Collections.Generic. Pour plus d’informations sur ces interfaces, consultez Interfaces génériques.

Quand une interface est spécifiée comme une contrainte sur un paramètre de type, seuls les types qui implémentent l’interface peuvent être utilisés. L’exemple de code suivant illustre une classe SortedList<T> qui dérive de la classe GenericList<T>. Pour plus d’informations, consultez Introduction aux génériques. SortedList<T> ajoute la contrainte where T : IComparable<T>. Cette contrainte permet à la méthode BubbleSort dans SortedList<T> d’utiliser la méthode générique CompareTo sur les éléments de liste. Dans cet exemple, les éléments de liste sont une classe simple, Person, qui implémente IComparable<Person>.

//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");
    }
}

Plusieurs interfaces peuvent être spécifiées en tant que contraintes sur un seul type, comme suit :

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

Une interface peut définir plusieurs paramètres de type, comme suit :

interface IDictionary<K, V>
{
}

Les règles d’héritage qui s’appliquent aux classes s’appliquent également aux interfaces :

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

Les interfaces génériques peuvent hériter d’interfaces non génériques si l’interface générique est de type covariante, ce qui signifie qu’elle utilise uniquement son paramètre de type comme valeur de retour. Dans la bibliothèque de classes .NET, IEnumerable<T> hérite d’IEnumerable, car IEnumerable<T> utilise uniquement T dans la valeur de retour de GetEnumerator et dans l’accesseur de propriété Current.

Les classes concrètes peuvent implémenter des interfaces construites fermées, comme suit :

interface IBaseInterface<T> { }

class SampleClass : IBaseInterface<string> { }

Les classes génériques peuvent implémenter des interfaces génériques ou des interfaces construites fermées tant que la liste de paramètres de classe fournit tous les arguments requis par l’interface, comme suit :

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

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

Les règles qui déterminent la surcharge des méthodes sont les mêmes pour les méthodes dans les classes génériques, les structs génériques ou les interfaces génériques. Pour plus d’informations, consultez Méthodes génériques.

À compter de C# 11, les interfaces peuvent déclarer des membres static abstract ou static virtual. Les interfaces qui déclarent des membres static abstract ou static virtual sont presque toujours des interfaces génériques. Le compilateur doit résoudre les appels aux méthodes static virtual et static abstract au moment de la compilation. Les méthodes static virtual et static abstract déclarées dans les interfaces n’ont pas de mécanisme de dispatch d’exécution analogue aux méthodes virtual ou abstract déclarées dans les classes. Au lieu de cela, le compilateur utilise des informations de type disponibles au moment de la compilation. Ces membres sont généralement déclarés dans les interfaces génériques. En outre, la plupart des interfaces qui déclarent les méthodes static virtual ou static abstract notifient que l’un des paramètres de types doit implémenter l’interface déclarée. Le compilateur utilise ensuite les arguments de types fournis pour résoudre le type du membre déclaré.

Voir aussi