Interfacce generiche (Guida per programmatori C#)

Spesso è utile definire le interfacce per le classi di raccolte generiche o per le classi generiche che rappresentano gli elementi nella raccolta. Per evitare operazioni boxing e unboxing sui tipi valore, è preferibile usare interfacce generiche, ad esempio IComparable<T>, nelle classi generiche. La libreria di classi .NET definisce diverse interfacce generiche da usare con le classi di raccolta nello spazio dei System.Collections.Generic nomi . Per altre informazioni su queste interfacce, vedere Interfacce generiche.

Quando un'interfaccia viene specificata come vincolo o parametro di tipo, è possibile usare solo i tipi che implementano l'interfaccia. L'esempio di codice seguente mostra una classe SortedList<T> che deriva dalla classe GenericList<T>. Per altre informazioni, vedere Introduzione ai generics. SortedList<T> aggiunge il vincolo where T : IComparable<T>. Questo vincolo consente al BubbleSort metodo in SortedList<T> di utilizzare il metodo generico CompareTo sugli elementi dell'elenco. In questo esempio gli elementi di elenco sono una classe semplice che Person implementa 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");
    }
}

È possibile specificare più interfacce come vincoli su un unico tipo, in questo modo:

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

Un'interfaccia può definire più parametri di tipo, in questo modo:

interface IDictionary<K, V>
{
}

Le regole di ereditarietà valide per le classi si applicano anche alle interfacce:

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

Le interfacce generiche possono ereditare da interfacce non generiche se l'interfaccia generica è covariante, il che significa che usa solo il parametro di tipo come valore restituito. Nella libreria IEnumerable<T> di classi .NET eredita da IEnumerable perché IEnumerable<T> usa T solo nel valore restituito di GetEnumerator e nel getter della Current proprietà.

Le classi concrete possono implementare interfacce costruite chiuse, in questo modo:

interface IBaseInterface<T> { }

class SampleClass : IBaseInterface<string> { }

Le classi generiche possono implementare interfacce generiche o interfacce costruite chiuse purché l'elenco di parametri delle classi specifichi tutti gli argomenti necessari per l'interfaccia, in questo modo:

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

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

Le regole che controllano l'overload dei metodi sono le stesse per i metodi all'interno di classi generiche, struct generici o interfacce generiche. Per altre informazioni, vedere Metodi generici.

A partire da C# 11, le interfacce possono dichiarare static abstract o static virtual membri. Le interfacce che dichiarano uno o static abstractstatic virtual i membri sono quasi sempre interfacce generiche. Il compilatore deve risolvere le chiamate ai metodi static virtual e static abstract in fase di compilazione. static virtual i metodi e static abstract dichiarati nelle interfacce non hanno un meccanismo di invio di runtime analogo a virtual o abstract metodi dichiarati nelle classi. Il compilatore usa invece le informazioni sul tipo disponibili in fase di compilazione. Questi membri vengono in genere dichiarati nelle interfacce generiche. Inoltre, la maggior parte delle interfacce che dichiarano metodi static virtual o static abstract, dichiarano che uno dei parametri di tipo deve implementare l'interfaccia dichiarata. Il compilatore usa quindi gli argomenti di tipo forniti per risolvere il tipo del membro dichiarato.

Vedi anche