Ограничения

До сих пор большинство приведенных примеров обобщений включали некоторого рода классы в стиле коллекций, способные хранить множества объектов или значений определенного типа.

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

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

Может показаться, что следующий код успешно решит задачу:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
using System;
using System.Collections.Generic;
public interface IShape
{
double Area {
get;
}
public class Circle : IShape {
public Circle ( double radius )  {
this.radius = radius;
}
public double Area {
get {
return 3.1415*radius*radius;
}
}
private double radius;
}
public class Rect : IShape {
public Rect( double width, double height )  {
this.width = width;
this.height = height;
}
public double Area {
get {
return width*height;
}
private double width;
private double height;
}
public class Shapes<t> {
public double TotalArea {
get {
double acc = 0;
foreach ( T shape in shapes ) { // Это не компилируется! ! !
асе += shape.Area;
}
return acc;
}
public void Add ( T shape )  {
shapes.Add( shape );
}
private List<t> shapes = new List<t>();
}
public class EntryPoint {
static void Main()  {
Shapes<ishape> shapes = new Shapes<ishape> ();
shapes.Add ( new Circle (2)  );
shapes.Add ( new Rect(3, 5)  );
Console.WriteLine ( "Общая площадь:  {0}", shapes.TotalArea );
}

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

error CS0117: ‘Tf does not contain a definition for ‘Area’ ошибка CS0117: T не содержит определения Area

ошибка CS0117: T не содержит определения Area

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

Поэтому принимается решение, что обе фигуры должны реализовать интерфейс IShape. Таким образом, интерфейс IShape определит контракт, и фигуры реализуют его. Однако этого все еще недостаточно, чтобы приведенный код мог быть скомпилирован.

Обобщения С# должны иметь способ навязать правило, гласящее, что тип Т обязан поддерживать определенный контракт во время выполнения. Наивная попытка решения этой проблемы могла бы выглядеть так:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Shapes<t> {
public double TotalArea {
get {
double acc = 0;
foreach( T shape in shapes )  {
//HE ПОСТУПАЙТЕ ТАК!!!
IShape theShape = (IShape) shape;
acc += theShape.Area;
}
return acc;
}
public void Add( T shape )  {
shapes.Add( shape );
}
private List<t> shapes = new List<t>();
}

Эта модификация Shapes действительно будет компилироваться и работать в большинстве случаев. Однако такое обобщение теряет некоторую часть своей чистоты из-за приведения типа внутри цикла foreach. Только представьте, что будет, если попытаться создать сконструированный тип Shapes.

Компилятор с удовольствием его проглотит. Но что случится, когда дело дойдет до чтения свойства TotalArea экземпляра Shapes? Как и можно ожидать, когда метод доступа get свойства TotalArea попытается выполнить приведение int к IShape, сгенерируется исключение времени выполнения.

Одним из главных преимуществ использования обобщений является повышенная безопасность типов, но в данном примере вся эту безопасность попросту отброшена. Каким же образом поступить? Ответ содержится в концепции, называемой ограничениями обобщений (generic constraints).

Взгляните на правильную реализацию:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Shapes<t> where T: IShape
public double TotalArea {
get {
double acc = 0;
foreach ( Т shape in shapes )  {
acc += shape.Area;
}
return acc;
}
public void Add( T shape )  {
shapes.Add( shape );
}
private List<t> shapes = new List<t>();
}

Обратите внимание на дополнительную строку, которая следует сразу за первой строкой объявления класса и в которой используется ключевое слово where. Она говорит следующее: “Определить класс Shapes, где Т должен реализовывать интерфейс IShape”.

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

Синтаксис ограничений замечательно прост.

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

Кроме того, вместо указания имени класса первичное ограничение может перечислять специальные слова class или struct, используемые для указания того, что параметр типа может быть классом или структурой.

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

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

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

Рассмотрим несколько примеров ограничений:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
using System.Collections.Generic;
public class MyValueList<t>
where T: struct
// А так нельзя:
// where T: struct, new()
{
public void Add( T v )  {
imp.Add( v ) ;
}
private List<t> imp = new List<t>();
}
public class EntryPoint {
static void Main()  {
MyValueList<int> intList = new MyValueList<int>();
intList.Add( 123 ) ;
// ТАК НЕЛЬЗЯ.
// MyValueList<object> objList = // new MyValueList<object> ();
}

В этом коде показан пример применения ограничения struct в объявлении контейнера, который может содержать только типы значений. Данное ограничение предотвращает объявление переменной obj List. Это объявление помещено в комментарий, потому что иначе во время компиляции было бы выдано следующее сообщение об ошибке:

error CS0453: The type ‘object1 must be a non-nullable value type in order to use it as parameter fT’ in the generic type or method ‘MyValueList

ошибка CS0453: Тип object должен быть типом значений, не допускающим null, чтобы его можно было использовать в качестве параметра Т в обобщенном типе или методе MyValueList

В качестве альтернативы может быть сформулировано ограничение, разрешающее только типы классов. Между прочим, в версии компилятора С# из Visual Studio создать ограничение, включающее и class и struct, невозможно. Конечно, это было бы бессмысленно, поскольку аналогичный эффект дает отсутствие обоих ограничений в списке. Тем не менее, если попытаться сделать это, компилятор выдаст следующее сообщение об ошибке:

error CS0449: The ‘class’ or ’struct’ constraint must come before any other constraints
ошибка CS0449: Ограничение class или struct должно находиться перед любыми другими ограничениями

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

Тем не менее, компилятор не позволяет применить ограничение new () вместе с ограничением struct.

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

1
2
3
4
5
6
7
8
9
10
using System;
using System.Collections.Generic-public interface IValue {
// Методы IValue.
}
public class MyDictionary<tkey, TValue> where TKey: struct, IComparable<tkey> where TValue: IValue, new()
{
public void Add( TKey key, TValue val )  {
imp.Add( key, val );
}
private Dictionary<tkey, TValue> imp = new Dictionary<tkey, TValue>();

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

В пример показаны две конструкции ограничений — по одной для каждого параметра типа. В этом случае типу TValue разрешено быть структурой или классом, но при этом он должен поддерживать интерфейс IValue вместе с конструктором по умолчанию.

В целом механизм ограничений, встроенный в обобщения С#, прост и понятен. Сложностью ограничений легко управлять и расшифровывать ее с минимумом (или без) сюрпризов. С развитием языка и CLR можно ожидать, что эта часть будет дополнена по мере нахождения областей для применения обобщений. Например, возможность использования ограничений class и struct была добавлена к стандарту относительно недавно.

И, наконец, формат ограничений на обобщенные интерфейсы идентичен тому же формату для классов и структур.

на неклассовых типах

До сих пор речь шла об ограничениях в контексте классов, структур и интерфейсов. В действительности любая сущность, которую можно объявить обобщенным образом, допускает применение ограничений.

В объявлениях обобщенных методов и делегатов ограничение следует за списком формальных параметров метода или делегата.

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

1
2
3
4
5
6
7
8
9
10
11
12
using System;
public delegate R Operational, T2, R> ( Tl vail,
T2 val2 )
where Tl: struct where T2: struct where R: struct;
public class EntryPoint {
public static double Add( int vail, float val2 )  {
return vail + val2;
}
static void Main()  {
var op = new Operation<int, float, double> ( EntryPoint.Add );
Console.WriteLine ( "{0} + {1} = {2}", 1, 3.2, op(l, 3.2f) );
}

В коде объявляется обобщенный делегат для метода операции, который принимает два параметра и имеет возвращаемое значение. Ограничение заключается в том, что параметры и возвращаемое значение должны быть типами значений.

Аналогично, для обобщенных методов конструкции ограничений следуют за объявлением метода, но предшествуют его телу.

Вы можете следить за любыми ответами на эту запись через RSS 2.0 ленту. Вы можете оставить ответ, или trackback с вашего собственного сайта.

Оставьте отзыв

XHTML: Вы можете использовать следующие теги: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

 
Rambler's Top100