КАТЕГОРИИ:
АстрономияБиологияГеографияДругие языкиДругоеИнформатикаИсторияКультураЛитератураЛогикаМатематикаМедицинаМеханикаОбразованиеОхрана трудаПедагогикаПолитикаПравоПсихологияРиторикаСоциологияСпортСтроительствоТехнологияФизикаФилософияФинансыХимияЧерчениеЭкологияЭкономикаЭлектроника
|
Правда о делегатахМожет показаться, что использовать делегаты несложно: вы определяете делегат ключевым словом delegate языка C#, создаете его экземпляр с помощью знакомого оператора new и вызываете метод обратного вызова, пользуясь известным вам синтаксисом «вызова метода» (в котором вместо имени метода указывается переменная, ссылающаяся на объект-делегат). Но на самом деле делегаты гораздо сложнее, чем показывают продемонстрированные примеры. Давайте-ка еще раз изучим эту строку кода: internal delegate void Feedback(Int32 value); На самом деле, обнаружив такую строку, компилятор создает полное определение класса, которое выглядит примерно так:
internal class Feedback : System.MulticastDelegate { // Конструктор. public Feedback(Object obj, IntPtr method) {}
// Метод, прототип которого задан в исходном тексте. public virtual void Invoke(Int32 value){}
// Методы, обеспечивающие асинхронный обратный вызов. public virtual IAsyncResult BeginInvoke(Int32 value, AsyncCallback callback, Object obj){} public virtual void EndInvoke(IAsyncResult result){} } У определенного компилятором класса четыре метода: 1. конструктор 2. Invoke 3. Beginlnvoke 4. Endlnvoke
В этом примере компилятор создал класс Feedback, производный от типа SystemMulticastDelegate, определенного в библиотеке классов .NET Framework Class Library. Внимание! Класс System.MulticastDelegate является производным от System.Delegate, который, в свою очередь, наследует класс System.Object. Наличие двух классов делегатов — факт неприятный, но так уж сложилось исторически; в FCL должен быть лишь один класс делегата. К сожалению, нужно помнить об обоих классах, потому что, даже если вы будете в качестве типов делегатов выбирать в качестве базового класс MulticastDelegate, иногда придется работать с типами делегатов, используя методы, определенные в классе Delegate, а не MulticastDelegate. В частности, у класса Delegate есть статические методы Combine и Remove. Сигнатуры обоих методов указывают, что они принимают параметры типа Delegate. Так как ваши типы делегатов являются производными от MulticastDelegate, который наследует Delegate, экземпляры вашего типа делегата можно передавать в эти методы.
Feedback закрытый класс, так как делегат был объявлен в исходном тексте как internal. Если бы он был объявлен в исходном тексте с модификатором public, то сгенерированный компилятором класс Feedback был бы соответственно открытым. Надо иметь в виду, что типы делегатов могут быть определены как внутри класса (вложенные внутрь другого типа), так и с глобальной областью действия. Поскольку делегаты — это классы, их можно определять в любом месте, где можно определить класс. Поскольку все типы делегатов являются потомками MulticastDelegate, они наследуют все поля, свойства и методы MulticastDelegate. Наверное, самые важные из них — это три закрытых поля (табл. 15-1).
Табл. 15-1. Важнейшие закрытые поля MulticastDelegate
У всех делегатов конструктор принимает два параметра — ссылку на объект и целое число, ссылающееся на метод обратного вызова. Но, изучив исходный текст, можно видеть, что я передаю такие значения, как Program.FeedbackToConsole или p.FeedbackToFile. Интуиция должна подсказать вам, что этот код не должен компилироваться! Но компилятор знает, что создается делегат, и путем синтаксического анализа кода определяет объект и метод, на которые передается ссылка. Ссылка на объект передается в параметре object конструктора, а специальное значение IntPtr (получаемое из маркеров метаданных MethodDef или MethodRef), идентифицирующее метод, передается в параметре method. В случае статического метода в параметре object передается null. Внутри конструктора значения этих параметров сохраняются в закрытых полях _target и _methodPtr. Конструктор также заносит null в поле _invocationList. Любой объект делегата на деле является оболочкой метода и объекта, обрабатываемого этим методом. Поэтому при наличии двух следующих строк кода: Feedback fbStatic = new Feedback(Program.FeedbackToConsole); Feedback fblnstance = new Feedback(new Program().FeedbackToFile); переменные fbStatic и fblnstance ссылаются на два разных инициализированных объекта-делегата Feedback (рис. 15-2).
Рис. 15-2. Переменная, ссылающаяся на делегат статического метода, и переменная, ссылающаяся на делегат экземплярного метода
В классе Delegate определены два неизменяемых открытых экземплярных свойства: Target и Method. При наличии ссылки на объект делегата можно запросить значения этих свойств. Target возвращает ссылку на объект, обрабатываемый при обратном вызове метода. В сущности, Target возвращает значение, хранимое в закрытом поле _target. Если этот метод — статический, Target возвращает null. Свойство Method возвращает объект SystemReflectionMethodlnfo, описывающий метод обратного вызова. В сущности, у свойства Method есть внутренний механизм, который преобразует значение из закрытого поля _methodPtr в объект Methodlnfo и возвращает его. Эту информацию можно использовать по-разному, например, чтобы проверить, не ссылается ли объект делегата на экземплярный метод определенного типа: Boolean DelegateRefersToInstanceMethodOfType(MulticastDelegate d, Type type) { return ((d.Target != null) && d.Target.GetType() == type); } Можно также написать код, проверяющий имя метода обратного вызова (например, FeedbackToMsgBox): Boolean DelegateRefersToMethodOfName(MulticastDelegate d, String methodName) { return (d.Method.Name == methodName); } Есть еще масса других ситуаций, в которых могут быть полезными эти свойства. Поговорим о том, как вызываются методы обратного вызова. Для удобства я еще раз покажу код метода Counter. private static void Counter(Int32 from, Int32 to, Feedback fb) { for (Int32 val = from; val <= to; val++) { // Если указаны какие-либо методы обратного вызова, вызвать их. if (fb != null) fb(val); } } Посмотрим повнимательнее на строку кода, следующую сразу после комментария. Оператор if сначала проверяет, не равна ли null переменная fb. Если нет, выполняется следующая строка, вызывающая метод обратного вызова. Проверка на неравенство null нужна, потому что fb — всего лишь переменная, которая может ссылаться на объект-делегат Feedback, но может быть и null. Может показаться, что я вызываю функцию fb, передавая ей один параметр (val), но такой функции нет. И в этом случае компилятор генерирует код для вызова метода Invoke объекта-делегата, поскольку знает, что fb — это переменная, ссылающаяся на объект-делегат. Иначе говоря, обнаружив строку: fb (val); компилятор генерирует код, как если бы он увидел в исходном тексте следующее: fb.Invoke(val); Вообще, можно в Counter вызывать Invoke явно, как здесь: private static void Counter(Int32 from, Int32 to, Feedback fb) { for (Int32 val = from; val <= to; val++) { // Если указаны какие-либо методы обратного вызова, вызвать их. if (fb != null) fb.Invoke(val); } } Наверное, вы помните, что компилятор определил метод Invoke при определении класса Feedback. При вызове метода Invoke он использует поля _target и _methodPtr для вызова желаемого метода на заданном объекте. Заметьте: сигнатура метода Invoke соответствует такой же сигнатуре делегата, то есть и делегат Feedback, и (созданный компилятором) метод Invoke принимают параметр типа Int32 и возвращают void.
Использование делегатов для обратного вызова множественных методов (цепочки делегатов) Цепочка делегатов (chaining) — это набор объектов-делегатов, которой позволяет вызывать все методы, представленные делегатами набора. Чтобы лучше понять принцип работы цепочки, обратите внимание на метод ChainDelegateDemo1. В нем после оператора ConsoleWriteLine я создаю три объекта-делегата, на которые соответственно ссылаются три переменные fb1,fb2 и fb3 (рис. 15-4). Рис. 15-4. Начальное состояние объектов-делегатов, на которые ссылаются три переменные fbl,fb2 и fb3.
Переменная-ссылка на объект-делегат Feedback должна ссылаться на цепочку или набор делегатов, служащих оболочками методам обратного вызова. Инициализация fbChain значением null говорит об отсутствии методов обратного вызова. Открытый статический метод Combine класса Delegate используется для добавления делегата в цепочку: fbChain = (Feedback)Delegate.Combine(fbChain, fb1); При выполнении этой строчки кода метод Combine «видит» попытку объединить null и fb1. Код метода Combine просто возвращает значение fb1, а в переменной fbChain размещается ссылка на тот же объект-делегат, на который ссылается fb1 (рис. 15-5). Рис. 15-5- Состояние объектов-делегатов после добавления в цепочку второго делегата
Чтобы добавить в цепочку еще один делегат, снова вызывается метод Combine. fbChain = (Feedback)Delegate.Combine(fbChain, fb2); Код метода Combine «видит», что fbChain уже ссылается на объект-делегат, и поэтому создает новый объект-делегат. Новый объект-делегат инициализирует свои закрытые поля _target и _methodPtr. Поле _invocationList инициализируется ссылкой на массив объектов-делегатов. Первый элемент массива (с индексом 0) инициализируется ссылкой на делегат, служащий оберткой метода FeedbackToConsole (это делегат, на который сейчас ссылается fbChain). Второй элемент массива (индекс 1) инициализируется ссылкой на делегат, служащий оберткой метода FeedbackToMsgBox (на этот делегат ссылается fb2). Наконец, переменной fbChain присваивается ссылка на вновь созданный объект-делегат (рис. 15-6). Рис. 15-6. Состояние объектов-делегатов после добавления в цепочку второго делегата
Для создания третьего делегата снова вызывается метод Combine. fbChain = (Feedback)Delegate.Combine(fbChain, fb3); Видя, что fbChain уже ссылается на объект-делегат, Combine создает новый объект-делегат (рис. 15-7). Как и раньше, новый объект-делегат инициализирует свои закрытые поля _target и _methodPtr какими-то значениями, а поле _invocationList инициализируется ссылкой на массив объектов-делегатов. Первый и второй элементы массива (индексы 0 и 1) инициализируются ссылками на те же делегаты, на которые ссылался предыдущий объект-делегат в массиве. Третий элемент массива (индекс 2) инициализируется ссылкой на делегат, служащий оберткой метода FeedbackToFile (на этот делегат ссылается fb3). Наконец, переменной fbChain присваивается ссылка на вновь созданный объект-делегат. Заметьте: ранее созданный делегат и массив, на который ссылается его же поле _invocationList, теперь подлежат обработке механизмом сборки мусора. Рис. 15-7. Окончательное состояние объектов-делегатов в готовой цепочке
После выполнения всего кода создания цепочки, переменная fbChain передается методу Counter. Counter(1, 2, fbChain); Внутри Counter содержится код, неявно вызывающий метод Invoke по отношению к объекту-делегату Feedback. Когда Invoke вызывается по отношению к делегату, на который ссылается fbChain, делегат обнаруживает, что поле _invocationList не равно null, и инициируется выполнение цикла, итеративно обрабатывающего все элементы массива, путем вызова метода, оболочкой которого служит указанный делегат. У нас методы вызываются в следующей последовательности: FeedbackToConsole, FeedbackToMsgBox и FeedbackToFile. В псевдокоде метод Invoke класса Feedback выглядит примерно так: public void Invoke(Int32 value) { Delegate[] delegateSet = _invocationList as Delegate[]; if (delegateSet != null) { // Этот массив делегатов указывает делегаты, которые нужно вызвать. foreach (Feedback d in delegateSet) d(value); // Вызываем каждый делегат. } else { // Этот делегат определяет один метод, // который нужно вызвать по механизму обратного вызова. // Вызвать метод обратного вызова для указанного объекта. _methodPtr.Invoke(_target, value); // Предыдущая строка - очень приблизительная копия реального кода. // Происходящее на самом деле не поддается // иллюстрации средствами С#. } } Делегаты можно удалять из цепочки, вызывая статический метод Remove объекта Delegate. В конце кода метода ChainDelegateDemo1 есть пример: fbChain = (Feedback)Delegate.Remove(fbChain, new Feedback(FeedbackToMsgBox));
Метод Remove сканирует массив делегатов (с конца и до члена с индексом 0), поддерживаемых объектом-делегатом, на который ссылается первый параметр (в нашем примере fbChain). Remove ищет делегат, поля _target и _methodPtr которого совпадают с соответствующими полями второго параметра (в нашем примере нового делегата Feedback). Если Remove находит совпадение и в массиве остается более одного элемента, создается новый объект-делегат — создается массив _invocationList и инициализируется ссылкой на все элементы исходного массива за исключением удаляемого элемента — и возвращается ссылка на новый объект-делегат. При удалении последнего элемента в цепочке Remove возвращает null. Заметьте: за раз Remove удаляет из цепочки лишь один делегат, а не все делегаты с заданными значениями в полях _target и _methodPtr.
Пока мы рассматривали тип-делегат Feedback, возвращающий значение void. Однако его можно было определить и так: internal delegate Int32 Feedback(Int32 value);
В этом случае в псевдокоде метод Invoke выглядел бы примерно так:
public Int32 Invoke(Int32 value) { Int32 result; Delegate[] delegateSet = _invocationList as Delegate[]; if (delegateSet != null) { // Этот массив делегатов указывает делегаты, которые нужно вызвать. foreach (Feedback d in delegateSet) result = d(value); // Вызываем каждый делегат. } else { // Этот делегат определяет один метод, // который нужно вызвать по механизму обратного вызова. //Вызвать метод обратного вызова для указанного объекта. result = _methodPtr.Invoke(_target, value); // Предыдущая строка - очень приблизительная копия реального кода. // Происходящее на самом деле не поддается // иллюстрации средствами С#. } return result; } По мере вызова отдельных делегатов массива возвращаемое значение сохраняется в переменной result. По завершении цикла переменная result содержит результат только последнего вызванного делегата (предыдущие возвращаемые значения отбрасываются); это значение возвращается вызывающему коду, вызвавшему Invoke. Поддержка цепочек делегатов в С# Компилятор С# облегчает жизнь разработчикам, автоматически предоставляя перегрузку операторов «+=» и «-=» для типов делегатов. Эти операторы вызывают методы Delegate.Combine и Delegate.Remove соответственно. Они упрощают построение цепочек делегатов. В результате компиляции методов ChainDelegateDemo1 и ChainDelegateDemo2 (см. пример в начале главы) получается абсолютно идентичный IL-код. Единственная разница в том, что исходный код ChainDelegateDemo2 проще за счет использования операторов «+=» и «-=» языка С#.
|