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

Универсальные классы инкапсулируют операции, которые не относятся к конкретному типу данных. Универсальные классы чаще всего используются для работы с коллекциями, такими как связанные списки, хэш-таблицы, стеки, очереди, деревья и т. д. Такие операции, как добавление и удаление элементов коллекции, по существу выполняются одинаково, независимо от типа хранимых данных.

В большинстве случаев для этого используются классы коллекций. Рекомендуется выбирать те из них, которые представлены в библиотеке классов платформы .NET. Дополнительные сведения об использовании этих классов см. в разделе Универсальные коллекции в .NET.

Как правило, при создании универсального класса сначала определяется конкретный класс, после чего его типы поочередно заменяются параметрами типов до тех пор, пока не будет достигнут необходимый баланс между степенью обобщения и удобством работы. При создании собственных универсальных классов необходимо учитывать следующие важные моменты:

  • Типы, которые требуется обобщить с использованием параметров типа.

    Как правило, чем больше типов параметризовано, тем более гибким и универсальным становится ваш код. Тем не менее слишком высокая степень обобщения может отрицательно сказаться на понятности создаваемого вами кода для других разработчиков.

  • Ограничения (если требуются), которые будут применяться к параметрам типа (см. раздел Ограничения параметров типа).

    Рекомендуется применять максимально возможный объем ограничений, при котором вы по-прежнему сможете работать с необходимыми типами. Например, если универсальный класс будет использоваться только для работы со ссылочными типами, примените ограничение класса. Это позволит исключить случайное использование класса с типами значений и позволит использовать оператор as в отношении T, а также проверять наличие значений null.

  • Требуется ли разбивать универсальные функции между базовыми классами и подклассами.

    Поскольку универсальные классы могут выступать в качестве базовых классов, здесь необходимо учитывать те же принципы разработки, что и для классов, не являющихся универсальными. См. описание правил наследования от универсальных базовых классов далее в этом разделе.

  • Требуется ли реализовывать один или несколько универсальных интерфейсов.

    Например, при разработке класса, который будет использоваться для создания элементов коллекции на основе универсальных шаблонов, может потребоваться реализовать интерфейс IComparable<T>, где T — это тип вашего класса.

Пример простого универсального класса можно найти в разделе Введение в универсальные шаблоны.

Правила в отношении параметров типа и ограничений влияют на поведение универсального класса, особенно в контексте наследования и доступа к элементам. Прежде чем продолжить, необходимо ознакомиться с некоторыми терминами и понятиями. Для клиентского кода универсального класса Node<T>, можно ссылаться на класс, указав аргумент типа , чтобы создать закрытый созданный тип (Node<int>); или оставив параметр типа не указанным, например при указании универсального базового класса, чтобы создать открытый созданный тип (Node<T>). Универсальные классы могут наследоваться от конкретных, а также закрытых или открытых сконструированных базовых классов:

class BaseNode { }
class BaseNodeGeneric<T> { }

// concrete type
class NodeConcrete<T> : BaseNode { }

//closed constructed type
class NodeClosed<T> : BaseNodeGeneric<int> { }

//open constructed type
class NodeOpen<T> : BaseNodeGeneric<T> { }

Классы, не являющиеся универсальными, то есть конкретные классы, могут наследоваться от закрытых сконструированных базовых классов. Наследование от аналогичных открытых классов или от параметров типа невозможно, поскольку во время выполнения клиентский код не может предоставить аргумент типа, необходимый для создания экземпляра базового класса.

//No error
class Node1 : BaseNodeGeneric<int> { }

//Generates an error
//class Node2 : BaseNodeGeneric<T> {}

//Generates an error
//class Node3 : T {}

Универсальные классы, наследуемые от открытых сконструированных типов, должны предоставлять аргументы типа для любых параметров типа базового класса, которые не используются совместно с наследующим классом. Это продемонстрировано в следующем коде:

class BaseNodeMultiple<T, U> { }

//No error
class Node4<T> : BaseNodeMultiple<T, int> { }

//No error
class Node5<T, U> : BaseNodeMultiple<T, U> { }

//Generates an error
//class Node6<T> : BaseNodeMultiple<T, U> {}

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

class NodeItem<T> where T : System.IComparable<T>, new() { }
class SpecialNodeItem<T> : NodeItem<T> where T : System.IComparable<T>, new() { }

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

class SuperKeyType<K, V, U>
    where U : System.IComparable<U>
    where V : new()
{ }

Открытые и закрытые сконструированные типы можно использовать в качестве параметров метода:

void Swap<T>(List<T> list1, List<T> list2)
{
    //code to swap items
}

void Swap(List<int> list1, List<int> list2)
{
    //code to swap items
}

Если универсальный класс реализует интерфейс, все экземпляры такого класса можно привести к этому интерфейсу.

Универсальные классы инвариантны. Другими словами, если входной параметр задает List<BaseClass>, при попытке предоставить List<DerivedClass> возникает ошибка времени компиляции.

См. также