Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Лекции OOP c#.doc
Скачиваний:
44
Добавлен:
22.09.2019
Размер:
3.38 Mб
Скачать

1.24. Обобщенные типы (generics)

Обобщенные (или параметризованные) типы (generics) позволяют при описании пользовательских классов, структур, интерфейсов, делегатов и методов указать как параметр тип данных для хранения и обработки.

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

class Stack {

object[] items;

int count;

public void Push(object item) {...}

public object Pop() {...}

}

Класс Stack универсален, он позволяет хранить произвольные объекты:

Stack stack = new Stack();

stack.Push(new Customer());

Customer c = (Customer)stack.Pop();

Stack stack2 = new Stack();

stack2.Push(3);

int i = (int)stack2.Pop();

Однако универсальность класса Stack имеет и отрицательные моменты. При извлечении данных из стека необходимо выполнять приведение типов. Для структурных типов (таких как int) при помещении данных в стек и при извлечении выполняются операции упаковки и распаковки, что отрицательно сказывается на быстродействии. И, наконец, неверный тип помещаемого в стек элемента может быть выявлен только на этапе выполнения, но не компиляции.

Stack stack = new Stack(); // планируем сделать стек чисел

stack.Push(1);

stack.Push(2);

stack.Push("three"); // вставили не число, а строку

for (int i = 0, i < 3, i++)

// компилируется, но при выполнении на третьей итерации

// будет сгенерирована исключительная ситуация

int result = (int)stack.Pop();

Основной причиной появления обобщенных типов была необходимость устранения описанных недостатков универсальных классов.

Опишем класс Stack как обобщенный тип. Для этого используется следующий синтаксис: после имени класса в угловых скобках < и > указывается параметр типа. Этот параметр затем может использоваться при описании элементов класса Stack.

class Stack<T> {

T[] items;

int count;

public void Push(T item) {...}

public T Pop() {...}

}

При использовании обобщенного типа Stack требуется указать фактический тип вместо параметра T. В следующем фрагменте Stack применяется для хранения данных типа int.

Stack<int> stack = new Stack<int>();

stack.Push(3);

int x = stack.Pop();

Тип вида Stack<int> будем в дальнейшем называть сконструированным типом (constructed type). Обратите внимание: при работе с типом Stack<int> отпала необходимость в выполнении приведения типов при извлечении элементов из стека. Кроме этого, теперь компилятор отслеживает, чтобы в стек помещались только данные типа int. И еще одна менее очевидная особенность. Нет необходимости в упаковке и распаковке структурного элемента, а это приводит к увеличению быстродействия.

При объявлении обобщенного типа можно использовать несколько параметров. Приведем фрагмент описания класса для хранения пар «ключ-значение» с возможностью доступа к значению по ключу:

class Dict<K,V> {

public void Add(K key, V value) {...}

public V this[K key] {...}

}

Сконструированный тип для Dict<K,V> должен быть основан на двух конкретных типах:

Dict<string,Customer> dict = new Dict<string,Customer>();

dict.Add("Alex", new Customer());

Customer c = dict["Alex"];

Как правило, обобщенные типы не просто хранят данные указанного параметра, а еще и вызывают методы у объекта, чей тип указан как параметр. К примеру, в классе Dict<K,V> метод Add() может использовать метод CompareTo() для сравнения ключей:

class Dict<K,V> {

public void Add(K key, V value) {

. . .

if(key.CompareTo(x) < 0) {...} // Ошибка компиляции!

. . .

}

}

Так как тип параметра K может быть любым, у key можно вызывать только методы класса object, и приведенный выше код просто не компилируется. Конечно, проблему можно решить, используя приведение типов:

class Dict<K,V> {

public void Add(K key, V value) {

. . .

if(((IComparable)key).CompareTo(x) < 0) {...}

. . .

}

}

Недостаток данного решения – необходимость приведения типов. К тому же, если тип K не поддерживает интерфейс IComparable, то при работе программы будет сгенерировано исключение InvalidCastException.

В C# для каждого параметра обобщенного типа может быть задан список ограничений (constraints). Только тип, удовлетворяющий ограничениям, может быть применен для записи сконструированного типа.

Ограничения объявляются с использованием ключевого слова where, после которого указывается параметр, двоеточие и список ограничения. Элементом списка ограничения могут являться интерфейсы, класс (только один) и ограничение на конструктор. Для класса Dict<K,V> можно установить ограничение на параметр K, гарантирующее, что тип K реализует IComparable.

class Dict<K,V> where K: IComparable

{

public void Add(K key, V value) {

. . .

if(key.CompareTo(x) < 0) {...}

. . .

}

}

Компилятор будет проверять соблюдение ограничения при создании сконструированного типа. Кроме этого, отпадает необходимость в выполнении приведения типов в теле класса Dict<K,V>.

В следующем примере используется несколько ограничений на различные параметры типа:

class EntityTable<K,E>

where K: IComparable<K>, IPersistable

where E: Entity, new()

{

public void Add(K key, E entity) {

. . .

if (key.CompareTo(x) < 0) {...}

. . .

}

}

Смысл ограничений, наложенных на параметр E: он должен быть приводим к классу Entity и иметь public-конструктор без параметров.

Порядок элементов в списке ограничений имеет значение в C#. Если есть элемент-класс, то он должен быть первым в списке. Если есть элемент-конструктор, то его надо помещать последним.

В некоторых случаях достаточно параметризовать не весь пользовательский тип, а только конкретный метод. Обобщенные методы (generic methods) объявляются с использованием параметра типа в угловых скобках после имени метода.

void PushMultiple<T>(Stack<T> stack, params T[] values) {

foreach (T value in values) stack.Push(value);

}

Использование обобщенного метода PushMultiple<T> позволяет работать с любым сконструированным типом на основе Stack<T>.

Stack<int> stack = new Stack<int>();

PushMultiple<int>(stack, 1, 2, 3, 4);

Для обобщенных методов, подобных PushMultiple<T>, компилятор способен самостоятельно установить значение параметра типа на основе фактических параметров метода. Это позволяет записывать вызов метода без указания типа:

Stack<int> stack = new Stack<int>();

// Так как stack – объект Stack<int>, то используем тип int

PushMultiple(stack, 1, 2, 3, 4);

Как и при описании типов, обобщенные методы могут содержать ограничения на параметр-тип:

public T Max<T>(T val1, T val2) where T: IComparable {

T retVal = val2;

if (val2.CompareTo(val1) < 0) retVal = val1;

return retVal;

}

В язык C# было введено новое ключевое слово default и соответствующая операция. Эта операция может применяться в тех методах, где возвращаемое значение задано как параметр типа. Как следует из названия, операция возвращает соответствующее значение по умолчанию для переменной некоторого типа.

class MyCache<K, V> {

. . .

// Метод для поиска элемента по ключу

// Если элемент найден, то метод возвращается его

// Иначе метод возвращает значение по умолчанию для V

public V LookupItem(K key) {

V retVal;

if (ContainsKey(key))

retVal = GetValue(key);

else

retVal = default(V);

return retVal;

}

}

Имеющиеся в .NET Framework обобщенные типы для представления структур данных обсуждаются ниже.