Interfaces genéricas (Guia de Programação em C#)

Muitas vezes, é útil definir interfaces para classes de coleção genéricas ou para as classes genéricas que representam itens na coleção. Para evitar operações de conversão boxe e unboxing em tipos de valor, é melhor usar interfaces genéricas, como IComparable<T>, em classes genéricas. A biblioteca de classes .NET define várias interfaces genéricas para uso com as classes de coleção no namespace System.Collections.Generic. Para obter mais informações sobre essas interfaces, consulte Interfaces genéricas.

Quando uma interface é especificada como uma restrição em um parâmetro de tipo, somente os tipos que implementam a interface podem ser usados. O exemplo de código a seguir mostra uma classe SortedList<T> que deriva da classe GenericList<T>. Para obter mais informações, consulte Introdução aos Genéricos. SortedList<T> adiciona a restrição where T : IComparable<T>. Essa restrição habilita o método BubbleSort em SortedList<T> a usar o método genérico CompareTo em elementos de lista. Neste exemplo, os elementos de lista são uma classe simples, Person, que 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");
    }
}

Várias interfaces podem ser especificadas como restrições em um único tipo, da seguinte maneira:

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

Uma interface pode definir mais de um parâmetro de tipo, da seguinte maneira:

interface IDictionary<K, V>
{
}

As regras de herança que se aplicam às classes também se aplicam às 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

Interfaces genéricas poderão herdar de interfaces não genéricas se a interface genérica for covariante, o que significa que ela usa apenas seu parâmetro de tipo como um valor retornado. Na biblioteca de classes do .NET, IEnumerable<T> herda de IEnumerable porque IEnumerable<T> usa apenas T no valor retornado de GetEnumerator e no getter de propriedade Current.

Classes concretas podem implementar interfaces construídas fechadas, da seguinte maneira:

interface IBaseInterface<T> { }

class SampleClass : IBaseInterface<string> { }

Classes genéricas podem implementar interfaces genéricas ou interfaces construídas fechadas, contanto que a lista de parâmetros de classe forneça todos os argumentos exigidos pela interface, da seguinte maneira:

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

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

As regras que controlam a sobrecarga de método são as mesmas para métodos em classes genéricas, structs genéricos ou interfaces genéricas. Para obter mais informações, consulte Métodos Genéricos.

A partir do C# 11, as interfaces podem declarar membros static abstract ou static virtual. As interfaces que declaram membros static abstract ou static virtual são quase sempre interfaces genéricas. O compilador deve resolver chamadas para métodos static virtual e static abstract em tempo de compilação. Os métodos static virtual e static abstract declarados em interfaces não têm um mecanismo de expedição de runtime análogo a métodos virtual ou abstract declarados em classes. Em vez disso, o compilador usa informações de tipo disponíveis em tempo de compilação. Normalmente, esses membros são declarados em interfaces genéricas. Além disso, a maioria das interfaces que declaram os métodos static virtual ou static abstract declaram que um dos parâmetros de tipo deve implementar a interface declarada. Em seguida, o compilador usa os argumentos de tipo fornecidos para resolver o tipo do membro declarado.

Confira também