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

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

Родовое порождение класса. Предложение using

До сих пор рассматривалась ситуация родового порождения экземпляров универсального класса. Фактические типы задавались в момент создания экземпляра. Это наглядно показывает преимущества применяемой технологии, поскольку очевидно, что не создается дублирующий код для каждого класса, порожденного универсальным классом. И все-таки остается естественный вопрос: можно ли породить класс из универсального класса путем подстановки фактических параметров, а потом спокойно использовать этот класс обычным образом? Такая вещь возможна. Это можно сделать не совсем обычным путем - не в программном коде, а в предложении using, назначение которого и состоит в выполнении подобных подстановок.

Давайте вернемся к универсальному классу OneLinkStack<T>, введенному в начале этой лекции, и породим на его основе вполне конкретный класс IntStack, заменив формальный параметр T фактическим - int. Для этого достаточно задать следующее предложение using:

using IntStack = Generic.OneLinkStack<int>;

Вот тест, в котором создаются несколько объектов этого класса:

public void TestIntStack()
{
	IntStack stack1 = new IntStack();
	IntStack stack2 = new IntStack();
	IntStack stack3 = new IntStack();
	stack1.put(11); stack1.put(22);
	int x1 = stack1.item(), x2 = stack1.item();
	if ((x1 == x2) && (x1 == 22)) Console.WriteLine("OK!");
	stack1.remove(); x2 = stack1.item();
	if ((x1 != x2) && (x2 == 11)) Console.WriteLine("OK!");
	stack1.remove(); x2 = (stack1.empty()) ? 77 : 
		stack1.item();
	if ((x1 != x2) && (x2 == 77)) Console.WriteLine("OK!");
	stack2.put(55); stack2.put(66);
	stack2.remove(); int s = stack2.item();
	if (!stack2.empty()) Console.WriteLine(s);
	stack3.put(333); stack3.put((int)Math.Sqrt(Math.PI));
	int res = stack3.item();
	stack3.remove(); res += stack3.item();
	Console.WriteLine("res= {0}", res);
}

Все работает заданным образом, можете поверить.

Универсальность и специальные случаи классов

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

Универсальные структуры

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

public struct Point<T>
{
	T x, y;//координаты точки, тип которых задан параметром
	// другие свойства и методы структуры
}

Универсальные интерфейсы

Интерфейсы чаще всего следует делать универсальными, предоставляя большую гибкость для позднейших этапов создания системы. Возможно, вы заметили применение в наших примерах универсальных интерфейсов библиотеки FCL - IComparable<T> и других. Введение универсальности, в первую очередь, сказалось на библиотеке FCL - внутренних классов, определяющих поведение системы. В частности, для большинства интерфейсов появились универсальные двойники с параметрами. Если бы в наших примерах мы использовали не универсальный интерфейс, а обычный, то потеряли бы в эффективности, поскольку сравнение объектов потребовало бы создание временных объектов типа object, выполнения операций boxing и unboxing.

Универсальные делегаты

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

class Delegate<T>
{
	public delegate T Del(T a, T b);
}

Как видите, тип аргументов и возвращаемого значения в сигнатуре функционального типа определяется классом Delegate.

Добавим в класс функцию высшего порядка FunAr, одним из аргументов которой будет функция типа Del, заданного делегатом. Эта функция будет применяться к элементам массива, передаваемого также функции FunAr. Приведу описание:

public T FunAr(T[] arr, T a0, Del f)
{
	T temp = a0;
	for(int i =0; i<arr.Length; i++)
	{
		temp = f(temp, arr[i]);
	}
	return (temp);
}

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

Рассмотрим теперь клиентский класс Testing, в котором определен набор функций:

public int max2(int a, int b)
		{ return (a > b) ? a : b; }
public double min2(double a, double b)
		{ return (a < b) ? a : b; }
public string sum2(string a, string b)
		{ return a + b; }
public float prod2(float a, float b)
		{ return a * b; }

Хотя все функции имеют разные типы, все они соответствуют определению класса Del - имеют два аргумента одного типа и возвращают результат того же типа. Посмотрим, как они применяются в тестирующем методе класса Testing:

public void TestFun()
{
	int[] ar1 = { 3, 5, 7, 9 };
	double[] ar2 = { 3.5, 5.7, 7.9 };
	string[] ar3 = { "Мама ", "мыла ", "Машу ", "мылом." };
	float[] ar4 = { 5f, 7f, 9f, 11f };
	Delegate<int> d1 = new Delegate<int>();
	Delegate<int>.Del del1;
	del1= this.max2;
	int max = d1.FunAr(ar1, ar1[0], del1);
	Console.WriteLine("max= {0}", max);
	Delegate<double> d2 = new Delegate<double>();
	Delegate<double>.Del del2;
	del2 = this.min2;
	double min = d2.FunAr(ar2, ar2[0], del2);
	Console.WriteLine("min= {0}", min);
	Delegate<string> d3 = new Delegate<string>();
	Delegate<string>.Del del3;
	del3 = this.sum2;
	string sum = d3.FunAr(ar3, "", del3);
	Console.WriteLine("concat= {0}", sum);
	Delegate<float> d4 = new Delegate<float>();
	Delegate<float>.Del del4;
	del4 = this.prod2;
	float prod = d4.FunAr(ar4, 1f, del4);
	Console.WriteLine("prod= {0}", prod);
}

Обратите внимание на объявление экземпляра делегата:

Delegate<int>.Del del1;

В момент объявления задается фактический тип, и сигнатура экземпляра становится конкретизированной. Теперь экземпляр можно создать и связать с конкретной функцией. В C# 2.0 это делается проще и естественнее, чем ранее, - непосредственным присваиванием:

del1= this.max2;

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

Покажем, что и сам функциональный тип-делегат можно объявлять с родовыми параметрами. Вот пример такого объявления:

public delegate T FunTwoArg<T>(T a, T b);

Добавим в наш тестовый пример код, демонстрирующий работу с этим делегатом:

FunTwoArg<int> mydel;
			mydel = max2;
			max = mydel(17, 21);
			Console.WriteLine("max= {0}", max);

Вот как выглядят результаты работы тестового примера:

Результаты работы с универсальными делегатами

Рис. 22.7. Результаты работы с универсальными делегатами

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

public void delegate EventHandler<T> (object sender, T args)
					where T:EventArgs

Этот делегат может применяться и для событий с собственными аргументами, поскольку вместо параметра T может быть подставлен конкретный тип - потомок класса EventArgs, дополненный нужными аргументами.

Framework .Net и универсальность

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

Решение этих задач потребовало введения универсальности не только в язык C#, но и поддержки на уровне каркаса Framework .Net и языка IL, включающем теперь параметризованные типы. Универсальный класс C# не является шаблоном, на основе которого строится конкретизированный класс, компилируемый далее в класс (тип) IL. Компилятору языка C# нет необходимости создавать классы для каждой конкретизации типов универсального класса. Вместо этого происходит компиляция универсального класса C# в параметризованный тип IL. Когда же CLR занимается исполнением управляемого кода, то вся необходимая информация о конкретных типах извлекается из метаданных, сопровождающих объекты.

При этом дублирования кода не происходит и на уровне JIT-компиляторов, которые, однажды сгенерировав код для конкретного типа, сохраняют ссылку на этот участок кода и передают ее, когда такой код понадобится вторично. Это справедливо как для ссылочных, так и значимых типов.

Естественно, что универсальность потребовала введения в библиотеку FCL соответствующих классов, интерфейсов, делегатов и методов классов, обладающих этим свойством.

Так, например, в класс System.Array добавлен ряд универсальных статических методов. Вот один из них:

public static int BinarySearch<T>(T[] array, T value);

В таблице 22.1 показаны некоторые универсальные классы и интерфейсы библиотеки FCL 2.0 из пространства имен System.Collections.Generic и их аналоги из пространства System.Collections.

Таблица 22.1. Соответствие между универсальными классами и их обычными двойниками
Универсальный класс Обычный класс Универсальный интерфейс Обычный интерфейс
Comparer<T> Comparer ICollection<T> ICollection
Dictionary<K,T> HashTable IComparable<T> IComparable
LinkedList<T> ---- IDictionary<K,T> IDictionary
List<T> ArrayList IEnumerable<T> IEnumerable
Queue<T> Queue IEnumerator<T> IEnumerator
SortedDictionary<K,T> SortedList IList<T> IList
Stack<T> Stack

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

Александр Галабудник
Александр Галабудник

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

Александра Гусева
Александра Гусева
Сергей Кузнецов
Сергей Кузнецов
Россия, Москва
Pavel Kuchugov
Pavel Kuchugov
Россия, Московский инженерно-физический институт, 2010