11 Индексатор (свойство с параметрами) public Boolean this[Int32 bltPos] {

11 Метод доступа get индексатора get {

// Сначала нужно проверить аргументы if ((bitPos < 0) || (bltPos >= m_numBits))

throw new ArgumentOutOfRangeException("bltPos");

// Вернуть состояние индексируемого бита

return (m_byteArray[bitPos / 8] & (1 << (bltPos % 8))) -=

}

// Метод доступа set индексатора set {

if ((bltPos < 0) || (bltPos >= m_numBits))
throw new ArgumentOutOfRangeException(

"bitPos", bltPos. ToStringQ);

If (value) {

// Установить индексируемый бит m_byteArray[bitPos / 8] = (Byte)

(m_byteArray[bitPos / 8] | (1 << (bltPos % 8)))j } else {

// Сбросить индексируемый бит m_byteArray[bitPos / 8] = (Byte)

(m_byteArray[bitPos / 8] & ~(1 << (bltPos % 8)));

}

}

}

}

Использовать индексатор типа BitArray невероятно просто:

// Выделить массив BitArray, который может хранить 14 бит BitArray ba = new BitArray(14);

// Установить все четные биты вызовом метода доступа set for (Int32 х = 0; х < 14; х++) { ba[x] = (х % 2 == 0);

}

// Вывести состояние всех битов вызовом метода доступа get for (Int32 х = 0; х < 14; х++) {

Console.WrlteLlne("Bit " + х + " Is " + (ba[x] ? "On" : "Off"));

}

В типе BitArray индексатор принимает один параметр bitPos типа Int32. У каждого индексатора должен быть хотя бы один параметр, но параметров может быть и больше. Тип параметров (как и тип возвращаемого значения) может быть любым. Пример индексатора с несколькими параметрами можно найти в классе System.Drawing. Imaging.ColorMatrix из сборки System.Drawing.dll.

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

Подобно методу доступа set свойства без параметров, метод доступа set индексатора содержит скрытый параметр (в C# его называют value), который указывает новое значение «индексируемого элемента».

CLR не различает свойства без параметров и с параметрами. Для среды любое свойство — это всего лишь пара методов, определенных внутри типа. Как уже отмечалось, в различных языках синтаксис создания и использования свойств с параметрами разный. Использование для индексатора в C# конструкции this [ ] — всего

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

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

□ метод get свойства с параметрами генерируется только в том случае, если у свойства определен метод доступа get;

□ метод set свойства с параметрами генерируется только в том случае, если у свойства определен метод доступа set;

□ определение свойства в метаданных управляемого модуля генерируется всегда; в метаданных нет отдельной таблицы для хранения определений свойств с параметрами: ведь для CLR свойства с параметрами — просто свойства.

Компиляция показанного ранее индексатора типа BitArray происходит так, как если бы он исходно был написан следующим образом:

public sealed class BitArray {

// Метод доступа get индексатора

public Boolean get_Item(Int32 bitPos) {/*...*/}

// Метод доступа set индексатора

public void set_Item(Int32 bitPos, Boolean value) { /* ... */ }

}

Компилятор автоматически генерирует имена этих методов, добавляя к имени индексатора префикс get_ или set_. Поскольку синтаксис индексаторов в C# не позволяет разработчику задавать имя индексатора, создателям компилятора C# пришлось самостоятельно выбрать имя для методов доступа, и они выбрали Item. Поэтому имена созданных компилятором методов — get_Item и set_Item.

Если в справочной документации .NET Framework указано, что тип поддерживает свойство Item, значит, данный тип поддерживает индексатор. Так, тип System. Collections .Generic. List предлагает открытое экземплярное свойство Item, которое является индексатором объекта List.

Программируя на С#, вы никогда не увидите имя Item, поэтому выбор его компилятором обычно не должен вызывать беспокойства. Однако если вы разрабатываете индексатор для типа, который будет использоваться в программах, написанных на других языках, возможно, придется изменить имена методов доступа индексатора (get и set). C# позволяет переименовать эти методы, применив к индексатору пользовательский атрибут System.Runtime.CompilerServices. IndexerNameAttribute. Пример:

using System;

using System.Runtime.CompilerServices;

public sealed class BitArray {

[IndexerName("Bit")]

public Boolean this[Int32 bitPos] {

// Здесь определен по крайней мере один метод доступа

}

}

Теперь компилятор сгенерирует вместо методов get_Item и set_Item методы get_Bit и set_Bit. Во время компиляции компилятор C# обнаруживает атрибут IndexerName, который сообщает ему, какие имена следует присвоить методам и метаданным свойств; сам по себе атрибут не включается в метаданные сборки[11].

Приведу фрагмент кода на языке Visual Basic, демонстрирующий обращение к индексатору, написанному на С#:

1 Создать экземпляр типа BitArray Dim ba as New BitArray(10)

' B Visual Basic индекс элемента массива задается в круглых скобках (),

' а не в квадратных [].

Console.WriteLine(ba(2)) " Выводит True или False

' Visual Basic также позволяет обращаться к индексатору по имени Console.WriteLine(ba.Bit(2)) " Выводит то же, что предыдущая строка

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

а значит, компилятор не будет знать, на какой индексатор ссылаются. При попытке компиляции следующего исходного текста на C# компилятор выдает сообщение об ошибке (ошибка CS0111: в классе SomeType уже определен член this с таким же типом параметра):

error CS0111: Class 'SomeType' already defines a member called 'this' with the same

parameter types

Фрагмент кода, приводящий к выдаче этого сообщения: using System;

using System.Runtime.CompilerServices;

public sealed class SomeType {

// Определяем метод доступа get_Item public Int32 this[Boolean b] { get { return 0; }

}

// Определяем метод доступа get_leff [IndexerName("leff")] public String this[Boolean b] { get { return null; }

}

}

Как видите, C# рассматривает индексаторы как механизм перегрузки оператора [ ], и этот оператор не позволяет различать свойства с одинаковыми наборами параметров и разными именами методов доступа.

Кстати, в качестве примера типа с измененным именем индексатора можно привести тип System. String, в котором индексатор String именуется Chars, а не Item. Это свойство позволяет получать отдельные символы из строки. Было решено, что для языков программирования, не использующих синтаксис с оператором [ ] для вызова этого свойства, имя Chars будет более информативно.

Обнаружив попытку чтения или записи индексатора, компилятор C# генерирует вызов соответствующего метода доступа. Некоторые языки программирования могут не поддерживать свойства с параметрами. Чтобы получить доступ к свойству с параметрами из программы на таком языке, нужно явно вызвать желаемый метод доступа. CLR не различает свойства с параметрами и без параметров, поэтому для поиска связи между свойством с параметрами и его методами доступа используется все тот же класс System. Reflection. Propertylnfо.

Выбор главного свойства с параметрами

При анализе ограничений, которые C# налагает на индексаторы, возникает два вопроса:

□ Что если язык, на котором написан тип, позволяет разработчику определить

несколько свойств с параметрами?

□ Как использовать этот тип в программе на С#?

Ответ: в этом типе надо выбрать один из методов среди свойств с параметрами и сделать его свойством по умолчанию, применив к самому классу экземпляр System.Reflection.DefaultMemberAttribute. Кстати, DefaultMemberAttribute можно применять к классам, структурам или интерфейсам. В C# при компиляции типа, определяющего свойства с параметрами, компилятор автоматически применяет к определяющему типу экземпляр атрибута DefaultMember и учитывает его при использовании атрибута IndexerName. Конструктор этого атрибута задает имя, которое будет назначено свойству с параметрами, выбранному как свойство по умолчанию для этого типа.

Итак, в C# при определении типа, у которого есть свойство с параметрами, но нет атрибута IndexerName, атрибут DefaultMember, задающий определяющий тип, будет указывать имя Item. Если применить к свойству с параметрами атрибут IndexerName, то атрибут DefaultMember определяющего типа будет указывать на строку, заданную атрибутом IndexerName. Помните: C# не будет компилировать код, содержащий разноименные свойства с параметрами.

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

Производительность при вызове методов доступа

В случае простых методов доступа get и set JIT-компилятор подставляет (inlines) код метода доступа внутрь кода вызываемого метода, поэтому характерного снижения производительности работы программы, проявляющегося при использовании свойств вместо полей, не наблюдается. Подстановка подразумевает компиляцию кода метода (или, в данном случае, метода доступа) непосредственно вместе с кодом вызывающего метода. Это избавляет от дополнительной нагрузки, связанной с вызовом во время выполнения, но за счет увеличения объема кода откомпилированного метода. Поскольку методы доступа свойств обычно содержат мало кода, их подстановка может приводить к сокращению общего объема машинного кода, а значит, к повышению скорости выполнения.

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

Доступность методов доступа свойств

Иногда при проектировании типа требуется назначить методам доступа get и set разный уровень доступа. Чаще всего применяют открытый метод доступа get и закрытый метод доступа set:

public class SomeType { private String mname; public String Name { get { return mname; } protected set {mname = value; }

>

>

Как видно из кода, свойство Name объявлено как public, а это означает, что метод доступа get будет открытым и доступным для вызова из любого кода. Однако следует учесть, что метод доступа set объявлен как protected, то есть он доступен для вызова только из кода SomeType или кода класса, производного от SomeType.

При определении для свойства методов доступа с различными уровнями доступа синтаксис C# требует, чтобы само свойство было объявлено с наименее строгим уровнем доступа, а более жесткое ограничение было наложено только на один из методов доступа. В этом примере свойство является открытым, а метод доступа set — защищенным (более ограниченным по сравнению с public).

Обобщенные методы доступа свойств

Поскольку свойства фактически представляют собой методы, a C# и CLR поддерживают параметризацию методов, некоторые разработчики пытаются определить свойства с собственными параметрами-типами (вместо использования таких параметров из внешнего типа). Однако C# не позволяет этого делать. Главная причина в том, что обобщения свойств лишены смысла с концептуальной точки зрения. Предполагается, что свойство представляет характеристику объекта, которую можно прочитать или задать. Введение обобщенного параметра типа означало бы, что поведение операции чтения/записи может меняться, но на концептуальном уровне от свойства не ожидается никакого поведения. Для задания какого-либо поведения объекта (обобщенного или нет) следует создать метод, а не свойство.

Глава 11. События

В этой главе рассматривается последняя разновидность членов, которые можно определить в типе, — события. Тип, в котором определено событие (или экземпляры этого типа), может уведомлять другие объекты о некоторых особых ситуациях, которые могут случиться. Например, если в классе Button (кнопка) определить событие Click (щелчок), то в приложение можно использовать объекты, которые будут получать уведомление о щелчке объекта Button, а получив такое уведомление исполнять некоторые действия. События — это члены типа, обеспечивающие такого рода взаимодействие. А именно определения события в типе означает, что тип поддерживает следующие возможности:

□ регистрация своей заинтересованности в событии;

□ отмена регистрации своей заинтересованности в событии;

□ оповещение зарегистрированных методов о произошедшем событии.

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

Модель событий CLR основана на делегатах (delegate). Делегаты обеспечивают реализацию механизма обратного вызова, безопасную по отношению к типам. Методы обратного вызова (callback methods) позволяют объекту получать уведомления, на которые он подписался. В этой главе мы будем постоянно пользоваться делегатами, но их детальный разбор отложим до главы 17.

Чтобы помочь вам в полной мере разобраться в работе событий в CLR, я начну с примера ситуации, в которой могут пригодиться события. Допустим, мы проектируем почтовое приложение. Получив сообщение по электронной почте, пользователь может захотеть переслать его на факс или пейджер. Допустим, вы начали проектирование приложения с разработки типа MailManager, получающего входящие сообщения. Тип MailManager будет поддерживать событие NewMail. Другие типы (например, Fax или Pager) могут регистрироваться для получения уведомлений об этом событии. Когда тип MailManager получит новое сообщение, он инициирует событие, в результате чего сообщение будет получено всеми зарегистрированными объектами. Далее каждый объект обрабатывает сообщение так, как считает нужным.

Пусть во время инициализации приложения создается только один экземпляр MailManager и любое число объектов Fax и Pager. На рис. 11.1 показано, как инициализируется приложение и что происходит при получении сообщения.

Рис. 11.1. Архитектура приложения, в котором используются события


 

При инициализации приложения создается экземпляр объекта MailManager, поддерживающего событие NewMail. Во время создания объекты Fax и Pager регистрируются в качестве получателей уведомлений о событии NewMail (приход нового сообщения) объекта MailManager, в результате MailManager «знает», что эти объекты следует уведомить о появлении нового сообщения. Если в дальнейшем MailManager получит новое сообщение, это приведет к вызову события NewMail, позволяющего всем зарегистрировавшимся объектам выполнить требуемую обработку нового сообщения.

Разработка типа, поддерживающего событие

Для создания типа, поддерживающего одно или более событий, разработчик должен выполнить ряд действий. Все эти действия будут описаны ниже. Наше приложение MailManager (его можно загрузить в разделе Books сайта http://wintellect.com) содержит весь необходимый код типов MailManager, Fax и Pager. Как вы заметите, типы Fax и Pager практически идентичны.

Этап 1. Определение типа для хранения всей дополнительной информации, передаваемой получателям уведомления о событии

При возникновении события объект, в котором оно возникло, должен передать дополнительную информацию объектам-получателям уведомления о событии. Для предоставления получателям эту информацию нужно инкапсулировать в собственный класс, содержащий набор закрытых полей и набор открытых неизменяемых (только для чтения) свойств. В соответствии с соглашением, классы, содержащие информацию о событиях, передаваемую обработчику события, должны наследовать от типа System. EventArgs, а имя типа должно заканчиваться словом EventArgs. В этом примере у типа NewMailEventArgs есть поля, идентифицирующие отправителя сообщения (m_fnom), его получателя (m_to) и тему (m_subject).

// Этап 1. Определение типа для хранения информации,

// которая передается получателям уведомления о событии internal class NewMailEventArgs : EventArgs {

private readonly String mfrom, m_to, m_subject;

public NewMailEventArgs(String from, String to, String subject) { mfrom = from; m_to = to; msubject = subject;

}

public String From { get { return m_from; } }

public String To { get { return m_to; } }

public String Subject { get { return m_subject; } }

>

ПРИМЕЧАНИЕ

Тип EventArgs определяется в библиотеке классов .NET Framework Class Library (FCL) и выглядит примерно следующим образом:

[ComVisible(true), Serializable] public class EventArgs {

public static readonly EventArgs Empty = new EventArgsQ; public EventArgs() { }

}

Как видите, в этом классе нет ничего особенного. Он просто служит базовым типом, от которого можно порождать другие типы. С большинством событий не передается дополнительной информации. Например, в случае уведомления объектом Button о щелчке на кнопке, само обращение к методу обратного вызова — и есть вся нужная информация. Определяя событие, не передающее дополнительные данные, можно не создавать новый объект Event-Args, достаточно просто воспользоваться свойством EventArgs.Empty.

Этап 2. Определение члена-события

В C# событие объявляется с ключевым словом event. Каждому члену-событию назначаются область действия (практически всегда он открытый, поэтому доступен из любого кода), тип делегата, указывающий на прототип вызываемого метода (или методов), и имя (любой допустимый идентификатор). Вот как выглядит член- событие нашего класса NewMail: internal class MailManager {

// Этап 2. Определение члена-события

public event EventHandler<NewMailEventArgs> NewMail;

}

Здесь NewMail — имя события, а типом события является EventHandler <New- MailEventArgsx Это означает, что получатели уведомления о событии должны предоставлять метод обратного вызова, прототип которого соответствует типу- делегату EventHandler<NewMailEventArgs>. Так как обобщенный делегат System. EventHandler определен следующим образом:

public delegate void EventHandler<TEventArgs>

(Object sender, TEventArgs e) where TEventArgs: EventArgs;

Поэтому прототип метода должен выглядеть так:

void MethodName(Object sender, NewMailEventArgs e);

ПРИМЕЧАНИЕ

Многих удивляет, почему механизм событий требует, чтобы параметр sender имел тип Object. Вообще-то, поскольку MailManager — единственный тип, реализующий события с объектом NewMailEventArgs, было бы разумнее использовать следующий прототип метода обратного вызова:

void MethodName(MailManager sender, NewMailEventArgs e);

Причиной того, что параметр sender имеет тип Object, является наследование. Что произойдет, если MailManager задействовать в качестве базового класса для создания класса SmtpMailManager? В методе обратного вызова придется в прототипе задать параметр sender как SmtpMailManager, а не MailManager, но этого делать нельзя, так как тип SmtpMailManager просто наследует событие NewMail. Поэтому код, ожидающий от SmtpMailManager информацию о событии, все равно будет вынужден приводить аргумент sender к типу SmtpMailManager. Иначе говоря, приведение все равно необходимо, поэтому параметр sender с таким же успехом можно объявить с типом Object.

Еще одна причина для объявления sender с типом Object — гибкость, поскольку делегат может применяться несколькими типами, которые поддерживают событие, передающее объект NewMailEventArgs. В частности, класс PopMailManager мог бы использовать делегат, даже если бы не наследовал от класса MailManager.

И еще одно: механизм событий требует, чтобы в имени делегата и методе обратного вызова параметр, производный от EventArgs, назывался «е». Такое требование устанавливается по единственной причине: для обеспечения единообразия, облегчающего и упрощающего изучение и реализацию событий разработчиками. Инструменты создания кода (например, такой как Microsoft Visual Studio) также «знают», что нужно вызывать параметр е.

И последнее: механизм событий требует, чтобы все обработчики возвращали void. Это обязательно, потому что при возникновении события могут выполняться несколько методов обратного вызова и невозможно получить у них все возвращаемое значение. Тип void просто запрещает методам возвращать какое бы то ни было значение. К сожалению, в библиотеке FCLecTb обработчики событий, в частности ResolveEventHandler, в которых Microsoft не следует собственным правилам и возвращает объект типа Assembly.

Этап 3. Определение метода, ответственного за уведомление зарегистрированных объектов о событии

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

internal class MailManager {

// Этап 3. Определение метода, ответственного за уведомление // зарегистрированных объектов о событии

// Если этот класс изолированный, нужно сделать метод закрытым // или невиртуальным

protected virtual void OnNewMail(NewMailEventArgs e) {

// Сохранить ссылку на делегата во временной переменной // для обеспечения безопасности потоков

EventHandler<NewMailEventArgs> temp = Volatile.Read (ref NewMail);

// Если есть объекты, зарегистрированные для получения // уведомления о событии, уведомляем их if (temp != null) temp(this, в);

}

}

Уведомление о событии, безопасное в отношении потоков

В первом выпуске .NET Framework рекомендовалось уведомлять о событиях следующим образом:

// Версия 1

protected virtual void OnNewMail(NewMailEventArgs e) { if (NewMail != null) NewMail(this, e);

}

Однако в методе OnNewMail кроется одна потенциальная проблема. Программный поток видит, что значение NewMail не равно null, однако перед вызовом NewMail другой поток может удалить делегата из цепочки, присвоив NewMail значение null. В результате будет выдано исключение NullRefenenceException. Для предотвращения состояния гонки многие разработчики пишут следующий код:

// Версия 2

protected void OnNewMail(NewMailEventArgs е) {

EventHandler<NewMailEventArgs> temp = NewMail; if (temp != null) temp(this, e);

}

Идея заключается в том, что ссылка на NewMail копируется во временную переменную temp, которая ссылается на цепочку делегатов в момент назначения. Этот метод сравнивает temp с null и вызывает temp, поэтому уже не имеет значения, поменял ли другой поток NewMail после назначения temp. Вспомните, что делегаты неизменяемы, поэтому теоретически этот способ работает. Однако многие разработчики не осознают, что компилятор может оптимизировать этот программный код, удалив переменную temp. В этом случае обе представленные версии кода окажутся идентичными, в результате опять-таки возможно исключение NullRefenenceException.

Для реального решения этой проблемы необходимо переписать OnNewMail так: // Версия 3

protected void OnNewMail(NewMailEventArgs е) {

EventHandler<NewMailEventArgs> temp = Thread.VolatileRead(ref NewMail); if (temp != null) temp(this, e);

}

Вызов VolatileRead заставляет считывать NewMail в точке вызова и именно в этот момент копировать ссылку в переменную temp. Затем вызов temp осуществляется лишь в том случае, если переменная не равна null. За дополнительной информацией о методе Volatile. Read обращайтесь к главе 28.

И хотя последняя версия этого программного кода является наилучшей и технически корректной, вы также можете использовать версию 2 с JIT-компилятором, не опасаясь за последствия, так как он не будет оптимизировать программный код. Все JIT-компиляторы Microsoft соблюдают принцип отказа от лишних операций чтения из кучи, а следовательно, кэширование ссылки в локальной переменной гарантирует, что обращение по ссылке будет производиться всего один раз. Такое поведение официально не документировано и теоретически может измениться,
поэтому лучше все же использовать последнюю версию представленного программного кода. На практике Microsoft никогда не станет вводить в JIT-компилятор изменения, которые нарушат работу слишком многих приложений1. Кроме того, события в основном используются в однопоточных сценариях (приложения Windows Presentation Foundation и Windows Store), так что безопасность потоков вообще не создает особых проблем.

Для удобства можно определить метод расширения (см. главу 8), инкапсулирующий логику, безопасную в отношении потоков. Определите расширенный метод следующим образом:

public static class EventArgExtensions {

public static void Raise<TEventArgs>(this TEventArgs e,

Object sender, ref EventHandler<TEventArgs> eventDelegate) {

// Копирование ссылки на поле делегата во временное поле // для безопасности в отношении потоков

EventHandler<TEventArgs> temp = Volatile.Read(ref eventDelegate);

// Если зарегистрированный метод заинтересован в событии, уведомите его if (temp != null) temp(sender, e);

>

>

Теперь можно переписать метод OnNewMail следующим образом:

protected virtual void OnNewMail(NewMailEventArgs e) {

e.Raise(this, ref mNewMail);

}

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

Этап 4. Определение метода, преобразующего входную информацию в желаемое событие

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

internal class MailManager {

// Этап 4. Определение метода, преобразующего входную

Меня в этом заверил участник группы разработки JIT-компилятора.

// информацию в желаемое событие

public void SimulateNewMail(String from, String to, String subject) {

// Создать объект для хранения информации, которую // нужно передать получателям уведомления

NewMailEventArgs е = new NewMailEventArgs(from, to, subject);

// Вызвать виртуальный метод, уведомляющий объект о событии // Если ни один из производных типов не переопределяет этот метод, // объект уведомит всех зарегистрированных получателей уведомления OnNewMail(e);

}

Метод SimulateNewMail принимает информацию о сообщении и создает новый объект NewMailEventArgs, передавая его конструктору данные сообщения. Затем вызывается OnNewMail — собственный виртуальный метод объекта MailManager, чтобы формально уведомить объект MailManager о новом почтовом сообщении. Обычно это вызывает инициирование события, в результате уведомляются все зарегистрированные объекты. (Как уже отмечалось, тип, производный от MailManager, может переопределять это действие.)

Реализация событий компилятором

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

public event EventHandler<NewMailEventArgs> NewMail;

При компиляции этой строки компилятор превращает ее в следующие три конструкции:

// 1. ЗАКРЫТОЕ поле делегата, инициализированное значением null private EventHandler<NewMailEventArgs> NewMail = null;

// 2. ОТКРЫТЫЙ метод addXxx (где Xxx - это имя события)

// Позволяет объектам регистрироваться для получения уведомлений о событии public void add_NewMail(EventHandler<NewMailEventArgs> value) {

// Цикл и вызов CompareExchange - хитроумный способ добавления // делегата способом, безопасным в отношении потоков EventHandler<NewMailEventArgs>prevHandler;

EventHandler<NewMailEventArgs> newMail = this.NewMail; do {

prevHandler = newMail;

EventHandler<NewMailEventArgs> newHandler =

(EventHandler<NewMailEventArgs>) Delegate.Combine(prevHandler, value); newMail = Interlocked.CompareExchange<EventHandler<NewMailEventArgs>>( ref this.NewMail, newHandler, prevHandler);

продолжение

} while (newMail != prevHandler);

У

II 3. ОТКРЫТЫЙ метод remove_Xxx (где Ххх - это имя события)

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

public void remove_NewMail(EventHandler<NewMailEventArgs> value) {

// Цикл и вызов CompareExchange - хитроумный способ II удаления делегата способом; безопасным в отношении потоков EventHandler<NewMailEventArgs> prevHandler;

EventHandler<NewMailEventArgs> newMail = this.NewMail; do {

prevHandler = newMail;

EventHandler<NewMailEventArgs> newHandler =

(EventHandler<NewMailEventArgs>) Delegate.Remove(prevHandler, value); newMail = Interlocked.CompareExchange<EventHandler<NewMaiIEventArgs>>( ref this.NewMail, newHandler, prevHandler);

} while (newMail != prevHandler);

}

Первая конструкция — просто поле соответствующего типа делегата. Оно содержит ссылку на заголовок списка делегатов, которые будут уведомляться о возникновении события. Поле инициализируется значением null; это означает, что нет получателей, зарегистрировавшихся на уведомления о событии. Когда метод регистрирует получателя уведомления, в это поле заносится ссылка на экземпляр делегата EventHandler<NewMailEventArgs>, который может, в свою очередь, ссылаться на дополнительные делегаты EventHandler<NewMailEventArgs>. Когда получатель регистрируется для получения уведомления о событии, он просто добавляет в список экземпляр типа делегата. Конечно, отказ от регистрации реализуется удалением соответствующего делегата.

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

Вторая конструкция, генерируемая компилятором С#, — метод, позволяющий другим объектам регистрироваться в качестве получателей уведомления о событии. Компилятор C# автоматически присваивает этой функции имя, добавляя приставку add_ к имени события (NewMail). Компилятор C# также автоматически генерирует код метода, который всегда вызывает статический метод Combine типа System.Delegate. Метод Combine добавляет в список делегатов новый экземпляр и возвращает новый заголовок списка, который снова сохраняется в поле.

Третья и последняя конструкция, генерируемая компилятором С#, — метод, позволяющий объекту отказаться от подписки на событие. И этой функции компилятор C# присваивает имя автоматически, добавляя приставку remove_ к имени события (NewMail). Код метода всегда вызывает метод Remove типа System. Delegate.

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

ВНИМАНИЕ

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

ПРИМЕЧАНИЕ

Оба метода — add и remove — используют хорошо известный паттерн обновления значения способом, безопасным в отношении потоков. Этот паттерн описывается в главе 28.

В приведенном примере методы add и remove объявлены открытыми, поскольку в соответствующей строке исходного кода событие изначально объявлено как открытое. Если бы оно было объявлено как защищенное, то методы add и remove, сгенерированные компилятором, тоже были бы объявлены как защищенные. Так что когда в типе определяется событие, модификатор доступа события указывает, какой код способен регистрироваться и отменять регистрацию для уведомления о событии, но прямым доступом к полю делегата обладает только сам тип. Члены- события также могут объявляться статическими и виртуальными; в этом случае сгенерированные компилятором методы add и remove также будут статическими или виртуальными соответственно.

Помимо генерирования этих трех конструкций, компиляторы генерируют запись с определением события и помещают ее в метаданные управляемого модуля. Эта запись содержит ряд флагов и базовый тип-делегат, а также ссылки на методы доступа add и remove. Эта информация нужна просто для того, чтобы очертить связь между абстрактным понятием «событие» и его методами доступа. Эти метаданные могут использовать компиляторы и другие инструменты, и, конечно же, эти сведения можно получить при помощи класса System. Reflection. Eventlnfo. Однако сама среда CLR эти метаданные не использует и во время выполнения требует лишь наличия методов доступа.

Создание типа, отслеживающего событие

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

internal sealed class Fax {

// Передаем конструктору объект MailManager

public Fax(MailManager mm) {

// Создаем экземпляр делегата EventHandler<NewMailEventArgs>,

// ссылающийся на метод обратного вызова FaxMsg

// Регистрируем обратный вызов для события NewMail объекта MailManager mm.NewMail += FaxMsg;

>

// MailManager вызывает этот метод для уведомления // объекта Fax о прибытии нового почтового сообщения private void FaxMsg(ObJect sender, NewMailEventArgs e) {

// 'sender' используется для взаимодействия с объектом MailManager,

// если потребуется передать ему какую-то информацию

// 'е' определяет дополнительную информацию о событии,

// которую пожелает предоставить MailManager

// Обычно расположенный здесь код отправляет сообщение по факсу // Тестовая реализация выводит информацию на консоль Console.WriteLine("Faxing mail message:");

Console.WriteLine(" From={0}, To={l}, Subject={2}", e.From, e.To, e.Subject);

}

// Этот метод может выполняться для отмены регистрации объекта Fax // в качестве получтеля уведомлений о событии NewMail public void Unregister(MailManager mm) {

// Отменить регистрацию на уведомление о событии NewMail объекта MailManager. mm.NewMail -= FaxMsg;

}

При инициализации почтовое приложение сначала создает объект MailManager и сохраняет ссылку на него в переменной. Затем оно создает объект Fax, передавая ссылку на MailManager как параметр. В конструкторе Fax объект Fax регистрируется на уведомления о событии NewMail объекта MailManager при помощи оператора +=

языка С#:

mm.NewMail += FaxMsg;

Обладая встроенной поддержкой событий, компилятор C# транслирует оператор += в код, регистрирующий объект для получения уведомлений о событии:

mm.add_NewMail(new EventHandler<NewMailEventArgs>(this.FaxMsg));

Как видите, компилятор C# генерирует код, конструирующий делегата EventHandler<NewMailEventArgs>, который инкапсулирует метод FaxMsg класса Fax. Затем компилятор C# вызывает метод add_NewMail объекта MailManager, передавая ему нового делегата. Конечно, вы можете убедиться в этом, скомпилировав код и затем изучив IL-код с помощью такого инструмента, как утилита ILDasm.exe.

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

Когда срабатывает событие объекта MailManager, вызывается метод FaxMsg объекта Fax. Этому методу в первом параметре sender передается ссылка на объект MailManager. Чаще всего этот параметр игнорируется, но он может и использоваться, если в ответ на уведомление о событии объект Fax пожелает получить доступ к полям или методам объекта MailManager. Второй параметр — ссылка на объект NewMailEventArgs. Этот объект содержит всю дополнительную информацию, которая, по мнению NewMailEventArgs, может быть полезной для получателей события.

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

Когда объекту больше не нужны уведомления о событиях, он должен отменить свою регистрацию. Например, объект Fax отменит свою регистрацию в качестве получателя уведомления о событии NewMail, если пользователю больше не нужно пересылать сообщения электронной почты по факсу. Пока объект зарегистрирован в качестве получателя уведомления о событии другого объекта, он не будет уничтожен уборщиком мусора. Если в вашем типе реализован метод Dispose объекта IDisposable, уничтожение объекта должно вызвать отмену его регистрации в качестве получателя уведомлений обо всех событиях (об объекте IDisposable см. также главу 21).

Код, иллюстрирующий отмену регистрации, показан в исходном тексте метода Unregister объекта Fax. Код этого метода фактически идентичен конструктору типа Fax. Единственное отличие в том, что здесь вместо += использован оператор -=. Обнаружив код, отменяющий регистрацию делегата при помощи оператора -=, компилятор C# генерирует вызов метода remove этого события:

mm.remove_NewMail(new ЕventHandlercNewMailEventArgs>(FaxMsg));

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

Кстати, C# требует, чтобы для добавления и удаления делегатов из списка в ваших программах использовались операторы += и -=. Если попытаться напрямую обратиться к методам add или remove, компилятор C# сгенерирует сообщение об ошибке (CS0571: оператор или метод доступа нельзя вызывать явно):

CS0571: cannot explicitly call operator or accessor

Явное управление регистрацией событий

В типе System.Windows.Forms.Control определено около 70 событий. Если тип Control реализует события, позволяя компилятору явно генерировать методы доступа add и remove и поля-делегаты, то каждый объект Control будет иметь 70 полей-делегатов для каждого события! Так как многих программистов интересует относительно небольшое подмножество событий, для каждого объекта, созданного из производного от Control типа, огромный объем памяти будет расходоваться напрасно. Кстати, типы System. Web. UI. Control (из ASP.NET) и System. Windows. UIElement (из Windows Presentation Foundation, WPF) также предлагают множество событий, которые большинство программистов не использует.

В этом разделе рассказано о том, каким образом компилятор C# позволяет разработчикам реализовывать события, управляя тем, как методы add и remove манипулируют делегатами обратных вызовов. Я покажу, как явная реализация события помогает эффективно реализовать класс с поддержкой множества событий. Впрочем, явная реализация событий типа может оказаться полезной и в других ситуациях.

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

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

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

using System;

using System.Collections.Generic; using System.Threading;

// Этот класс нужен для поддержания безопасности типа //и кода при использовании EventSet public sealed class EventKey : Object { }

public sealed class EventSet {

// Закрытый словарь служит для отображения EventKey -> Delegate

private readonly DictionarycEventKey, Delegate) m_events = newDictionary<EventKey, Delegate)();

// Добавление отображения EventKey -> Delegate, если его не существует // И компоновка делегата с существующим ключом EventKey public void Add(EventKey eventKey, Delegate handler) {

Monitor.Enter(m_events);

Delegate d;

m_events.TryGetValue(eventKey, out d); m_events[eventKey] = Delegate.Combine(d, handler);

Monitor.Exit(mevents);

>

// Удаление делегата из EventKey (если он существует)

// и разрыв связи EventKey -) Delegate при удалении // последнего делегата

public void Remove(EventKey eventKey, Delegate handler) {

Monitor.Enter(mevents);

// Вызов TryGetValue предотвращает выдачу исключения

// при попытке удаления делегата с отсутствующим ключом EventKey.

Delegate d;

if (m_events.TryGetValue(eventKey, out d)) { d = Delegate.Remove(d, handler);

// Если делегат остается, то установить новый ключ EventKey,

// иначе - удалить EventKey

if (d != null) m_events[eventKey] = d;

else mevents.Remove(eventKey);

}

Monitor.Exit(mevents);

}

// Информирование о событии для обозначенного ключа EventKey public void Raise(EventKey eventKey, Object sender, EventArgs e) {

// He выдавать исключение при отсутствии ключа EventKey Delegate d;

Monitor.Enter(m_events);

m_events.TryGetValue(eventKey, out d);

Monitor.Exit(m_events);

if (d != null) {

// Из-за того что словарь может содержать несколько разных типов // делегатов, невозможно создать вызов делегата, безопасный по // отношению к типу, во время компиляции. Я вызываю метод // Dynamiclnvoke типа System.Delegate, передавая ему параметры метода // обратного вызова в виде массива объектов. Dynamiclnvoke будет // контролировать безопасность типов параметров для вызываемого // метода обратного вызова. Если будет найдено несоответствие типов, // выдается исключение.

d.DynamicInvoke(newObject[] { sender, е });

}

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

using System;

// Определение типа, унаследованного от EventArgs для этого события public class FooEventArgs : EventArgs { }

public class TypeWithLotsOfEvents {

// Определение закрытого экземплярного поля, ссылающегося на коллекцию.

// Коллекция управляет множеством пар "Event/Delegate"

// Примечание: Тип EventSet не входит в FCL,

// это мой собственный тип

private readonly EventSet m_eventSet = newEventSet();

// Защищенное свойство позволяет производным типам работать с коллекцией protected EventSet EventSet { get { return m_eventSet; } }

#region Code to support the Foo event (repeat this pattern for additional events) // Определение членов, необходимых для события Foo.

// 2а. Создайте статический, доступный только для чтения объект // для идентификации события.

// Каждый объект имеет свой хеш-код для нахождения связанного списка // делегатов события в коллекции.

protected static readonly EventKey sfooEventKey = newEventKeyO;

// 2b. Определение для события методов доступа для добавления // или удаления делегата из коллекции, public event EventHandler<FooEventArgs> Foo { add { meventSet.Add(s_fooEventKey, value); } remove { meventSet.Remove(s_fooEventKey, value); }

}

// 2c. Определение защищенного виртуального метода On для этого события, protected virtual void OnFoo(FooEventArgs e) { m_eventSet.Raise(s_fooEventKey, this, e);

>

// 2d. Определение метода, преобразующего входные данные этого события public void SimulateFooQ {OnFoo(newFooEventArgs());}

#endregion

>

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

public sealed class Program { public static void Main() {

TypeWithLotsOfEvents twle = new TypeWithLotsOfEventsQ;

11 Добавление обратного вызова twle.Foo += HandleFooEvent;

// Проверяем работоспособность twle. SimulateFooQ;

}

private static void HandleFooEvent(obJect sendee FooEventArgs e) { Console.WriteLine("Handling Foo Event here...");

}

Глава 12. Обобщения

Разработчикам хорошо известны достоинства объектно-ориентированного программирования. Одно из ключевых преимуществ — возможность многократного использования кода за счет создания производных классов, наследующих все возможности базового класса. В производном классе можно просто переопределить виртуальные методы или добавить новые методы, чтобы изменить унаследованные от базового класса характеристики для решения новых задач. Обобщения (generics) — еще один механизм, поддерживаемый средой CLR и языками программирования для другой разновидности многократного использования кода — а именно многократного использования алгоритмов.

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

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

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

FCL-класс, инкапсулирующий обобщенный алгоритм управления списками, называется List<T> и определен в пространстве имен System. Collections .Generic. Исходный текст определения этого класса выглядит следующим образом (приводится с сокращениями):

[Serializable]

public class List<T> : IList<T>j ICollection<T>j IEnumerable<T>,

IList, ICollection, IEnumerable {

public List();

public void Add(T item);

public Int32 BinarySearch(T item);

public void ClearQ;

public Boolean Contains(T item); public Int32 IndexOf(T item); public Boolean Remove(T item); public void Sort();

public void Sort(IComparer<T> comparer); public void Sort(Comparison<T> comparison); public T[] ToArray();

public Int32 Count { get; }

public T this[Int32 index] { get; set; }

}

Символами <T> сразу после имени класса автор обобщенного класса List указал, что класс работает с неопределенным типом данных. При определении обобщенного типа или метода переменные, указанные вместо типа (например, Т), называются параметрами типа (type parameters). Т — это имя переменной, которое применяется в исходном тексте во всех местах, где используется соответствующий тип данных. Например, в определении класса List переменная Т служит параметром (метод Add принимает параметр типа Т) и возвращаемым значением (метод ТоАггау возвращает одномерный массив типа Т) метода. Другой пример — метод-индексатор (в C# он называется this). У индексатора есть метод доступа get, возвращающий значение типа Т, и метод доступа set, получающий параметр типа Т. Переменную Т можно использовать в любом месте, где должен указываться тип данных — а значит, и при определении локальных переменных внутри метода или полей внутри типа.

ПРИМЕЧАНИЕ

В рекомендациях Microsoft для проектировщиков указано, что переменные параметров должны называться Т или, в крайнем случае, начинаться с Т (как, например, ТКеу или TValue). Т означает тип (type), а I означает интерфейс (например, IComparable).

Итак, после определения обобщенного типа List<T> готовый обобщенный алгоритм могут использовать другие разработчики; для этого они просто указывают конкретный тип данных, с которым должен работать этот алгоритм. В случае обобщенного типа или метода указанные типы данных называют аргументами-типами (type arguments). Например, разработчик может использовать алгоритм List, указав тип DateTime в качестве аргумента-типа:

private static void SomeMethodQ {

// Создание списка (List), работающего с объектами DateTime List<DateTime> dtList = new List<DateTime>();

// Добавление объекта DateTime в список dtList.Add(DateTime.Now); // Без упаковки

// Добавление еще одного объекта DateTime в список dtList.Add(DateTime.MinValue); // Без упаковки

// Попытка добавить объект типа String в список dtList.Add("l/l/2004"); // Ошибка компиляции

// Извлечение объекта DateTime из списка

DateTime dt = dtList[0]j // Приведение типов не требуется

>

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

□ Защита исходного кода. Разработчику, использующему обобщенный алгоритм, не нужен доступ к исходному тексту алгоритма (при работе с шаблонами С++ разработчику, использующему алгоритм, необходим его исходный текст).

□ Безопасность типов. Когда обобщенный алгоритм применяется с конкретным типом, компилятор и CLR понимают это и следят за тем, чтобы в алгоритме использовались лишь объекты, совместимые с этим типом данных. Попытка использования несовместимого объекта приведет к ошибке на этапе компиляции или исключению во время выполнения. В нашем примере попытка передачи объекта String методу Add вызывает ошибку компиляции.

□ Более простой и понятный код. Поскольку компилятор обеспечивает безопасность типов, в исходном тексте требуется меньше операция приведения типов, а такой код проще писать и сопровождать. В последней строке SomeMethod разработчику не нужно использовать приведение (DateTime), чтобы присвоить переменной dt результат вызова индексатора (при запросе элемента с индексом 0).

□ Повышение производительности. До появления обобщений один из способов определения обобщенного алгоритма заключался в таком определении всех его членов, чтобы они «умели» работать с типом данных Object. Чтобы алгоритм работал с экземплярами значимого типа, перед вызовом членов алгоритма среда CLR должна была упаковать этот экземпляр. Как показано в главе 5, упаковка требует выделения памяти в управляемой куче, что приводит к более частым процедурам уборки мусора, а это, в свою очередь, снижает производительность приложения. Поскольку обобщенный алгоритм можно создать для работы с конкретным значимым типом, экземпляры значимого типа могут передаваться по значению и CLR не нужно выполнять упаковку. Операции приведения типа также не нужны (см. предыдущий пункт), поэтому CLR не нужно контролировать безопасность типов при их преобразовании, что также ускоряет работу кода.

Чтобы убедить вас в том, что обобщения повышают производительность, я написал программу для сравнения производительности необобщенного алгоритма ArrayList из библиотеки классов FCL и обобщенного алгоритма List. В ходе тестирования измерялась производительность алгоритмов с объектами как значимых, так и ссылочных типов:

 

using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics;

public static class Program { public static void Main() {

ValueTypePerfTest();

ReferenceTypePerfTest();

}

private static void ValueTypePerfTest() { const Int32 count = 10000000;

using (new OperationTimer("List<Int32>")) { List<Int32> 1 = new List<Int32>(); for (Int32 n = 0; n < count; n++) {

l.Add(n); // Без упаковки

Int32 x = l[n]; // Без распаковки

}

1 = null; // Для удаления в процессе уборки мусора

}

using (new OperationTimer("ArrayList of Int32")) { ArrayList a = new ArrayList(); for (Int32 n = 0; n < count; n++) {

a.Add(n); // Упаковка

Int32 x = (Int32) a[n]; // Распаковка

}

a = null; // Для удаления в процессе уборки мусора

}

private static void ReferenceTypePerfTestQ {

const Int32 count = 10000000;

using (new OperationTimer("List<String>")) {

List<String> 1 = new List<String>(); for (Int32 n = 0; n < count; n++) {

l.Add("X"); // Копирование ссылки

String x = l[n]; // Копирование ссылки

}

1 = null; // Для удаления в процессе уборки мусора

}

using (new OperationTimer("ArrayList of String")) {

ArrayList a = new ArrayListQ; for (Int32 n = 0; n < count; n++) {

a.Add("X“); // Копирование ссылки

String x = (String) a[n]; // Проверка преобразования

} //и копирование ссылки

а = null; // Для удаления в процессе уборки мусора

}

}

}

// Класс для оценки времени выполнения операций internal sealed class OperationTimer : IDisposable { private Int64 mstartTime; private String mtext; private Int32 mcollectionCount;

public OperationTimer(String text) {

PrepareForOperationQ;

mtext = text;

mcollectionCount = GC.CollectionCount(0);

// Эта команда должна быть последней в этом методе // для максимально точной оценки быстродействия mstartTime = Stopwatch.StartNewQ;

}

public void DisposeQ {

Console.WriteLine("{0} (GCs={l,3}) {2}", (m_stopwatch.Elapsed), GC.CollectionCount(0) mcollectionCount, mtext);

}

private static void PrepareForOperationQ {

GC.CollectQ;

GC.WaitForPendingFinalizers();

GC. CollectQ;

}

Скомпилировав эту программу в окончательной версии (с включенной оптимизацией) и выполнив ее на своем компьютере, я получил следующий результат:

00:00:01.6246959 (GCs= 6) List<Int32>

00:00:10.8555008 (GCs=390) ArrayList of Int32 00:00:02.5427847 (GCs= 4) List<String>

00:00:02.7944831 (GCs= 7) ArrayList of String

Как видите, с типом Int32 обобщенный алгоритм List работает гораздо быстрее, чем необобщенный алгоритм ArrayList. Более того, разница огромная: 1,6 секунды против 11 секунд, то есть в 7 раз быстрее! Кроме того, использование значимого типа (Int32) с алгоритмом ArrayList требует множества операций упаковки, и, как результат, 390 процедур уборки мусора, а в алгоритме List их всего 6.

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

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

ПРИМЕЧАНИЕ

Необходимо понимать, что CLR генерирует машинный код для любого метода при первом его вызове в применении кконкретному типу данных. Это увеличивает размер рабочего набора приложения и снижает производительность. Подробнее об этом мы поговорим чуть позже в разделе «Инфраструктура обобщений».

Обобщения в библиотеке FCL

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

Также имеются безопасные в отношении потоков классы коллекций в пространстве имен System. Collections. Concurrent. Microsoft рекомендует программистам отказаться от необобщенных классов коллекций в пользу их обобщенных аналогов по нескольким причинам. Во-первых, необобщенные классы коллекций, в отличие от обобщенных, не обеспечивают безопасность типов, простоту и понятность кода и повышение производительности. Во-вторых, объектная модель у обобщенных классов лучше, чем у необобщенных. Например, у них меньше виртуальных методов, что повышает производительность, а новые члены, добавленные в обобщенные коллекции, добавляют новую функциональность.

Классы коллекций реализуют множество интерфейсов, а объекты, добавляемые в коллекции, могут реализовывать интерфейсы, используемые классами коллекций для таких операций, как сортировка и поиск. В составе FCL поставляется множество определений обобщенных интерфейсов, поэтому при работе с интерфейсами также доступны преимущества обобщений. Большинство используемых интерфейсов содержится в пространстве имен System.Collections.Generic.

Новые обобщенные интерфейсы не заменяют необобщенные: во многих ситуациях приходится задействовать оба вида интерфейсов. Причина — необходимость сохранения обратной совместимости. Например, если бы класс List<T> реализовывал только интерфейс IList<T>, в коде нельзя было бы рассматривать объект List< DateTime> как Hist.

Также отмечу, что класс System. Array, базовый для всех типов массивов, поддерживает множество статических обобщенных методов, в том числе AsReadOnly, BinarySearch, ConvertAll,Exists,Find, FindAll, Findlndex, FindLast, FindLastlndex, ForEach, IndexOf, LastlndexOf, Resize, Sort и TrueForAll. Вот как

выглядят некоторые из них:

public abstract class Array : ICloneable, IList, ICollection, IEnumerable,

IStructuralComparable, IStructuralEquatable {

public static void Sort<T>(T[] array);

public static void Sort<T>(T[] array, IComparer<T> comparer);

public static Int32 BinarySearch<T>(T[] array, T value); public static Int32 BinarySearch<T>(T[] array, T value, IComparer<T> comparer);

>

Следующий код демонстрирует применение нескольких из этих методов:

public static void Main() {

// Создание и инициализация массива байтов Byte[] byteArray = new Byte[] { 5, 1, 4, 2, 3 };

// Вызов алгоритма сортировки Byte[]

Array.So rt <Byt e >(byt eAr ray);

// Вызов алгоритма двоичного поиска Byte[]

Int32 i = Array.BinarySearch<Byte>(byteArray, 1);

Console.WriteLine(i); // Выводит "0"

}

Инфраструктура обобщений

Поддержка обобщений была добавлена в версию 2.0 CLR, над ее реализацией долго

трудилось множество специалистов. Для поддержания работы обобщений Microsoft

нужно было сделать следующее:

□ Создать новые IL-команды, работающие с аргументами типа.

□ Изменить формат существующих таблиц метаданных для выражения имен типов и методов с обобщенными параметрами.

□ Обновить многие языки программирования (в том числе С#, Microsoft Visual Basic .NET и др.), чтобы обеспечить поддержку нового синтаксиса и позволить разработчикам определять и ссылаться на новые обобщенные типы и методы.