Универсальные интерфейсы. (Руководство по программированию на C#)

Часто рекомендуется определять интерфейсы для универсальных классов коллекций или универсальных классов, представляющих элементы в коллекции. Чтобы избежать операций бокса и распаковки с типами значений, лучше использовать универсальные интерфейсы, такие как IComparable<T>универсальные классы. В библиотеке классов .NET в пространстве имен System.Collections.Generic определяется несколько универсальных интерфейсов для работы с классами коллекций. Дополнительные сведения об этих интерфейсах см. в разделе "Универсальные интерфейсы".

Если интерфейс задан в качестве ограничения для параметра типа, можно использовать только типы, реализующие такой интерфейс. В следующем примере кода демонстрируется класс SortedList<T>, который является производным от класса GenericList<T>. Дополнительные сведения см. в разделе Введение в универсальные шаблоны. SortedList<T> добавляет ограничение where T : IComparable<T>. Это ограничение позволяет BubbleSort методу SortedList<T> использовать универсальный CompareTo метод для элементов списка. В этом примере элементы списка — это простой класс, Person реализующий 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");
    }
}

В качестве ограничений для одного типа можно задать несколько интерфейсов, как показано ниже:

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

Интерфейс может определять несколько параметров типа, как показано ниже:

interface IDictionary<K, V>
{
}

Правила наследования, действующие в отношении классов, также применяются к интерфейсам:

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

Универсальные интерфейсы могут наследоваться от неуниверсальных интерфейсов, если универсальный интерфейс является ковариантным (то есть использует в качестве возвращаемого значения только свой параметр типа). В библиотеке классов .NET IEnumerable<T> наследуется от IEnumerable, поскольку IEnumerable<T> использует T только в возвращаемом значении GetEnumerator и в методе считывания свойства Current.

Конкретные классы могут реализовывать закрытые сконструированные интерфейсы, как показано ниже:

interface IBaseInterface<T> { }

class SampleClass : IBaseInterface<string> { }

Универсальные классы могут реализовывать универсальные интерфейсы или закрытые сконструированные интерфейсы при условии, что в списке параметров класса заданы все аргументы, требуемые интерфейсом, как показано ниже:

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

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

Правила, управляющие перегрузкой методов, одинаковы для методов с универсальными классами, структурами или интерфейсами. Дополнительные сведения см. в разделе Универсальные методы.

Начиная с C# 11 интерфейсы могут объявлять или static virtual членыstatic abstract. Интерфейсы, объявляющие либо static abstractstatic virtual члены, почти всегда являются универсальными интерфейсами. Компилятор должен разрешать вызовы static virtual и static abstract методы во время компиляции. static virtual и static abstract методы, объявленные в интерфейсах, не имеют механизма диспетчеризации среды выполнения, аналогичного virtual методам, abstract объявленным в классах. Вместо этого компилятор использует сведения о типе, доступные во время компиляции. Обычно эти члены объявляются в универсальных интерфейсах. Кроме того, большинство интерфейсов, объявляющих или методы, static virtual объявляют, что один из параметров типа должен реализовать объявленный интерфейс.static abstract Затем компилятор использует предоставленные аргументы типа для разрешения типа объявленного члена.

См. также