Опубликован: 02.12.2009 | Уровень: специалист | Доступ: платный | ВУЗ: Тверской государственный университет
Лекция 9:

Универсальность. Классы с родовыми параметрами

Аннотация: Универсальные классы, шаблоны, классы с родовыми параметрами – синонимичные понятия для класса, у которого есть параметры, задающие типы. Эти классы являются одним из мощнейших механизмов, позволяющих существенно сокращать объем кода объектно-ориентированных программных систем. Как ни парадоксально, но ограничение универсальности увеличивает свободу программиста. Лекция сопровождается задачами.
Ключевые слова: контроль типов, плата, универсальность, типизированный язык, формальный аргумент, фактический аргумент, присваивание, класс, обмен данными, параметр, универсальный класс, список, встроенные классы, библиотека FCL, делегаты, переменная, фактический тип, мощность, наследование, операция класса, операции, механизмы, сигнатура метода, конкретизация, абстрактный класс, инструментарий, значение, xml-отчет, стек, представление, представление реализации, эквивалентная операция, свопинг, операция присваивания, сложение, равенство, net, статический контроль типов, ограничение наследования, значимый тип, очередь, тип ограничения, типовые параметры, синтаксически корректный, IENumerable, односвязный список, формальная спецификация, сложение строк, встраивания, объект, потомок, родовое порождение экземпляров, формальный параметр, синтаксис объявления, универсальные интерфейсы, unboxing, функция высших порядков, универсальная функция, экземпляры делегата, универсальные делегаты, связывание, полиморфизм, компиляция, CLR, информация, ICollection

Проект к данной лекции Вы можете скачать здесь.

Наследование и универсальность

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

public void Swap(ref T x1, ref T x2)
  {
     T temp;
     temp = x1; x1 = x2; x2 = temp;
  }

Если тип T - это вполне определенный тип, например, int, string или Person, то никаких проблем не существует, все совершенно прозрачно. Но как быть, если возникает необходимость обмена данными и типа int, и типа string, и типа Person? Неужели нужно писать копии этой процедуры для каждого типа? Проблема легко решается в языках, где нет контроля типов, - там достаточно иметь единственный экземпляр такой процедуры, прекрасно работающий, но лишь до тех пор, пока передаются аргументы одного типа. Когда же процедуре будут переданы фактические аргументы разного типа, то немедленно возникнет ошибка периода выполнения, и это слишком дорогая плата за универсальность.

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

public void SwapObject( ref object item1, ref object item2)
    {
    object temp = item1;
    item1 = item2; item2 = temp;
    }

Эта процедура нормально компилируется, но, заметьте, ей нельзя передать аргументы, тип которых отличается от object. Дело в том, что формальному аргументу ref object соответствует только такой же тип фактического аргумента, поскольку для этого аргумента присваивание выполняется в обе стороны, а присваивание из типа object в конкретный тип T должно быть явным. Так что использовать этот метод клиент может, если только сам будет выполнять приведение типа.

В примерах, приводимых ниже, как обычно, построено решение с именем Ch8. В это решение вложен консольный проект ConsoleGeneric. В проект добавлен класс Generic, содержащий методы, подлежащие исследованию, в частности метод SwapObject. В проект также добавлен класс Testing, являющийся клиентом класса Generic:

class Testing
    {
        Generic gen = new Generic();
      //тестирующие методы
}

Рассмотрим пример тестирования работы метода SwapObject:

public void TestSwapObject()
 {
   int n1 = 7, n2 = 11;
   double x1 = 7.7, x2 = 11.1;
   Console.WriteLine("n1 = {0}, n2 = {1}", n1, n2);
   Console.WriteLine("x1 = {0}, x2 = {1}", x1, x2);
   Console.WriteLine("Попытка прямого обмена данными одного типа");
           // gen.SwapObject(ref n1, ref n2);
           // gen.SwapObject(ref x1, ref x2);
   Console.WriteLine("заканчивается ошибкой еще на этапе компиляции!");
   Console.WriteLine("Необходимо явное приведение к типу! ");
            object obj1, obj2;
            obj1 = n1; obj2 = n2;
            gen.SwapObject(ref obj1, ref obj2);
            n1 = (int)obj1; n2 = (int)obj2;
            Console.WriteLine("n1 = {0}, n2 = {1}", n1, n2);
            obj1 = x1; obj2 = x2;
            gen.SwapObject(ref obj1, ref obj2);
            x1 = (double)obj1; x2 = (double)obj2;
            Console.WriteLine("x1 = {0}, x2 = {1}", x1, x2);

Здесь клиент сам выполняет приведение данных к типу object и обратно, так что все работает нормально. Более того, можно, используя процедуру обмена, выполнить обмен данными разного типа после их приведения к универсальному типу object. Но, если меняются значениями переменные типа int и Person, то после обмена попытка привести тип Person к типу int приведет к ошибке периода выполнения.

Console.WriteLine("Обмен данными разного типа");
  obj1 = n1; obj2 = x1;
  gen.SwapObject(ref obj1, ref obj2);
  Console.WriteLine("Приводит к ошибке периода выполнения!");
  //n1 = (int)obj1; x1 = (double)obj2;

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

Для достижения универсальности процедуры Swap следует рассматривать тип T как ее параметр, такой же, как и сами аргументы x1 и x2. Суть универсальности в том, чтобы в момент вызова процедуры передавать ей не только фактические аргументы, но и их фактический тип. Вот как можно в C# объявить процедуру Swap с параметром, задающим тип аргументов:

public void Swap<T>(ref T item1, ref T item2)
 {
  T temp = item1;
  item1 = item2; item2 = temp;
 }

Вот как клиент может вызывать этот метод для данных разных типов:

public void TestSwapT()
   {
       int n1 = 7, n2 = 11;
       double x1 = 7.7, x2 = 11.1;
       Console.WriteLine("n1 = {0}, n2 = {1}", n1, n2);
       Console.WriteLine("x1 = {0}, x2 = {1}", x1, x2);
       Console.WriteLine("После обмена данными одного типа");
       gen.Swap<int>(ref n1, ref n2);
       gen.Swap<double>(ref x1, ref x2);
       Console.WriteLine("n1 = {0}, n2 = {1}", n1, n2);
       Console.WriteLine("x1 = {0}, x2 = {1}", x1, x2);

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

Console.WriteLine("Попытка обмена данными разного типа");
//gen.Swap<int>(ref n1, ref x1);
//gen.Swap<double>(ref x1, ref n1);
Console.WriteLine("заканчивается ошибкой еще на этапе компиляции!");

Ошибка, естественно, возникнет. Но! Это ошибка периода компиляции, а не выполнения, она уведомляет разработчика, что нельзя так просто смешивать "королей" и "капусту". Так что механизм работает должным образом. Рассмотрим, как универсальность распространяется на класс.

Под универсальностью (genericity) понимается способность класса объявлять используемые им типы как параметры. Класс с параметрами, задающими типы, называется универсальным классом (generic class). Терминология не устоялась, и синонимами термина "универсальный класс" являются термины: родовой класс, параметризованный класс, класс с родовыми параметрами. В языке С++ универсальные классы называются шаблонами (template).

Синтаксис универсального класса

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

class MyClass<T1, … Tn> {…}

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

В C# универсальными могут быть как классы, так и все их частные случаи - интерфейсы, структуры, делегаты, события.

Класс с универсальными методами

Специальным частным случаем универсального класса является класс, не объявляющий сам параметров, но разрешающий делать это своим методам. Давайте начнем рассмотрение универсальности с этого частного случая. Построенный нами класс Generic является примером такого класса:

class Generic
    {
        public void Swap<T>(ref T item1, ref T item2)
        {
            T temp = item1;
            item1 = item2; item2 = temp;
        }
        public void SwapObject( ref object item1, ref object item2)
        {
            object temp = item1;
            item1 = item2; item2 = temp;
        }
    }

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

Два основных механизма объектной технологии

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

Эти механизмы взаимно дополняют друг друга. Универсальность можно ограничить (об этом подробнее будет сказано ниже), указав, что тип, задаваемый родовым параметром, обязан быть наследником некоторого класса и/или ряда интерфейсов. С другой стороны, когда формальный тип T заменяется фактическим типом TFact, то там, где разрешено появляться объектам типа TFact, разрешены и объекты, принадлежащие классам-потомкам TFact.

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

Этапы процесса разработки программной системы

увеличить изображение
Рис. 8.1. Этапы процесса разработки программной системы

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

Для наполнения этой схемы реальным содержанием давайте рассмотрим некоторый пример с прохождением всех трех этапов.

Федор Антонов
Федор Антонов

Здравствуйте!

Записался на ваш курс, но не понимаю как произвести оплату.

Надо ли писать заявление и, если да, то куда отправлять?

как я получу диплом о профессиональной переподготовке?

Илья Ардов
Илья Ардов

Добрый день!

Я записан на программу. Куда высылать договор и диплом?

Анатолий Федоров
Анатолий Федоров
Россия, Москва, Московский государственный университет им. М. В. Ломоносова, 1989
Олег Волков
Олег Волков
Россия, Балаково, МБОУ СОШ 19