Обобщенияx

advertisement
Тема 6. Основы программирования на языке C#
6.5 Обобщения
• Обзор обобщений
• Создание обобщенных классов
• Средства обобщенных классов
• Обобщенные интерфейсы
• Обобщенные структуры
• Обобщенные методы
1
Начиная с версии 2.0, в .NET поддерживаются обобщения. Обобщения — не просто
часть языка программирования С#, но также средство, тесно интегрированное с кодом IL в
сборках. С помощью обобщений можно создавать классы и методы, независимые от хранящихся в них типов. Вместо написания множества методов или классов с одинаковой функциональностью для разных типов можно создать только один метод или класс.
Другой способ сокращения объема кода  использование класса Object. Однако его
применение не обеспечивает безопасности типов. Обобщенные классы работают с обобщенными типами, вместо которых при необходимости подставляются конкретные. Это обеспечивает безопасность типов: компилятор сообщает, когда определенный конкретный тип не поддерживается обобщенным классом.
Обобщения касаются не только классов; в этой разделе вы увидите применение обобщенных делегатов, интерфейсов и методов. Обобщения с делегатами описаны в разделе 6.8.
Обзор обобщений
Обобщения  это не совсем новая конструкция; подобные концепции присутствуют и в
других языках. Например, схожие с обобщениями черты имеют шаблоны C++. Однако между
шаблонами C++ и обобщениями .NET есть большая разница. В C++ при создании экземпляра
шаблона с конкретным типом необходим исходный код шаблонов. В отличие от шаблонов C++,
обобщения являются не только конструкцией языка С#, но также определены для CLR. Это
позволяет создавать экземпляры шаблонов с определенным типом- параметром на языке Visual
Basic, даже если обобщенный класс определен на С#.
В последующих разделах речь пойдет о преимуществах и недостатках обобщений, в частности, будут рассмотрены такие темы:





производительность;
безопасность типов;
повторное использование двоичного кода;
“разбухание” кода;
руководства по именованию.
Производительность
Одним из основных преимуществ обобщенийчявляется производительность. В разделе
6.10 мы увидим примеры использования необобщенных и обобщенных классов коллекций из
пространств имен System.Collections и System.Collections.Generic. Использование
типов значений с не обобщенными классами коллекций вызывает упаковку (boxing) и распаковку (unboxing) при преобразовании в ссылочный тип и обратно.
Упаковка и распаковка обсуждаются в разделе 7. Здесь дается только краткое их
описание.
Типы значений сохраняются в стеке, а типы ссылок  в куче. Классы C# являются ссылочными типами, а структуры  типами значений. .NET позволяет легко преобразовывать типы
значений в ссылочные, поэтому их можно использовать там, где ожидаются объекты (т.е. ссылочные типы). Например, объекту можно присвоить значение типа int. Преобразование типа
значений в ссылочный тип называется упаковкой (boxing). Упаковка происходит автоматически, когда метод ожидает параметр ссылочного типа, а ему передается тип значений. С другой
стороны, упакованный тип значений может быть обратно преобразован к простому типу значений с помощью распаковки (unboxing). При распаковке требуется операция приведения.
В следующем примере показан класс ArrayList из пространства имен System.Collections. Класс ArrayList хранит объекты, а его метод Add() определен так, что
требует в качестве параметра объект, поэтому при вставке значение int упаковывается. Когда
значение читается из ArrayList, при преобразовании объекта в целый тип происходит распаковка. Это очевидно, когда в примере явно используется операция приведения для присваивания значения первого элемента ArrayList переменной i1, но также это происходит и внутри
оператора foreach, где используется переменная i2 типа int:
2
var list = new ArrayList() ;
list.Add(44);
//упаковка - тип значения преобразуется в ссылочный
int il = (int)list[0]; //распаковка - ссылочный тип преобразуется
//в тип значений
foreach (int i2 in list)
{
Console.WriteLine (i2); //распаковка
}
Упаковку и распаковку применять легко, но они сильно влияют на производительность,
особенно при выполнении итераций по большому количеству элементов.
Вместо использования объектов класс List<T> из пространства имен System.Collections.Generic позволяет определить тип элемента при создании коллекции. В
следующем примере обобщенный тип (тип-параметр) класса List<T> определяется как int, и
потому внутри динамически сгенерированного JIT-компилятором класса применяется тип int,
без каких-либо преобразований. Упаковка и распаковка не требуются.
var list = new List<int>();
list.Add(44) ; //никакой упаковки - элементы типа значений
//сохраняются в List<int>
int il = list[0]; // никакой распаковки, приведения не нужны
foreach (int i2 in list)
{
Console.WriteLine(i2);
}
Безопасность типов
Другим свойством обобщений является безопасность типов. Когда в классе ArrayList
сохраняются объекты, то в коллекцию могут быть вставлены объекты различных типов. Следующий пример демонстрирует вставку целого, строки и объекта типа MyClass в коллекцию
ArrayList:
ArrayList list = new ArrayList();
list.Add(44);
list.Add("mystring");
list.Add(new MyClass());
Теперь, если реализовать итерацию по коллекции с помощью приведенного ниже оператора foreach, который проходит по целым элементам, то компилятор примет этот код. Од-
нако поскольку не все элементы коллекции могут быть приведены к типу int, возникнет ошибка во время выполнения:
foreach (int i in list)
{
Console.WriteLine(i);
}
Ошибки должны быть обнаружены как можно раньше. При использовании обобщенного
класса List<T> обобщенный тип-параметр Т задает тип элементов, который допускается
вставлять в коллекцию. Компилятор не пропустит следующий код, потому что метод Add()
имеет недопустимые аргументы:
var list = new List<int>();
list.Add(44);
list.Add(“mystring”);
// ошибка компиляции
list.Add(new MyClass());
// ошибка компиляции
Повторное использование двоичного кода
Обобщения повышают степень повторного использования двоичного кода. Обобщенный
класс может быть определен однажды, и на его основе могут быть созданы экземпляры многих
типов. При этом не нужно иметь доступ к исходным текстам, как это необходимо в случае шаблонов C++.
В качестве примера рассмотрим код, использующий класс List<T> пространства имен
System.Collections.Generic для создания специализированных версий, предназначенных
для хранения элементов типов int, string и MyClass:
3
var list = new List<int>();
list.Add(44);
var stringList = new List<string>();
stringList.Add(“mystring”);
var myclassList = new List<MyClass>();
myClassList.Add(new MyClass());
Обобщенные типы могут быть определены на одном языке, а использоваться на любом
другом из языков .NET.
“Разбухание” кода
Насколько много кода генерируется при создании экземпляров конкретных типов из
обобщений?
Поскольку определение обобщенного класса включается в сборку, создание на его основе конкретных классов специфических типов не приводит к дублированию кода в IL. Однако
когда обобщенные классы компилируются JIT-компилятором в родной машинный код, для
каждого конкретного типа значения создается новый класс. Ссылочные типы при этом разделяют общую реализацию одного родного класса. Причина в том, что в случае ссылочных типов
каждый элемент представлен в памяти 4-байтным адресом (на 32-разрядных системах) и машинные реализации обобщенного класса с различными ссылочными типами-параметрами не
отличаются друг от друга. В отличие от этого, типы значений содержатся в памяти целиком, и
поскольку каждый из них требует разного объема памяти, то для каждого из них создаются
свои экземпляры классов на основе обобщенного.
Рекомендации по именованию
Если в программе используются обобщения, то очень полезно, когда переменные обобщенных типов легко можно отличить от необобщенных. Ниже представлены рекомендации по
именованию обобщенных типов.
 Имена обобщенных типов должны начинаться с буквы Т.
 Если обобщенный тип может быть заменен любым классом, поскольку нет никаких
специальных требований, и используется только один обобщенный тип, Т — вполне
подходящее имя для обобщенного типа:
public class List<T> { }
public class LinkedList<T> { }
 Если к обобщенному типу предъявляются специальные требования (например, что тип
должен реализовывать интерфейс или наследоваться от определенного класса), либо
же используется два или более обобщенных типа в качестве параметров, то следует
применять осмысленные имена типов:
public delegate void EventHandler<TEventArgs>(object sender, TEventArgs e);
public delegate TOutput Converter(TInput, TOutput>(TInput from);
public class SortedList<TKey, TValue> { }
Создание обобщенных классов
Начнем с нормального, необобщенного упрощенного класса связного списка, который
может содержать объекты любого рода, а затем преобразуем его в обобщенный класс.
В связном списке один элемент ссылается на другой. Поэтому необходимо создать класс,
обертывающий объект внутри связного списка и ссылающийся на следующий объект. Класс
LinkedListNode содержит свойство по имени Value, которое инициализируется конструктором.
В дополнение к этому, класс LinkedListNode содержит ссылки на следующий и предыдущий
элемент в списке, которые также доступны через свойства.
4
public class LinkedListNode
{
public LinkedListNode(object value)
{
this.Value = value;
}
public object Value { get; private set; }
public LinkedListNode Next { get; internal set; }
public LinkedListNode Prev { get; internal set; }
}
Класс LinkedList включает свойства First и Last типа LinkedListNode, которые
отмечают начало и конец списка. Метод AddLast() добавляет новый элемент в конец списка.
Сначала создается объект типа LinkedListNode. Если список пуст, свойства First и Last
устанавливаются в этот элемент; в противном случае элемент добавляется как последний элемент списка. Реализуя метод GetEnumerator(), можно выполнить итерацию по списку оператором foreach. Метод GetEnumerator() использует оператор yield для создания типа перечислителя.
public class LinkedList: IEnumerable
{
public LinkedListNode First {get; private set;}
public LinkedListNode Last {get; private set;)
public LinkedListNode AddLast(object node)
{
var newNode = new LinkedListNode(node);
if (First == null)
{
First = newNode;
Last = First;
}
else
{
LinkedListNode previous = Last;
}
}
Last.Next = newNode;
Last = newNode;
Last.Prev = previous;
^
return newNode;
public IEnumerator GetEnumerator()
{
LinkedListNode current = First;
while (current != null)
{
yield return current.Value;
current = current.Next;
}
}
}
После этого класс LinkedList можно использовать с любым типом. В следующем фрагменте кода создается экземпляр нового объекта LinkedList, после чего к нему добавляется
два целочисленных значения и одно строковое. Поскольку целочисленный тип преобразуется в
объект, происходит упаковка, как было описано ранее. В операторе foreach выполняется распаковка. В операторе foreach элементы списка приводятся к целым числам, поэтому с третьим
элементом списка происходит исключение времени выполнения из-за невозможности осуществить приведение.
5
var list1 = new LinkedList () ;
list1.AddLast(2) ;
listl.AddLast(4);
list1.AddLast("6");
foreach (int i in list1)
{
Console.WriteLine(i);
}
Теперь давайте построим обобщенную версию связного списка. Обобщенный класс создается подобно нормальному классу, с объявлением обобщенного типа. Затем этот обобщенный
тип может использоваться внутри класса как поле-член либо с типами параметров методов.
Класс LinkedListNode объявлен с обобщенным типом Т. Свойство Value теперь будет иметь
тип Т вместо object; конструктор также изменен для того, чтобы принимать и возвращать
объект типа Т. Кроме того, обобщенный тип может быть возвращен и установлен, поэтому
свойства Next и Prev теперь имеют тип LinkedListNode<T>.
public class LinkedListNode<T>
{
public LinkedListNode(T value)
{
this.Value = value;
}
public T Value {get; private set;}
public LinkedListNode<T> Next {get; internal set;}
public LinkedListNode<T> Prev (get; internal set;}
}
Класс LinkedList также заменен обобщенным классом. LinkedList<T> содержит элементы типа LinkedListNode<T>. Тип Т из LinkedList определяет тип Т свойств First и
Last. Метод AddLast() теперь принимает параметр типа Т и создает экземпляр объекта
LinkedListNode<T>.
Наряду с интерфейсом IEnumerable, доступна также его обобщенная версия IEnumerable<T>. Интерфейс IEnumerable<T> унаследован от IEnumerable и добавляет метод
GetEnumerator(), возвращающий IEnumerator<T>. Интерфейс LinkedList<T> реализует
обобщенный интерфейс IEnumerable<T>.
6
public class LinkedList<T>: IEnumerable<T>
{
public LinkedListNode<T> First {get; vate set;}
public LinkedListNode<T> Last {get; private set;}
public LinkedListNode<T> AddLast(T node)
{
var newNode = new LinkedListNode<T>(node);
if (First == null)
{
First = newNode;
Last = First;
}
else
{
LinkedListNode previous = Last;
Last.Next = newNode;
Last = newNode;
Last.Prev = previous;
}
return newNode;
{
public IEnumerator<T> GetEnumerator()
{
LinkedListNode<T> current = First;
while (current != null)
{
yield return current.Value;
current = current.Next;
}
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
Имея обобщенный LinkedList<T>, можно создавать его экземпляры с типом-параметром int, и при этом никакой упаковки/распаковки не понадобится. К тому же, если не передатт AddList() параметр типа int, возникнет ошибка компиляции. Применение обобщенного
IEnumerable<T> обеспечит безопасность типа в операторе foreach; если переменная в операторе foreach не будет иметь тип int, также возникнет ошибка компиляции.
var list2 = new LinkedList<int>();
list2.AddLast(1);
list2.AddLast(3);
list2.AddLast(5);
foreach (int i in list2)
{
Console.WriteLine(i);
}
Аналогично можно использовать обобщенный LinkedList<T> с типом string и передавать методу AddList() строки:
var list3 = new LinkedList<string>();
list3.AddLast("2");
list3.AddLast("four");
list3.AddLast("foo");
foreach(string s in list3)
{
Console.WriteLine(s);
}
Каждый класс, имеющий дело с объектным типом, является кандидатом на
обобщенную реализацию. К тому же, если классы используют иерархии, то обобщения могут помочь исключить необходимость в приведении типов.
Средства обобщений
При создании обобщенных классов могут понадобиться дополнительные ключевые слова С#. Например, обобщенному типу невозможно присвоить null. В этом случае можно использовать ключевое слово default, как показано в следующем разделе. Если обобщенный
тип не нуждается в средствах класса Object, но необходимо вызывать некоторые специфические методы в обобщенном классе, можно определить ограничения (constraints).
В настоящем подразделе рассматриваются следующие темы:
-
7
значения по умолчанию;
ограничения;
наследование;
статические члены.
Начнем этот пример с обобщенного диспетчера документов. Диспетчер документов используется для чтения и записи документов в очереди. Создадим новый консольный проект по
имени DocumentManager и добавим класс DocumentManager<T>. Метод AddDocument()
добавляет документ в очередь. Доступное только для чтения свойство IsDocumentAvailable
возвращает true, если очередь не пуста.
using System;
using System.Collections.Generic;
namespace Wrox.ProCSharp.Generics
{
public class DocumentManager<T>
{
private readonly Queue<T> documentQueue = new Queue<T>();
public void AddDocument(T doc)
{
lock (this)
{
documentQueue.Enqueue(doc);
}
}
public bool IsDocumentAvailable
{
get {return documentQueue.Count >0;}
}
}
}
Значения по умолчанию
Теперь необходимо добавить в класс DocumentManager<T> метод GetDocument().
Внутри этого метода тип Т должен быть присвоен null. Однако обобщенному типу нельзя присваивать null. Причина в том, что обобщенный тип позволяет создавать экземпляр как тип
значения, a null допускается только для ссылочных типов. Чтобы преодолеть эту проблему,
следует воспользоваться ключевым словом default. С помощью ключевого слова default
ссылочным типам присваиваются значения null, а типам значений  0.
public Т GetDocument()
{
Т doc = default(Т);
lock (this)
{
doc = documentQueue.Dequeue();
}
return doc;
}
В зависимости от контекста, в котором оно применяется, ключевое слово default
имеет различный смысл. Оператор switch использует default для определения варианта по умолчанию, а в случае с обобщениями default применяется для инициализации обобщенных типов значениями либо null, либо 0 — соответственно, для ссылочного типа или типа значения.
Ограничения
Если обобщенному классу нужно вызывать некоторые методы из обобщенного типа, вы
должны добавить ограничения. В случае DocumentManager<T> все заголовки документов
должны отображаться в методе DisplayAllDocuments(). Класс Document реализует интерфейс IDocument со свойствами Title и Content:
8
public interface IDocument
{
string Title { get; set; }
string Content ( get; set; }
}
public class Document: IDocument
{
public Document()
{
}
public Document(string title, string content)
{
this.title = title;
this.content = content;
}
public string Title { get; set; }
public string Content { get; set; )
}
При отображении документов в классе DocumentManager<T> тип Т можно приводить к
IDocument для отображения заголовка:
public void DisplayAllDocuments()
{
foreach (T doc in documentQueue)
{
Console.WriteLine(((IDocument)doc).Title);
}
}
Проблема в том, что выполнение приведения приводит к исключению времени выполнения, если тип Т не реализует интерфейс IDocument. Вместо этого может быть лучше определить с помощью класса DocumentManager<TDocument> ограничение, гласящее, что тип
TDocument обязан реализовать интерфейс IDocument. Чтобы прояснить требование в имени
обобщенного типа, Т заменим типом TDocument. Конструкция where определяет требование о
реализации интерфейса IDocument:
public class DocumentManager<TDocument>
where TDocument: IDocument
{
В этом случае можно записать оператор foreach так, вроде тип TDocument содержит
свойство Title.
Вы получите поддержку от средства IntelliSense и от компилятора:
public void DisplayAllDocuments()
{
foreach (TDocument doc in documentQueue)
{
Console.WriteLine(doc.Title);
}
}
В методе Main() создается экземпляр класса DocumentManager<T> с типом Document,
реализующим интерфейс IDocument. Затем добавляются и отображаются новые документы, а
один из документов извлекается.
static void Main()
{
var dm = new DocumentManager<Document>();
dm.AddDocument(new Document("Заголовок А", "Пример A")) ;
dm.AddDocument(new Document("Заголовок Б", "Пример Б"));
dm.DisplayAllDocuments();
if (dm.IsDocumentAvailable)
{
Document d = dm.GetDocument();
Console.WriteLine(d.Content);
}
9
}
Теперь DocumentManager будет работать с любым классом, реализующим интерфейс
IDocument.
В приведенном примере вы видели ограничение интерфейса. Помимо этого обобщения
поддерживают несколько других типов ограничений, которые описаны в табл. 5.1.
Таблица 5.1. Ограничения, поддерживаемые обобщениями
Ограничение
Описание
where T: struct
При ограничении struct тип Т должен быть типом значений
Ограничение class указывает на то, что тип Т должен быть ссылочным типом
where T: IFoo специфицирует, что тип Т обязан реализовать интерфейс IFoo
where T: Foo специфицирует, что тип Т должен наследоваться от
where T: class
where T: IFoo
where T: Foo
where T: new()
where T1: T2
Foo
where T: new() – ограничение конструктора, указывающее на то,
что тип Т должен иметь конструктор по умолчанию
С помощью ограничений можно указать, что тип Т1 наследуется от
обобщенного типа Т2. Такое ограничение известно как ограничение
“голого” типа
Ограничения конструктора могут быть определены только для конструктора по
умолчанию. Для других конструкторов определение таких ограничений невозможно.
С обобщенным типом можно также комбинировать множество ограничений. Ограничение where Т: IFoo, new() в объявлении MyClass<T> указывает, что тип Т реализует интерфейс IFoo и имеет конструктор по умолчанию:
public class MyClass<T>
where T: IFoo, new()
{
//...
Одним важным ограничением конструкции where в C#является невозможность определения операций, которые должны быть реализованы обобщенным типом. Операции нельзя определять в интерфейсах. С конструкцией where можно определять
только базовые классы, интерфейсы и конструктор по умолчанию.
Наследование
Созданный ранее класс LinkedList<T> реализует интерфейс IEnumerable<T>:
public class LinkedList<T>: IEnumerable<out T>
{
//...
Обобщенный тип может реализовать обобщенный интерфейс. То же самое возможно за
счет наследования от класса. Обобщенный класс может быть унаследован от обобщенного базового класса:
10
public class Base<T>
{
}
public class Derived<T>: Base<T>
{
}
Существующее требование состоит в том, что обобщенные типы интерфейса должны
повторяться, либо должен быть указан тип базового класса, как в следующем случае:
public class Base<T>
{
}
public class Derived<T>: Base<string>
{
}
Таким образом, производный класс может быть обобщенным или не обобщенным.
Например, можно определить обобщенный абстрактный базовый класс, реализованный конкретным типом в производном классе. Это позволяет делать специализации для определенных
типов:
public abstract class Calc<T>
{
public abstract T Add(T x, T y) ;
public abstract T Sub(T x, T y);
}
public class IntCalc: Calc<int>
{
public override int Add(int x, int y)
{
return x + y;
}
public override int Sub(int x, int y)
{
return x - y;
}
}
Статические члены
Статические члены обобщенных классов требуют особого внимания. Статические члены
обобщенного класса разделяются только одним экземпляром класса. Рассмотрим пример, в котором класс StaticDemo<T> содержит статическое поле х:
public class StaticDemo<T>
{
public static int x;
}
Поскольку класс StaticDemo<T> используется как с типом string, так и с типом int,
существуют два набора статических полей:
StaticDemo<string>.x = 4;
StaticDemo<int>.x = 5;
Console.WriteLine(StaticDemo<string>.x); // записывает 4
Обобщенные интерфейсы
11
Применяя обобщения, можно определять интерфейсы, объявляющие методы с обобщенными параметрами. В примере со связным списком вы уже реализовали интерфейс IEnumerable<T>, который определяет метод GetEnumerator() для возврата IEnumerable<out Т>. В
.NET предлагается множество обобщенных интерфейсов для различных сценариев; к примерам
можно отнести IComparable<T>, ICollection<T> и IExtensibleObject<T>. Часто доступны старые, не обобщенные версии того же интерфейса; например, в .NET 1.0 существовал интерфейс IComparable, основанный на объектах. Интерфейс IComparable<in Т> основан на
обобщенном типе:
public interface IComparable<in T>
{
int CompareTo(T other);
}
Старый необобщенный интерфейс IComparable, который требует объекта в методе
CompareTo(). Это требует приведения к специфическим типам, таким как класс Person для
использования свойства LastName:
public class Person: IComparable
{
public int CompareTo(object obj)
{
Person other = obj as Person;
return this.lastname.CompareTo(other.LastName);
}
//...
В реализации обобщенной версии необходимость приведения объекта к типу Person отпадает:
public class Person: IComparable<Person>
{
public int CompareTo(Person other)
{
return this.lastname.CompareTo(other.LastName);
}
//...
Ковариантность и контравариантность
До выхода версии .NET 4 обобщенные интерфейсы были инвариантными. В .NET 4 появилось важное расширение для обобщенных интерфейсов и обобщенных делегатов  ковариантность и контравариантность. Эти понятия касаются преобразований типов аргументов и возвращаемых типов. Например, можно ли передать тип Rectangle методу, принимающему
Shape? Давайте обратимся к примерам, чтобы увидеть преимущества этих расширений.
В .NET типы параметров ковариантны. Предположим, что есть классы Shape и Rectangle, причем Rectangle наследуется от базового класса Shape. Метод Display() объявлен для приема объекта типа Shape в качестве параметра:
public void Display(Shape о) { }
Теперь можно передавать любой объект, который наследуется от базового класса. Поскольку класс Rectangle унаследован от Shape, он отвечает всем требованиям Shape, и компилятор разрешает следующий вызов метода:
Rectangle r = new Rectangle { Width= 5, Height=2.5};
Display (r);
Возвращаемые типы методов контравариантны. Когда метод возвращает Shape, присвоить его Rectangle невозможно, поскольку Shape не обязательно всегда Rectangle. Вот
противоположное  возможно. Если метод возвращает Rectangle как метод
GetRectangle():
public Rectangle GetRectangle();
12
то результат может быть присвоен Shape:
Shape s = GetRectangle() ;
До версии .NET 4 такое поведение с обобщениями было невозможным. В C# 4 язык расширен для поддержки ковариантности и контравариантности с обобщенными интерфейсами и
обобщенными делегатами. Давайте начнем с определения базового класса Shape для класса
Rectangle:
public class Shape
{
public double Width {get; set;}
public double Height (get; set;}
public override string ToString()
{
return String.Format("Width: {0}, Height: {1}”, -Width, Height);
}
}
public class Rectangle: Shape
{
}
Ковариантность обобщенных интерфейсов
Обобщенный интерфейс ковариантен, если обобщенный тип аннотирован ключевым
словом out. Это также означает, что тип Т разрешен только в качестве типа возврата. Интерфейс
IIndex ковариантен с типом Т и возвращает этот тип из доступного только для чтения индексатора:
public interface IIndex<out Т>
{
Т this[int index] { get; }
int Count { get; }
}
Если с интерфейсом IIndex используется индексатор чтения-записи, то обобщенный тип Т передается в метод и также возвращается из метода. Это невозможно при ковариантности  обобщенный тип должен быть определен как инвариант. Определение типа как инвариантного задается аннотациями out и in.
Интерфейс IIndex<T> реализован классом RectangleCollection. Класс RectangleCollection определяет Rectangle для обобщенного типа Т:
using System;
namespace Wrox.ProCSharp.Generics
{
public class RectangleCollection : IIndex<Rectangle>
{
private Rectangle[] data = new Rectangle[3]
{
new Rectangle { Height=2, Width=5},
new Rectangle { Height=3, Width=7},
new Rectangle { Height=4.5, Width=2.9}
};
public static RectangleCollection GetRectangles()
{
return new RectangleCollection();
}
13
public Rectangle this[int index]
{
get
{
if (index < 0 || index > data.Length)
throw new ArgumentOutOfRangeException("index");
return data[index];
}
}
public int Count
{
get
{
return data.Length;
}
}
}
}
Метод RectangleCollection.GetRectangles() возвращает RectangleCollection,
реализующий интерфейс IIndex<Rectangle>, так что возвращенное значение можно присвоить переменной rectangles типа IIndex<Rectangle>. Поскольку интерфейс ковариантен, возвращенное значение можно присвоить также и переменной типа IIndex<Shape>. Типу
Shape не нужно ничего более того, что может предоставить Rectangle. Через переменную
shapes индексатор из интерфейса и свойство Count используются внутри цикла for:
static void Main()
{
IIndex<Rlectangle> rectangles = RectangleCollection.GetRectangles();
IIndex<Shape> shapes = rectangles;
for (int i = 0; i < shapes.Count; i++)
{
Console.WriteLine(shapes[i]) ;
}
}
Контравариантность обобщенных интерфейсов
Обобщенный интерфейс контравариантен, если обобщенный тип аннотирован ключевым словом in. Таким образом, интерфейс позволяет использовать обобщенный тип Т только
в качестве входного для своих методов:
public interface IDisplay<in Т>
{
void Show(T item);
}
Класс ShapeDisplay реализует интерфейс IDisplay<Shape> и использует объект
Shape в качестве входного параметра:
public class ShapeDisplay: IDisplay<Shape>
{
public void Show(Shape s)
{
Console.WriteLine ("{0} Width: {1}, Height: {2}", s.GetType().Name,
s.Width, s.Height);
}
}
14
Создание нового экземпляра ShapeDisplay возвращает IDisplay<Shape>, который
присваивается переменной shapeDisplay. Поскольку IDisplay<T> контравариантен, результат можно присвоить также IDisplay<Rectangle>, где тип Rectangle унаследован от
Shape. На этот раз методы интерфейса определяют только обобщенный тип в качестве входного, a Rectangle удовлетворяет всем требованиям Shape.
static void Main()
{
//...
IDisplay<Shape> shapeDisplay = new ShapeDisplay();
IDisplay<Rectangle> rectangleDisplay = shapeDisplay;
rectangleDisplay.Show(rectangles[03);
}
Обобщенные структуры
Подобно классам, структуры также могут быть обобщенными. Они очень похожи на
обобщенные классы, за исключением возможности наследования. В этом разделе рассматривается обобщенная структура Nullable<T>, которая определена в .NET Framework.
Итак, примером обобщенный структуры в .NET Framework является Nullable<T>. Число в базе данных и число в языке программирования имеют важное отличие в своих характеристиках, поскольку число в базе данных может быть null. Число в C# не может быть null.
Проблема существует не только с базами данных, но также с отображением данных XML на
типы .NET.
Это отличие часто служит источником сложностей и требует массы дополнительной работы по отображению данных. Одно из решений состоит в отображении чисел из баз данных и
файлов XML на ссылочные типы, потому что ссылочные типы могут иметь значение null. Однако это также означает дополнительные накладные расходы во время выполнения.
За счет использования структуры Nullable<T> эта проблема может быть легко решена.
Ниже показан фрагмент кода с упрощенной версией определения типа Nullable<T>.
Структура Nullable<T> определяет ограничение, которое состоит в том, что обобщенный тип Т должен быть структурой. С классами в качестве обобщенных типов пре-
имущество минимальных накладных расходов исчезло бы, и поскольку объекты классов все
равно могут быть null, то в использовании класса с типом Nullable<T> смысла нет. Единственное дополнение к типу Т, определенное Nullable<T>, состоит булевском поле hasValue,
которое определяет, установлено значение или же оно равно null. Помимо этого, обобщенная
структура определяет доступные только для чтения свойства HasValue и Value, а также перегрузки некоторых операций. Перегрузка операции приведения Nullable<T> к Т определена
явно, так как она может генерировать исключение в случае, если hasValue равно false. Перегрузка операции для приведения к Nullable<T> определена неявно, потому что она всегда
успешна:
public struct Nullable<T>
where T: struct
{
public Nullable(T value)
{
this.hasValue = true;
this.value = value;
}
private bool hasValue;
public bool HasValue
{
get
{
return hasValue;
}
}
15
private T value;
public T Value
{
get
{
if (!hasValue)
{
throw new InvalidOperationException("no value");
}
return value;
}
}
public static explicit operator T(Nullable<T> value)
{
return value.Value;
}
public static implicit operator Nullable<T>(T value)
{
return new Nullable<T>(value);
}
public override string ToString()
{
if (!HasValue)
return String.Empty;
return this.value.ToString();
}
}
В этом примере экземпляр Nullable<T> создан как Nullable<int>. Переменная х теперь может быть использована как.int, т.е. ей можно присваивать значения и применять в операциях для выполнения некоторых вычислений. Такое поведение стало возможным благодаря
операциям приведения типа Nullable<T>. Однако х также может быть null. Свойства HasValue и Value типа Nullable<T> могут проверять, есть ли значение, и обращаться к нему:
Nullable<int> х;
х = 4;
х += 3;
if(х.HasValue)
{
int у = х.Value;
}
х = null;
Поскольку допускающие null типы используются часто, в C# предусмотрен специальный синтаксис для определения переменных подобного рода. Вместо применения синтаксиса с
обобщенной структурой можно использовать операцию ?. В следующем примере переменные
х1 и х2 являются экземплярами типа int, допускающего null:
Nullable<int> xl;
int? x2;
Допускающий null тип можно сравнивать с null и числами, как показано ниже. Здесь
значение х сравнивается с null, и если оно не равно null, то сравнивается со значением 0:
16
int? х = GetNullableType!);
if (x == null)
{
Console.WriteLine("x равно null");
}
else if (x < 0)
{
Console.WriteLine("x меньше 0") ;
}
Теперь, когда известно, как определен тип Nullable<T>, давайте приступим к использованию типов, допускающих null. Эти типы могут использоваться с арифметическими операциями. Переменная х3 равна сумме переменных х1 и х2. Если любое из слагаемых содержит
значение null, то результат тоже будет null:
int? xl = GetNullableType();
int? х2 = GetNullableType();
int? хЗ = xl + х2;
\
Вызываемый здесь метод GetNullableType()  это только место заполнения для любого метода, который возвращает тип int, допускающий null. Для целей тестирования его
можно реализовать как просто возвращающий null либо какое-то целочисленное значение.
Типы, не допускающие null, могут быть преобразованы в типы, допускающие null.
При этом возможно неявное преобразование, не требующее приведения. Такое преобразование
всегда успешно:
int yl = 4;
int? xl = yl;
В противоположной ситуации операция преобразования от допускающего null типа к
не допускающему null может дать сбой. Если допускающий null тип содержит значение
null, и оно присваивается не допускающему null типу, генерируется исключение InvalidOperationException. Вот почему здесь нужна операция явного приведения:
int? xl = GetNullableType();
int yl = (int)xl;
Вместо выполнения явного приведения преобразовать допускающий null тип в не допускающий также можно с помощью операции поглощения (coalescing). В синтаксисе ?? опе-
рации поглощения определяется значение по умолчанию для преобразования в случае, если допускающий null тип как раз имеет значение null. В следующем коде y1 получает значение 0,
если x1 равно null:
int? x1 = GetNullableType();
int y1 = xl ?? 0;
Обобщенные методы
В дополнение к обобщенным классам можно также определять обобщенные методы. В
объявлении обобщенного метода присутствует обобщенный тип. Обобщенные методы могут
быть определены внутри необобщенного класса.
Метод Swap<T> определяет Т как обобщенный тип, который используется для двух аргументов и переменной temp:
void Swap<T>(ref Т х, ref Т у)
{
Т temp;
temp = х;
х = у;
у = temp;
}
Обобщенный метод может быть вызван с указанием конкретного типа вместо обобщенного:
17
int i = 4 ;
int j = 5;
Swap<int>(ref i, ref j);
Однако поскольку компилятор C# может получить тип для подстановки из типа параметров, указывать его при вызове такого метода не обязательно. Обобщенный метод может
быть вызван точно так же, как простой необобщенный:
int i = 4;
int j = 5;
Swap(ref i, ref j);
Пример обобщенного метода
Ниже приведен пример применения обобщенного метода для накопления всех элементов
коллекции. Для того чтобы продемонстрировать возможности обобщенных методов, воспользуемся классом Account, который имеет свойства Name и Balance:
public class Account
{
public string Name {get; private set;}
public decimal Balance {get; private set;}
public Account(string name, Decimal balance)
{
this. Name = name;
this.Balance = balance;
}
}
Все счета, баланс которых нужно аккумулировать, добавляются в список счетов типа
List<Account>:
var accounts = new List<Account>()
{
new Account("Christian", 1500),
new Account("Stephanie", 2200),
new Account("Angela", 1800)
};
Традиционный способ накопления всех объектов Account заключается в применении
цикла foreach для прохода по всем объектам Account, как показано ниже. Поскольку оператор foreach требует интерфейса IEnumerable для выполнения итерации по элементам коллекции, аргумент метода AccumulateSimple() имеет тип IEnumerable. Оператор foreach
работает с любым объектом, реализующим IEnumerable. Таким образом, метод
AccumulateSimple() может быть использован со всеми классами коллекций, которые реализуют интерфейс IEnumerable. В реализации этого метода производится прямое обращение
к свойству Balance класса Account:
public static class Algorithm
{
public static decimal AccumulateSimple(IEnumerable<Account> source)
{
decimal sum = 0;
foreach (Account a in source)
{
sum += a.Balance;
}
return sum;
}
}
Метод AccumulateSimple() вызывается следующим образом:
decimal amount = Algorithm.AccumulateSimple(accounts);
Обобщенные методы с ограничениями
18
Проблема первой реализации связана с тем, что она работает только с объектами Account. Этого можно избежать, применив обобщенный метод.
Вторая версия Accumulate() принимает только тип, реализующий интерфейс
IAccount. Как вы уже видели в случае обобщенных классов, обобщенный тип может быть
ограничен с помощью конструкции where. То же ограничение, которое используется с обобщенными классами, может быть применено и к обобщенным методам. Изменим параметр метода Accumulate() на IEnumerable<T>. Интерфейс IEnumerable<T>  это обобщенная версия
интерфейса IEnumerable, которая реализована обобщенными классами коллекций:
public static decimal Accumulate<TAccount>(IEnumerable<TAccount> source)
where TAccount: IAccount
{
decimal sum = 0;
foreach (TAccount a in source)
{
sum += a.Balance;
}
return sum;
}
Класс Account теперь реконструирован, чтобы реализовать интерфейс IAccount:
public class Account: IAccount
{
//...
Интерфейс IAccount определяет доступные только для чтения свойства Balance и
Name:
public interface IAccount
{
decimal Balance { get; }
string Name { get; }
}
Новый метод Accumulate() может быть вызван определением типа Account в качестве
параметра обобщенного типа:
decimal amount = Algorithm.Accumulate<Account>(accounts);
Поскольку шаблонный параметр может быть автоматически выведен компилятором на
основе типа параметра метода, допускается вызывать метод Accumulate() следующим образом:
decimal amount = Algorithm.Accumulate(accounts);
Обобщенные методы с делегатами
Требование реализации интерфейса IAccount для обобщенного типа может оказаться
слишком строгим. В следующем примере показано, как можно изменить метод Accumulate()
передачей обобщенного делегата. Подробные сведения о работе с обобщенными делегатами и
применении лямбда-выражений даны в разделе 6.8.
Метод Accumulate() использует два обобщенных параметра  Т1 и Т2. Тип Т1 выбран
для реализующего коллекцию IEnumerable<T> параметра, который первым передается в метод. Второй параметр использует обобщенный делегат Func<Tl,Т2,TResult>. Здесь второй и
третий обобщенные параметры относятся к одному и тому же типу Т2. Это значит, что должен
передаваться метод, имеющий два входных параметра (Т1 и Т2) и возвращающий Т2:
19
public static Т2 Accumulate<Tl, Т2> (IEnumerable<Tl> source,
Func<Tl, T2, T2> action)
{
Т2 sum = default (Т2) ;
foreach (T1 item in source)
{
sum = action(item, sum);
}
return sum;
}
При вызове этого метода необходимо специфицировать параметры  обобщенные типы,
потому что компилятор не может вывести,их автоматически. Первым параметром метода является коллекция accounts, которой присвоено значение типа IEnumerable<Account>. Во втором параметре используется лямбда-выражение, определяющее два параметра типа Account и
decimal и возвращающее decimal. Это лямбда-выражение вызывается для каждого элемента
методом Accumulate():
decimal amount = Algorithm.Accumulate<Account, decimal>(
accounts, (item, sum) => sum += item.Balance);
He пугайтесь, если этот синтаксис пока непонятен. Приведенный пример просто демонстрирует возможные способы расширения метода Accumulate(). Исчерпывающие сведения о лямбда-выражениях вы получите в разделе 6.8.
Специализация обобщенных методов
Обобщенные методы могут быть перегружены для определения специализаций определенных типов. Это также верно и для обобщенных параметров. Метод Foo() определен в двух
версиях. Первая принимает обобщенный параметр, а вторая  специализированную версию параметра int. Во время компиляции выбирается наилучшее соответствие. Если передан int,
выбирается метод с параметром int. С любым другим типом параметра компилятор выбирает
обобщенную версию метода.
public class MethodOverloads
{
public void Foo<T>(T obj)
{
Console.WriteLine("Foo<T>(T obj), тип obj : {0}",
obj.GetType().Name);
}
public void Foo(int x)
{
Console.WriteLine("Foo(int x)");
}
public void Bar<T>(T obj)
{
Foo(obj);
}
}
Метод Foo() теперь может быть вызван с любым типом параметра. В приведенном ниже примере кода в Foo() передается int и string:
static void Main()
{
var test = new MethodOverloads() ;
test.Foo(33);
test.Foo("abc");
}
На основании вывода, полученного во время выполнения этого кода, можно убедиться,
что в каждом случае компилятором выбирается наиболее подходящий метод:
20
Foo(int х)
Foo<T>(T obj), тип obj: String
Необходимо помнить, что вызываемый метод определяется во время компиляции, а не
выполнения. Это легко продемонстрировать, добавив обобщенный метод Ваг(), который вызывает метод Foo(), передав ему значение обобщенного параметра:
public class MethodOverloads
{
//...
public void Bar<T>(T obj)
{
Foo(obj) ;
}
Изменим метод Main() для вызова Bar() с передачей ему значения int:
static void Main()
{
var test = new MethodOverloads() ;
test.Bar(44);
В выводе на консоль можно заметить, что методом Ваr() был выбран обобщенный метод Foo(), а вовсе не перегрузка с параметром int. Причина в том, что компилятор выбирает
метод, вызываемый Ваr(), во время компиляции. Так как метод Ваг() определяет обобщенный параметр и поскольку метод Foo() соответствует этому типу, вызывается именно обобщенный метод Foo(). И это не изменяется во время выполнения, когда методу Ваг() передается значение int:
Foo<T>(T obj), тип obj: Int32
Итоги
В этой главе было представлено очень важное средство CLR  обобщения. Благодаря
механизму обобщений, можно создавать классы и методы, независимые от типов. Интерфейсы,
структуры и делегаты также могут быть созданы в обобщенной манере. Обобщения позволяют
применить новый стиль программирования. Было показано, как алгоритмы, в частности, действия и предикаты, могут быть реализованы для работы с различными классами — и все это с
обеспечением безопасности типов. Обобщенные делегаты позволяют отделить алгоритмы от
коллекций.
На протяжении всей книги вы встретите другие средства и применения обобщений. В
разделе 6.8 будут представлены делегаты, которые часто реализуются как обобщения, в разделе
6.10 дается информация о классах обобщенных коллекций, а в разделе 6.11 рассматриваются
обобщенные расширяющие методы.
В следующем разделе демонстрируется использование обобщенных методов с массивами.
21
Download