Сообщений 5    Оценка 15 [+2/-0]         Оценить  
Система Orphus

Обобщения

Глава из книги “Язык программирования C# 2005 (Си Шарп) и платформа .NET 2.0”

Автор: Эндрю Троелсен
Источник: Язык программирования C# 2005 (Си Шарп) и платформа .NET 2.0
Материал предоставил: Издательство ''Вильямс''
Опубликовано: 30.09.2006
Версия текста: 1.0
Снова о создании объектных образов, восстановлении значений и System.Object
Проблемы создания объектных образов и восстановления значений
Типовая безопасность и строго типизованные коллекции
Проблемы создания объектных образов и строго типизованные коллекции
Пространство имен System.Collections.Generic
Тип List<T>
Создание обобщенных методов
Пропуск параметров типа
Создание обобщенных структур (и классов)
Ключевое слово default в обобщенном программном коде
Создание пользовательских обобщенных коллекций
Установка ограничений для параметров типа с помощью where
Отсутствие поддержки ограничений при использовании операций
Создание обобщенных базовых классов
Создание обобщенных интерфейсов
Создание обобщенных делегатов
Имитация обобщенных делегатов в .NET 1.1
Несколько слов о вложенных делегатах
Резюме

Исходные коды примеров к главе

С появлением .NET 2.0 язык программирования C# стал поддерживать новую возможность CTS (Common Type System — общая система типов), названную обобщениями (generics). Упрощенно говоря, обобщения обеспечивают программисту возможность определения “заполнителей” (формально называемых параметрами типа) для аргументов методов и определений типов, которые будут конкретизированы во время вызова обобщенного метода или при создании обобщенного типа.

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

Снова о создании объектных образов, восстановлении значений и System.Object

Чтобы понять, в чем заключаются преимущества использования обобщений, следует выяснить, какие проблемы возникают у программиста без их использования. Вы должны помнить из главы 3, что платформа .NET поддерживает автоматическое преобразование объектов стека и динамической памяти с помощью операций создания объектного образа и восстановления из объектного образа. На первый взгляд, эта возможность кажется не слишком полезной и имеющей, скорее, теоретический, чем практический интерес. Но на самом деле возможность создания объектного образа и восстановления из объектного образа очень полезна тем, что она позволяет интерпретировать все, как System.Object, оставив все заботы о распределении памяти на усмотрение CLR.

Чтобы рассмотреть особенности процесса создания объектного образа, предположим, что мы создали System.Collections.ArrayList для хранения числовых (т.е. размещаемых в стеке) данных. Напомним, что все члены ArrayList обладают прототипами для получения и возвращения типов System.Object. Но вместо того, чтобы заставлять программиста вручную вкладывать размещенное в стеке целое число в соответствующую объектную оболочку, среда выполнения делает это автоматически с помощью операции создания объектного образа.

static void Main(string[] args)
{
    // При передаче данных члену, требующему объект, для 
    // характеризуемых значениями типов автоматически создается 
    // объектный образ. 
    ArrayList myInts = new ArrayList(); 
    myInts.Add(10); 
    Console.ReadLine(); 
}

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

static void Main(string[] args) 
{
...
    // Значение восстанавливается... и снова становится объектом! 
    Console.WriteLine("Значение вашего int: {0}", 
        (int)myInts[0]); 
    Console.ReadLine(); 
}

Для представления операции создания объектного образа в терминах CIL компилятор C# использует блок box. Точно так же операция восстановления из объектного образа преобразуется в CIL-блок unbox. Вот соответствующий CIL-код для показанного выше метода Main() (этот код можно увидеть с помощью ildasm.exe).

.method private hidebysig static void Main(string[] args) cil managed 
{
...
    box  [mscorlib]System.Int32
    callvirt  instance int32 [mscorlib]
        System.Collections.ArrayList::Add(object)
    pop
    ldstr  "Значение вашего int: {0}"
    ldloc.0
    ldc.i4.0
    callvirt  instance object [mscorlib]
        System.Collections.ArrayList::get_Item(int32) 
    unbox  [mscorlib]System.Int32 
    ldind.i4
    box  [mscorlib]System.Int32 
    call void [mscorlib]System.Console::WriteLine(string, object)
...
}

Обратите внимание на то, что перед обращением к ArrayList.Add() размещенное в стеке значение System.Int32 преобразуется в объект, чтобы передать требуемый System.Object. Также заметьте, что при чтении из ArrayList с помощью индексатора типа (что отображается в скрытый метод get_Item()) объект System.Object восстанавливается в System.Int32 только для того, чтобы снова стать объектным образом при передаче методу Console.WriteLine().

Проблемы создания объектных образов и восстановления значений

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

  1. Новый объект нужно разместить в управляемой динамической памяти.
  2. Значение размещенных в стеке данных нужно записать в соответствующее место в памяти.
  3. При восстановлении значения, сохраненного в объекте, размещенном в динамической памяти, это значение нужно снова вернуть в стек.
  4. Неиспользуемый объект в управляемой динамической памяти (в конце концов) должен быть уничтожен сборщиком мусора.

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

Теперь рассмотрим проблему отсутствия типовой безопасности в отношении операции восстановления значений из объектного образа. Вы знаете, что для восстановления значения в рамках синтаксиса C# используется оператор преобразования. Но каким будет это преобразование — успешным или неудачным, — выяснится только в среде выполнения. При попытке восстановить значение в неправильный тип данных вы получите InvalidCastException.

static void Main(string[] args) 
{
...
    // Ой!  Исключение времени выполнения!
    Console.WriteLine("Значение вашего int: {0}", 
        (short)myInts[0]); 
Console.ReadLine(); 
}

В идеальной ситуации компилятор C# должен решать проблемы некорректных операций восстановления из объектного образа во время компиляции, а не в среде выполнения. В связи с этим, в действительно идеальной ситуации, можно было бы сохранить типы, характеризуемые значениями, в контейнере, который не требовал бы преобразования в объект. В .NET 2.0 обобщения дают решение именно этих проблем. Однако перед тем как углубиться в детали использования обобщений, давайте посмотрим, как программисты пытались бороться с этими проблемами в .NET 1.x. с помощью строго типизованных коллекций.

Типовая безопасность и строго типизованные коллекции

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

public class Person
{
    // Определены открытыми для простоты. 
    public int currAge;
    public string fName, lName;

    public Person(){}
    public Person(string firstName, string lastName, int age)
    {
        currAge = age;
        fName = firstName;
        lName = lastName; 
    }

    public override string ToString()
    {
        return string.Format("Возраст {0}, {1} равен {2}", 
            lName, fName, currAge); 
    }
}

Чтобы построить коллекцию персон, можно определить член-переменную System.Collections.ArrayList в рамках класса PeopleCollection и настроить все члены на работу со строго типизованными объектами Person, а не с общими объектами System.Object.

public class PeopleCollection : IEnumerable 
{
    private ArrayList arPeople = new ArrayList();
    public PeopleCollection(){}

    // Преобразование для вызывающей стороны. 
    public Person GetPerson(int pos) 
    { return (Person)arPeople[pos]; }

    // Вставка только типов Person. 
    public void AddPerson(Person p) 
    { arPeople.Add(p); }

    public void ClearPeople() 
    { arPeople.Clear(); }

    public int Count
    { get { return arPeople.Count; } }

    // Поддержка foreach нумератора. 
    IEnumerator IEnumerable.GetEnumerator() 
    { return arPeople.GetEnumerator(); } 
}

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

static void Main(string[] args)
{
    Console.WriteLine("***** Custom Person Collection *****\n"); 
    PeopleCollection my People = new PeopleCollection(); 
    myPeople.AddPerson(new Person("Homer", "Simpson", 40)); 
    myPeople.AddPerson(new Person("Marge", "Simpson", 38)); 
    myPeople.AddPerson(new Person("Lisa", "Simpson", 9)); 
    myPeople.AddPerson(new Person("Bart", "Simpson", 7)); 
    myPeople.AddPerson(new Person("Maggie", "Simpson", 2));

    // Это приведет к ошибке компиляции!
    myPeople.AddPerson(new Car());

    foreach (Person p in myPeople)
        Console.WriteLine(p); 
    Console.ReadLine(); 
}

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

public class CarCollection : IEnumerable 
{
    private ArrayList arCars = new ArrayList();
    public CarCollection(){}

    // Преобразование для вызывающей стороны. 
    public Car GetCar(int pos) 
    { return (Car) arCars[pos]; }

    // Вставка только типов Car. 
    public void AddCar(Car c) 
    { arCars.Add(c); }

    public void ClearCars() 
    { arCars.Clear(); }

    public int Count
    { get { return arCars.Count; } }

    // Поддержка foreach нумератора. 
    IEnumerator IEnumerable.GetEnumerator() 
    { return arCars.GetEnumerator(); }
}

Вы, наверное, знаете из своего собственного опыта, что процесс создания множества строго типизованных коллекций для учета различных типов является не только трудоемким, но просто кошмарным для последующего обслуживания. Обобщенные коллекции позволяют отложить указание спецификации содержащегося типа до времени создания. Пока что не слишком беспокойтесь о синтаксических деталях. Рассмотрите следующий программный код, в котором используется обобщенный класс с именем System.Collections.Generic.List<> для создания двух контейнерных объектов, обеспечивающих типовую безопасность.

static void Main(string[] args)
{
    // Использование обобщенного типа List только для Person. 
    List<Person> morePeople = new List<Person>(); 
    morePeople.Add(new Person());

    // Использование обобщенного типа List только для Car. 
    List<Car> moreCars = new List<Car>();

    // Ошибка компиляции! 
    moreCars.Add(new Person()); 
}

Проблемы создания объектных образов и строго типизованные коллекции

Строго типизованные коллекции можно найти в библиотеках базовых классов .NET, и это очень полезные программные конструкции. Однако эти пользовательские контейнеры мало помогают в решении проблем создания объектных образов. Даже если вы создадите пользовательскую коллекцию с именем IntCollection, предназначенную для работы только с типами данных System.Int32, вам придется создать объект некоторого типа для хранения самих данных (System.Array, System.Collections.ArrayList и т.п.).

public class IntCollection : IEnumerable
{
    private ArrayList arInts = new ArrayList(); 
    public IntCollection() { }

    // Восстановление значения для вызывающей стороны. 
    public int GetInt(int pos) { return (int)arInts[pos]; }

    // Операция создания объектного образа! 
    public void AddInt(int i) 
    { arInts.Add(i); }

    public void ClearInts() 
    { arInts.Clear(); }

    public int Count
    { get { return arInts.Count; } }

    IEnumerator IEnumerable.GetEnumerator() 
    { return arInts.GetEnumerator(); }
}

Вне зависимости от того, какой тип вы выберете для хранения целых чисел (System.Array, System.Collections.ArrayList и т.п.), вы не сможете избавиться от проблемы .NET 1.1, связанной с созданием объектных образов. Нетрудно догадаться, что здесь снова на помощь приходят обобщения. В следующем фрагменте программного кода тип System.Collections.Generic.List<> используется для создания контейнера целых чисел, не имеющего проблем создания объектных образов и восстановления значений при вставке и получении типов характеризуемых значений.

static void Main(string[] args)
{
    // Без создания объектного образа! 
    List<int> myInts = new List<int>(); 
    myInts.Add(5);

    // Без восстановления значения! 
    int i = myInts[0]; 
}

Просто в качестве подтверждения рассмотрите следующий CIL-код для этого метода Main() (обратите внимание на отсутствие в нем каких бы то ни было блоков box и unbox).

.method private hidebysig static void  Main(string[] args) cil managed
{
  .entrypoint
  // Code size       24 (0x18)
  .maxstack  2
  .locals init ([0] class 
   [mscorlib]System.Collections.Generic.List`1<int32> myInts,
           [1] int32 i)
  IL_0000:  nop
  IL_0001:  newobj     instance void class 
   [mscorlib]System.Collections.Generic.List`1<int32>::.ctor()
  IL_0006:  stloc.0
  IL_0007:  ldloc.0
  IL_0008:  ldc.i4.5
  IL_0009:  callvirt   instance void class 
   [mscorlib]System.Collections.Generic.List`1<int32>::Add(!0)
  IL_000e:  nop
  IL_000f:  ldloc.0
  IL_0010:  ldc.i4.0
  IL_0011:  callvirt   instance !0 class 
   [mscorlib]System.Collections.Generic.List`1<int32>::get_Item(int32)
  IL_0016:  stloc.1
  IL_0017:  ret
} // end of method Program::Main

Теперь, когда вы имеете лучшее представление о роли обобщений в .NET 2.0, мы с вами готовы углубиться в детали. Для начала мы формально рассмотрим пространство имен System.Collections.Generic.

Пространство имен System.Collections.Generic

Обобщенные типы присутствуют во многих библиотеках базовых классов .NET 2.0, но пространство имен System.Collections.Generic буквально наполнено ими (что вполне соответствует его названию). Подобно своему “родственнику” без обобщений (System.Collections), пространство имен System.Collections.Generic содержит множество типов класса и интерфейса, что позволяет вкладывать элементы в самые разные контейнеры. Совсем не удивительно, что обобщенные интерфейсы имитируют соответствующие необобщенные типы из пространства имен System.Collections.

ПРИМЕЧАНИЕ

По соглашению для обобщенных типов их замещаемые параметры обозначаются буквами верхнего регистра. И хотя здесь допустимо использовать любые буквы (или слова), обычно используют T для обозначения типов, K — для ключей, а V — для значений.

В пространстве имен System.Collections.Generic определяется и ряд классов, реализующих многие из этих ключевых интерфейсов. В табл. 10.1 представлены описания базовых типов класса из этого пространства имен, реализуемые ими интерфейсы и соответствующие типы из пространства имен System.Collections.

Обобщенный классНеобобщенный аналог в System.CollectionsОписание
Collection<T>CollectionBaseБаза для обобщенной коллекции
Comparer<T>ComparerВыполняет сравнение двух обобщенных объектов
Dictionary<K, V>HashtableОбобщенная коллекция пар имен и значений
List<T>ArrayListСписок элементов с динамически изменяемыми размерами
Queue<T>QueueОбобщенная реализация списка FIFO (дисциплина обслуживания типа “очередь”)
SortedDictionary<K, V>SortedListОбобщенная реализация сортированного набора пар имен и значений
Stack<T>StackОбобщенная реализация списка LIFO (дисциплина обслуживания типа “стек”)
LinkedList<T>Обобщенная реализация двусвязного списка
ReadOnlyCollection<T>ReadOnlyCollectionBaseОбобщенная реализация набора элементов только для чтения
Таблица 10.1. Классы System.Collections.Generic

В пространстве имен System.Collections.Generic также определяется целый ряд “вспомогательных” классов и структур для работы с конкретными контейнерами. Например, тип LinkedListNode<T> представляет узел в обобщенном LinkedList<T>, исключение KeyNotFoundException возникает при попытке доступа к элементу контейнера с несуществующим ключом и т.д.

Как видно из табл. 10.1, многие обобщенные классы коллекции имеют необобщенные аналоги в пространстве имен System.Collections (иногда даже с одинаковыми именами). В главе 7 было показано, как работать с такими необобщенными типами, поэтому дальше не предполагается рассматривать все их обобщенные “дубликаты”. Мы рассмотрим только List<T>, чтобы проиллюстрировать приемы использования обобщений. Если вам нужны подробности о других элементах пространства имен System.Collections.Generic, обратитесь к документации .NET Framework 2.0.

Тип List<T>

Подобно необобщенным классам, обобщенные классы являются объектами, размещаемыми в динамической памяти, поэтому для них следует использовать new со всеми необходимыми аргументами конструктора. Кроме того, вы должны указать типы, замещающие параметры, определенные обобщенным типом. Так, для System.Collections.Generic.List<T> требуется указать одно значение, задающее вид элемента, с которым будет функционировать List<T>. Например, чтобы создать три объекта List<> для хранения целых чисел, объектов SportsCar и объектов Person, вы должны записать следующее.

static void Main(string[] args)
{
    // Создается List для хранения целых чисел. 
    List<int> myInts = new List<int>();

    // Создается List для хранения объектов SportsCar. 
    List<SportsCar> myCars = new List<SportsCar>();

    // Создается List для хранения объектов Person. 
    List<Person> myPeople = new List<Person>(); 
}

В этот момент вы можете поинтересоваться, что же на самом деле становится значением заполнителя. Открыв окно определения программного кода в Visual Studio 2005 (см. главу 2), вы увидите, что везде в определении типа List<T> используется заполнитель T. Ниже показана часть соответствующего листинга (обратите внимание на элементы, выделенные полужирным шрифтом).

// Часть листинга для типа List<T>. 
namespace System.Collections.Generic
{
    public class List<T> : 
     IList<T>, ICollection<T>, IEnumerable<T>, 
     IList, ICollection, IEnumerable
    {
    ...
        public void Add(T item);
        public IList<T> AsReadOnly();
        public int BinarySearch(T item);
        public bool Contains(T item);
        public void CopyTo(T[] array);
        public int FindIndex(System.Predicate<T> match);
        public T FindLast(System.Predicate<T> match);
        public bool Remove(T item);
        public int RemoveAll(System.Predicate<T> match);
        public T[] ToArray();
        public bool TrueForAll(System.Predicate<T> match);
        public T this[int index] { get; set; }
    ...
    }
}

Когда вы создаете тип List<T> и указываете для него SportsCar, это эквивалентно следующему определению типа List<T>.

namespace System.Collections.Generic
{
    public class List<SportsCar> : 
     IList<SportsCar>, ICollection<SportsCar>, IEnumerable<SportsCar>, 
     IList, ICollection, IEnumerable
    {
    ...
        public void Add(SportsCar item);
        public IList<SportsCar> AsReadOnly();
        public int BinarySearch(SportsCar item);
        public bool Contains(SportsCar item);
        public void CopyTo(SportsCar[] array);
        public int FindIndex(System.Predicate<SportsCar> match);
        public SportsCar FindLast(System.Predicate<SportsCar> match);
        public bool Remove(SportsCar item);
        public int RemoveAll(System.Predicate<SportsCar> match); 
        public SportsCar [] ToArray();
        public bool TrueForAll(System.Predicate<SportsCar> match); 
        public SportsCar this[int index] { get; set; } 
    ...
    }
}

Конечно, когда вы создаете обобщенный List<T>, нельзя сказать, что компилятор буквально создает совершенно новую реализацию типа List<T>. Он обращается только к тем членам обобщенного типа, которые вы вызываете фактически. Чтобы пояснить это, предположим, что вы используете List<T> для объектов SportsCar так.

static void Main(string[] args)
{
    // Проверка List, содержащего объекты SportsCars. 
    List<SportsCar> myCars = new List<SportsCar>(); 
    myCars.Add(new SportsCar());
    Console.WriteLine("Your List contains {0}.", 
        myCars.Count); 
}

Если с помощью ildasm.exe проверить генерируемый CIL-код, обнаружатся следующие подстановки.

.method private hidebysig static void Main(string[] args) cil managed
{
    .entrypoint
    .maxstack  2
    .locals init ([0] class [mscorlib]
        System.Collections.Generic.'List`1'
        <class SportsCar> myCars) 
    newobj instance void class [mscorlib]
        System.Collections.Generic.'List`1'
        <class SportsCar>::.ctor() 
    stloc.0 ldloc.0
    newobj instance void CollectionGenerics.SportsCar::.ctor() 
    callvirt instance void class [mscorlib]
        System.Collections.Generic.'List`1'
        <class SportsCar>::Add(!0) 
    nop
    ldstr "Your List contains {0} item(s)." 
    ldloc.0 callvirt instance int32 class 
        [mscorlib]System.Collections.Generic.'List`1'
        <class SportsCar>::get_Count() 
    box [mscorlib]System.Int32
    call void [mscorlib]System.Console::WriteLine(string, object) 
    nop 
    ret 
}

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

Создание обобщенных методов

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

// Этот метод переставляет любые два элемента, 
// определенные параметром типа <T>. 
static void Swap<T>(ref T a, ref T b) 
{
    Console.WriteLine("Методу Swap() передано {0}", 
        typeof(T));
    T temp;
    temp = a;
    a = b;
    b = temp; 
}

Обратите внимание на то, что обобщенный метод определяется с помощью указания параметра типа, размещаемого после имени метода, но перед списком параметров. Здесь вы заявляете, что метод Swap() может работать с любыми двумя параметрами типа <T>. Просто для информации вы выводите имя типа соответствующего заменителя на консоль с помощью оператора C# typeof(). Теперь рассмотрите следующий метод Main(), в котором происходит обмен между целочисленными и строковыми типами.

static void Main(string[] args)
{
    Console.WriteLine("***** Забавы с обобщениями *****\n");

    // Обмен между двумя целыми. 
    int a = 10, b = 90;
    Console.WriteLine("До обмена: {0}, {1}", a, b);
    Swap<int>(ref a, ref b);
    Console.WriteLine("После обмена: {0}, {1}", a, b);
    Console.WriteLine();

    // Обмен между двумя строками. 
    string s1 = "Hello", s2 = "There";
    Console.WriteLine("До обмена: {0} {1}!", s1, s2);
    Swap<string>(ref s1, ref s2);
    Console.WriteLine("После обмена: {0} {1}!", s1, s2);
    Console.ReadLine();
}

Пропуск параметров типа

При вызове обобщенных методов, подобных Swap<T>, у вас есть возможность не указывать параметр типа, но только в том случае, когда обобщенный метод требует указания аргументов, поскольку тогда компилятор может “выяснить” тип этих аргументов на основе вводимых параметров. Например, можно переставить два типа System.Boolean так.

// Компилятор будет предполагать System.Boolean. 
bool b1 = true, b2 = false; 
Console.WriteLine("До обмена: {0}, {1}", b1, b2); 
Swap(ref b1, ref b2); 
Console.WriteLine("После обмена: {0}, {1}", b1, b2);

Но если, например, у вас есть обобщенный метод с именем DisplayBaseClass<T>, не имеющий входных параметров, как показано ниже:

static void DisplayBaseClass<T>()
{
    Console.WriteLine("Базовым классом {0} является: {1}.", 
        typeof(T), typeof(T).BaseType); 
}

то при вызове этого метода вы должны указать параметр типа.

static void Main(string[] args)
{
...
    // Если метод не имеет параметров, 
    // необходимо указать параметр типа. 
    DisplayBaseClass<int>(); 
    DisplayBaseClass<string>();

    // Ошибка компиляции! 
    // Нет параметров?  Тогда должен быть заполнитель! 
    DisplayBaseClass(); 
...
}


Рис. 10.1. Обобщенные методы в действии

В данном случае обобщенные методы Swap<T> и DisplayBaseClass<T> были определены в рамках объекта приложения (т.е. в рамках типа, определяющего метод Main()). Если вы предпочтете определить эти члены в новом типе класса (MyHelperClass), то должны записать следующее.

public class MyHelperClass
{
    public static void Swap<T>(ref T a, ref T b)
    {
        Console.WriteLine("Методу Swap() передано {0}", 
            typeof(T)); 
        T temp; 
        temp = a; 
        a = b; 
        b = temp;
    }

    public static void DisplayBaseClass<T>()
    {
        Console.WriteLine("Базовым классом {0} является: {1}.", 
            typeof(T), typeof(T).BaseType);
    }
}

Обратите внимание на то, что тип MyHelperClass сам по себе не является обобщенным, но определяет два обобщенных метода. Так или иначе, теперь, когда методы Swap<T> и DisplayBaseClass<T> находятся в контексте нового типа класса, при вызове их членов придется указать имя типа.

MyHelperClass.Swap<int>(ref a, ref b);

Наконец, обобщенные методы не обязаны быть статическими. Если бы Swap<T> и DisplayBaseClass<T> были методами уровня экземпляра, нужно было бы просто создать экземпляр MyHelperClass и вызвать их из объектной переменной.

MyHelperClass c = new MyHelperClass(); 
c.Swap<int>(ref a, ref b);

Создание обобщенных структур (и классов)

Теперь, когда вы понимаете, как определять и вызывать обобщенные методы, давайте рассмотрим построение обобщенных структур (процедура построения обобщенных классов оказывается аналогичной). Предположим, что мы построили гибкую структуру Point, поддерживающую один параметр типа, который представляет единицу хранения координат (x, y). Вызывающая сторона может создавать типы Point<T> так.

// Point с использованием int. 
Point<int> p = new Point<int>(10, 10);

// Point с использованием double. 
Point<double> p2 = new Point<double>(5.4, 3.3);

Вот полное определение Point<T>, необходимое нам для дальнейшего анализа.

// Обобщенная структура Point.
public struct Point<T>
{
    // Обобщенные данные. 
    private T xPos; private T yPos;

    // Обобщенный конструктор. 
    public Point(T xVal, T yVal) 
    {
        xPos = xVal;
        yPos = yVal; 
    }

    // Обобщенные свойства. 
    public T X 
    {
        get { return xPos; }
        set { xPos = value; } 
    }

    public T Y
    {
        get { return yPos; } 
        set { yPos = value; }
    }

    public override string ToString()
    {
        return string.Format("[{0}, {1}]", xPos, yPos);
    }

    // Переустановка полей со значениями параметра типа, 
    // принятыми по умолчанию. 
    public void ResetPoint()
    {
        xPos = default(T); 
        yPos = default(T);
    }
}

Ключевое слово default в обобщенном программном коде

Как видите, Point<T> использует параметр типа в определении полей данных, аргументов конструктора и в определениях свойств. Обратите внимание на то, что вдобавок к переопределению ToString() обобщенный тип Point<T> определяет метод ResetPoint(), в котором используется новый синтаксис.

// Ключевое слово 'default' в C# 2005 является перегруженным. 
// При использовании с обобщениями оно представляет значение 
// параметра типа, принимаемое по умолчанию.
public void ResetPoint()
{
    xPos = default(T); 
    yPos = default(T); 
}

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

Для Point<T> вы можете непосредственно установить xPos и yPos равными 0, поскольку вполне безопасно предполагать, что вызывающая сторона будет поставлять только числовые данные. Однако с помощью синтаксиса default(T) вы можете сделать обобщенный тип более гибким. В любом случае вы теперь можете использовать методы Point<T> так.

static void Main(string[] args)
{
    Console.WriteLine("***** Забавы с обобщениями *****\n");

    // Point с использованием int. 
    Point<int> p = new Point<int>(10, 10);
    Console.WriteLine("p.ToString()={0}", p.ToString());
    p.ResetPoint();
    Console.WriteLine("p.ToString()={0}", p.ToString());
    Console.WriteLine();

    // Point с использованием double. 
    Point<double> p2 = new Point<double>(5.4, 3.3); 
    Console.WriteLine("p2.ToString()={0}", p2.ToString()); 
    p2.ResetPoint(); 
    Console.WriteLine("p2.ToString()={0}", p2.ToString()); 
    Console.WriteLine();

    // Обмен двух Point. 
    Point<int> pointA = new Point<int>(50, 40);
    Point<int> pointB = new Point<int>(543, 1);
    Console.WriteLine( "До обмена: {0}, {1}", pointA, pointB); 
    Swap<Point<int>>(ref pointA, ref pointB); 
    Console.WriteLine("После обмена: {0}, {1}", pointA, pointB); 
    Console.ReadLine();
}

Соответствующий вывод показан на рис. 10.2.


Рис. 10.2. Использование обобщенного типа Point

Создание пользовательских обобщенных коллекций

Итак, пространство имен System.Collections.Generic предлагает множество типов, позволяющих создавать эффективные контейнеры, удовлетворяющие требованиям типовой безопасности. С учетом множества доступных вариантов очень велика вероятность того, что в .NET 2.0 у вас вообще не возникнет необходимости в построении пользовательских типов коллекции. Тем не менее, чтобы показать, как строится обобщенный контейнер, нашей следующей задачей будет создание обобщенного класса коллекции, который мы назовем CarCollection<T>.

Подобно созданному выше необобщенному типу CarCollection, наш новый вариант будет использовать уже существующий тип коллекции для хранения своих элементов (в данном случае это List<>). Будет реализована и поддержка цикла foreach путем реализации обобщенного интерфейса IEnumerable<>. Обратите внимание на то, что IEnumerable<> расширяет необобщенный интерфейс IEnumerable, поэтому компилятор ожидает, что вы реализуете две версии метода GetEnumerator(). Вот как может выглядеть соответствующая модификация.

public class CarCollection<T> : IEnumerable<T>
{
    private List<T> arCars = new List<T>();

    public T GetCar(int pos) 
    { return arCars[pos]; }

    public void AddCar(T c) 
    { arCars.Add(c); }

    public void ClearCars() 
    { arCars.Clear(); }

    public int Count
    { get { return arCars.Count; } }

    // IEnumerable<T> расширяет IEnumerable, поэтому 
    // нужно реализовать обе версии GetEnumerator(). 
    IEnumerator<T> IEnumerable<T>.GetEnumerator() 
    { return arCars.GetEnumerator(); } 
    IEnumerator IEnumerable.GetEnumerator() 
    { return arCars.GetEnumerator(); } 
}

Этот обновленный тип CarCollection<T> можно использовать так.

static void Main(string[] args)
{
    Console.WriteLine("* Пользовательская обобщенная коллекция *\n");

    // Создание коллекции объектов Car. 
    CarCollection<Car> myCars = new CarCollection<Car>(); 
    myCars.AddCar(new Car("Rusty", 20)); 
    myCars.AddCar(new Car("Zippy", 90)); 

    foreach (Car c in myCars) 
    {
        Console.WriteLine("PetName: {0}, Speed: {1}", 
            c.PetName, c.Speed);
    }
    Console.ReadLine(); 
}

Здесь создается тип CarCollection<T>, который должен содержать только типы Car. Снова заметим, что того же результата можно достичь и с помощью непосредственного использования типа List<T>. Главным преимуществом данного подхода является то, что теперь вы можете добавлять в CarCollection уникальные методы, делегирующие запросы к внутреннему типу List<T>.

Установка ограничений для параметров типа с помощью where

В настоящий момент класс CarCollection<T> привлекает нас только открытыми методами с уникальными именами. Кроме того, пользователь объекта может создать экземпляр CarCollection<T> и указать практически любой параметр типа.

// Это синтаксически корректно, но выглядит, 
// по крайней мере, странно... 
CarCollection<int> myInts = new CarCollection<int>();
myInts.AddCar(5);
myInts.AddCar(11);

Чтобы проиллюстрировать другую форму типичного непредусмотренного использования объекта, предположим, что вы создали два новых класса — SportsCar (спортивная машина) и MiniVan (минивэн), — которые являются производными от Car.

public class SportsCar : Car
{
    public SportsCar(string p, int s) 
        : base(p, s){} 
    // Дополнительные методы для SportsCar. 
}

public class MiniVan  : Car 
{
    public MiniVan(string p, int s) 
        : base(p, s){}
    // Дополнительные методы для MiniVan. 
}

В соответствии с законами наследования, в коллекцию CarCollection<T>, созданную с параметром типа Car, можно добавлять и типы MiniVan и SportsCar.

// CarCollection<Car> может хранить любой тип, производный от Car. 
CarCollection<Car> myCars = new CarCollection<Car>(); 
myInts.AddCar(new MiniVan("Family Truckster", 55)); 
myInts.AddCar(new SportsCar("Crusher", 40)); 

Это синтаксически корректно, но что делать, если вдруг понадобится добавить в CarCollection<T> новый открытый метод, например, с именем PrintPetName()? Такая задача кажется простой — достаточно получить доступ к подходящему элементу из List<T> и вызвать свойство PetName.

// Ошибка! 
// System.Объект не имеет свойства с именем PetName. 
public void PrintPetName(int pos)
{
    Console.WriteLine(arCars[pos].PetName); 
}

Однако в таком виде программный код скомпилирован не будет, поскольку истинная суть <T> еще не известна, и вы не можете с уверенностью утверждать, что какой-то элемент типа List<T> будет иметь свойство PetName. Когда параметр типа не имеет никаких ограничений (как в данном случае), обобщенный тип называется свободным (unbound). По идее параметры свободного типа должны иметь только члены System.Object (которые, очевидно, не имеют свойства PetName).

Вы можете попытаться “обмануть” компилятор путем преобразования элемента, возвращенного из метода индексатора List<T>, в строго типизованный объект Car, чтобы затем вызвать PetName возвращенного объекта.

// Ошибка! 
// Нельзя превратить тип 'T' в 'Car'! 
public void PrintPetName(int pos)
{
    Console.WriteLine(((Car)arCars[pos]).PetName);
}

Но это тоже не компилируется, поскольку компилятор не знает значения параметра типа <T> и не может гарантировать, что преобразование будет законным.

Для решения именно таких проблем обобщения .NET могут опционально определяться с ограничениями, для чего используется ключевое слово where. В .NET 2.0 обобщения могут иметь ограничения, описанные в табл. 10.2.

Ограничение обобщенияОписание
where T : structПараметр типа <T> должен иметь в цепочке наследования System.ValueType
where T : classПараметр типа <T> не должен иметь в цепочке наследования System.ValueType (т.е. <T> должен быть ссылочным типом)
where T : new()Параметр типа <T> должен иметь конструктор, заданный по умолчанию. Это полезно тогда, когда обобщенный тип должен создать экземпляр параметра типа, а вы не имеете ясных предположений о формате пользовательских конструкторов. Заметьте, что это ограничение должно быть последним в списке ограничений, если у типа их несколько
where T : БазовыйКлассПараметр типа <T> должен быть производным класса, указанного параметром БазовыйКласс
where T : ИнтерфейсПараметр типа <T> должен реализовывать интерфейс, указанный параметром Интерфейс
Таблица 10.2. Возможные ограничения обобщений для параметров типа

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

// Вложенные элементы должны иметь конструктор, 
// заданный по умолчанию. 
public class MyGenericClass<T> where T : new()
{...}

// Вложенные элементы должны быть классами, реализующими IDrawable 
// и поддерживающими конструктор, заданный по умолчанию. 
public class MyGenericClass<T> where T : class, IDrawable, new()
{...}

// MyGenericClass получается из MyBase и реализует ISomeInterface, 
// а вложенные элементы должны быть структурами. 
public class MyGenericClass<T> : MyBase, ISomeInterface 
    where T : struct
{...}

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

// <K> должен иметь конструктор, заданный по умолчанию, 
// а <T> должен реализовывать открытый интерфейс IComparable. 
public class MyGenericClass<K, T> where K : new() 
    where T : IComparable<T>
{...}

Если вы хотите изменить тип CarCollection<T> так, чтобы в него можно было поместить только производные от Car, вы можете записать следующее.

public class CarCollection<T> : IEnumerable<T> where T : Car
{
...
    public void PrintPetName(int pos)
    {
        // Поскольку теперь все элементы должны быть из семейства Car, 
        // свойство PetName можно вызывать непосредственно. 
        Console.WriteLine(arCars[pos].PetName);
    }
}

При таких ограничениях на CarCollection<T> реализация PrintPetName() становится очень простой, поскольку теперь компилятор может предполагать, что <T> является производным от Car. Более того, если указанный пользователем параметр типа не совместим с Car, будет сгенерирована ошибка компиляции.

// Ошибка компиляции! 
CarCollection<int> myInts = new CarCollection<int>();

Вы должны понимать, что обобщенные методы тоже могут использовать ключевое слово where. Например, если нужно гарантировать, чтобы методу Swap(), созданному в этой главе выше, передавались только типы, производные от System.ValueType, измените свой программный код так.

// Этот метод переставит любые типы, характеризуемые значениями. 
static void Swap<T>(ref T a, ref T b) where T : struct 
{
...
}

Следует также понимать то, что при таком ограничении метод Swap() уже не сможет переставлять строковые типы (поскольку они являются ссылочными).

Отсутствие поддержки ограничений при использовании операций

При создании обобщенных методов для вас может оказаться сюрпризом появление ошибок компилятора, когда с параметрами типа используются операции C# (+, -, *, == и т.д.). Например, я уверен, вы сочли бы полезными классы Add(), Subtract(), Multiply() и Divide(), способные работать с обобщенными типами.

// Ошибка компиляции! 
// Нельзя применять операции к параметрам типа! 
public class BasicMath<T>
{
    public T Add(T arg1, T arg2)
    { return arg1 + arg2; }
    public T Subtract(T arg1, T arg2)
    { return arg1 - arg2; }
    public T Multiply(T arg1, T arg2)
    { return arg1 * arg2; }
    public T Divide(T arg1, T arg2)
    { return arg1 / arg2; } 
}

Как ни печально, этот класс BasicMath<T> не компилируется. Это может показаться большим ограничением, но не следует забывать, что обобщения являются обобщениями. Конечно, тип System.Int32 может прекрасно работать с бинарными операциями C#. Однако, если, например, <T> будет пользовательским классом или типом структуры, компилятор не сможет сделать никаких предположений о характере перегруженных операций +, -, * и /. В идеале C# должен был бы позволять обобщенному типу ограничения с использованием операций, например, так.

// Только для иллюстрации! 
// Этот программный код не является допустимым в C# 2.0. 
public class BasicMath<T> where T : operator +, operator -, 
    operator *, operator /
{
    public T Add(T arg1, T arg2)
    { return arg1 + arg2; }
    public T Subtract(T arg1, T arg2)
    { return arg1 - arg2; }
    public T Multiply(T arg1, T arg2)
    { return arg1 * arg2; }
    public T Divide(T arg1, T arg2)
    { return arg1 / arg2; }
}

Увы, ограничения обобщенных типов при использовании операций в C# 2005 не поддерживаются.

Создание обобщенных базовых классов

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

// Предположим, что создан пользовательский 
// обобщенный класс списка. 
public class MyList<T> 
{
    private List<T> listOfData = new List<T>();
}

// Конкретные типы должны указать параметр типа, 
// если они получаются из обобщенного 
// базового класса. 
public class MyStringList : MyList<string> 
{}

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

// Обобщенный класс с виртуальным методом. 
public class MyList<T>
{
    private List<T> listOfData = new List<T>();
    public virtual void PrintList(T data) { } 
}

public class MyStringList : MyList<string> 
{
    // В производных методах нужно заменить параметр типа, 
    // используемый в родительском классе. 
    public override void PrintList(string data) { } 
}

Если производный тип тоже является обобщенным, дочерний класс может (опционально) использовать заменитель типа в своем определении. Однако знайте, что любые ограничения, размещенные в базовом классе, должны “учитываться” и производным типом. Например:

// Обратите внимание, теперь здесь имеется ограничение, 
// требующее конструктор по умолчанию. 
public class MyList<T> where T : new()
{
    private List<T> listOfData = new List<T>();

    public virtual void PrintList(T data) { } 
}

// Производный тип должен учитывать ограничения базового. 
public class MyReadOnlyList<T> : MyList<T> where T : new()
{
    public override void PrintList(T data) { } 
}

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

Создание обобщенных интерфейсов

Вы уже видели при рассмотрении пространства имен System.Collections.Generic, что обобщенные интерфейсы в C# также допустимы (например, IEnumerable<T>). Вы, конечно, можете определить свои собственные обобщенные интерфейсы (как с ограничениями, так и без ограничений). Предположим, что нужно определить интерфейс, который сможет выполнять бинарные операции с параметрами обобщенного типа.

public interface IBinaryOperations<T>
{
    T Add(T arg1, T arg2);
    T Subtract(T arg1, T arg2);
    T Multiply(T arg1, T arg2);
    T Divide(T arg1, T arg2); 
}

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

public class BasicMath : IBinaryOperations<int>
{
    public int Add(int arg1, int arg2) 
    { return arg1 + arg2; }

    public int Subtract(int arg1, int arg2) 
    { return arg1 - arg2; }

    public int Multiply(int arg1, int arg2) 
    { return arg1 * arg2; }

    public int Divide(int arg1, int arg2) 
    { return arg1 / arg2; } 
}

После этого вы можете использовать BasicMath, как и ожидали.

static void Main(string[] args)
{
    Console.WriteLine("***** Обобщенные интерфейсы *****\n");
    BasicMath m = new BasicMath();
    Console.WriteLine("1 + 1 = {0}", m.Add(1, 1));
    Console.ReadLine(); 
}

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

public class BasicMath : IBinaryOperations<double>
{
    public double Add(double arg1, double arg2) 
    { return arg1 + arg2; }
...
}

Создание обобщенных делегатов

Наконец, что не менее важно, .NET 2.0 позволяет определять обобщенные типы делегата. Предположим, например, что требуется определить делегат, который сможет вызывать любой метод, возвращающий void и принимающий один аргумент. Если аргумент может меняться, это можно учесть с помощью параметра типа. Для примера рассмотрим следующий программный код (обратите внимание на то, что целевые объекты делегата регистрируются как с помощью “традиционного” синтаксиса делегата, так и с помощью группового преобразования метода).

namespace GenericDelegate
{
    // Этот обобщенный делегат может вызвать любой метод, 
    // возвращающий void и принимающий один параметр. 
    public delegate void MyGenericDelegate<T>(T arg); 

    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***** Обобщенные делегаты *****\n");

            // Регистрация цели с помощью 'традиционного' 
            // синтаксиса делегата. 
            MyGenericDelegate<string> strTarget = 
                new MyGenericDelegate<string>(StringTarget); 
            strTarget("Некоторые строковые данные");

            // Регистрация цели с помощью 
            // группового преобразования метода. 
            MyGenericDelegate<int> intTarget = IntTarget; 
            intTarget(9); 
            Console.ReadLine(); 
        }

        static void StringTarget(string arg)
        {
            Console.WriteLine("arg в верхнем регистре: {0}", 
                arg.ToUpper()); 
        }

        static void IntTarget(int arg) 
        {
            Console.WriteLine("++arg: {0}", ++arg);
        }
    }
}

Обратите внимание на то, что MyGenericDelegate<T> определяет один параметр типа, представляющий аргумент, отправляемый целевому объекту делегата. При создании экземпляра этого типа требуется конкретизировать значение параметра типа, а также имя метода, вызываемого делегатом. Так, если вы укажете строковый тип, то отправите целевому методу строковое значение.

// Создание экземпляра MyGenericDelegate<T> 
// со значением string для параметра типа. 
MyGenericDelegate<string> strTarget = 
    new MyGenericDelegate<string>(StringTarget); 
strTarget("Некоторые строковые данные");

С учетом формата объекта strTarget метод StringTarget() должен теперь получить в качестве параметра одну строку.

static void StringTarget(string arg)
{
    Console.WriteLine("arg в верхнем регистре: {0}", arg.ToUpper());
}

Имитация обобщенных делегатов в .NET 1.1

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

public delegate void MyDelegate(object arg);

Это позволяет посылать любой тип данных целевому объекту делегата, но при этом не гарантируется типовая безопасность, а кроме того, остаются нерешенными проблемы создания объектных образов. Предположим, например, что мы создали два экземпляра MyDelegate, которые указывают на один и тот же метод MyTarget. Обратите внимание на проблемы создания объектных образов и восстановления значений, а также на отсутствие гарантий типовой безопасности.

class Program
{
    static void Main(string[] args)
    {
    ...
        // Регистрация цели с помощью 
        // 'традиционного' синтаксиса делегата. 
        MyDelegate d = new MyDelegate(MyTarget); 
        d("Дополнительные строковые данные");

        // Регистрация цели с помощью 
        // группового преобразования метода. 
        MyDelegate d2 = MyTarget; 
        d2(9);    // Проблема объектного образа. 
    ...
    }

    // Ввиду отсутствия типовой безопасности мы должны 
    // определить соответствующий тип до преобразования. 
    static void MyTarget(object arg)
    {
        if(arg is int) 
        {
            int i = (int)arg; // Проблема восстановления значения. 
            Console.WriteLine("++arg: {0}", ++i);
        }
        if(arg is string)
        {
            string s = (string)arg;
            Console.WriteLine("arg в верхнем регистре: {0}", 
                s.ToUpper());
        }
    }
}

Когда вы посылаете целевому объекту тип, характеризуемый значением, это значение (конечно же) “упаковывается” в объект и “распаковывается” после его получения целевым методом. Точно также, поскольку поступающий параметр может быть всем чем угодно, вы должны динамически проверить соответствующий тип перед тем, как выполнить преобразование. С помощью обобщенных делегатов вы можете получить всю необходимую гибкость без “проблем”.

Несколько слов о вложенных делегатах

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

// Вложенные обобщенные делегаты могут иметь доступ к параметрам 
// обобщенного типа-контейнера. 
public class MyList<T>
{
    private List<T> listOfData = new List<T>();
    public delegate void ListDelegate(T arg); 
}

Резюме

Обобщения можно обоснованно считать главным из усовершенствований, предложенных в C# 2005. Как вы могли убедиться, обобщенный элемент позволяет указать “заполнители” (т.е. параметры типа), которые конкретизируются в момент создания типа (или вызова, в случае обобщенных методов). По сути, обобщения дают решение проблем объектных образов и типовой безопасности, усложняющих разработку программ в среде .NET 1.1.

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


Любой из материалов, опубликованных на этом сервере, не может быть воспроизведен в какой бы то ни было форме и какими бы то ни было средствами без письменного разрешения владельцев авторских прав.
    Сообщений 5    Оценка 15 [+2/-0]         Оценить