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

Делегаты. Функциональный тип данных

< Лекция 6 || Лекция 7: 123456 || Лекция 8 >

Операции над делегатами. Класс Delegate

Давайте просуммируем то, что уже известно о функциональном типе данных. Ключевое слово delegate позволяет задать определение функционального типа (класса), фиксирующее контракт, которому должны удовлетворять все функции, принадлежащие классу. Функциональный класс можно рассматривать как ссылочный тип, экземпляры которого являются ссылками на функции. Заметьте, ссылки на функции - это безопасные по типу указатели, которые ссылаются на функции с жестко фиксированной сигнатурой, заданной делегатом. Следует также понимать, что это не простая ссылка на функцию. В том случае, когда экземпляр делегата инициирован динамическим методом, этот экземпляр хранит ссылку на метод и на объект X, вызвавший этот метод.

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

Delegate FType = new Delegate(<определение типа>)

Но так объявлять переменные этого класса нельзя, и стоит понять, почему. Есть ли вообще класс Delegate? Ответ положителен - есть такой класс. При определении функционального типа, например:

public delegate int FType(int X);

переменная FType принадлежит классу Delegate. Почему же ее нельзя объявить привычным образом? Дело не только в синтаксических особенностях этого класса. Дело в том, что класс Delegate является абстрактным классом. Вот его объявление:

public abstract class Delegate: ICloneable, ISerializable

Для абстрактных классов реализация не определена, и это означает, что нельзя создавать экземпляры класса. Класс Delegate служит базовым классом для классов-наследников. Но создавать наследников могут только компиляторы и системные программы, этого нельзя сделать в программе на C#. Именно поэтому введено ключевое слово delegate, которое косвенно позволяет работать с классом Delegate, создавая уже не абстрактный, а реальный класс. Заметьте, при этом все динамические и статические методы класса Delegate становятся доступными программисту.

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

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

В чем суть комбинирования? Она прозрачна. К экземпляру делегату разрешается поочередно присоединять другие экземпляры делегата того же типа. Поскольку каждый экземпляр хранит ссылку на функцию, в результате создается список ссылок на функции. Этот список называется списком вызовов (invocation list).Когда вызывается экземпляр с присоединенным списком вызова, поочередно, в порядке присоединения, начинают вызываться и выполняться функции, заданные ссылками. Так один вызов порождает выполнение списка работ.

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

Рассмотрим основные методы и свойства класса Delegate. Начнем с двух статических методов - Combine и Remove. Первый из них присоединяет экземпляры делегата к списку, второй - удаляет из списка. Оба метода имеют похожий синтаксис:

Combine(del1, del2)
Remove(del1, del2)

Аргументы del1 и del2 должны быть одного функционального класса. При добавлении del2 в список, в котором del2 уже присутствует, будет добавлен второй экземпляр. При попытке удаления del2 из списка, в котором del2 нет, Remove благополучно завершит работу, не выдавая сообщения об ошибке.

Класс Delegate относится к неизменяемым классам, поэтому оба метода возвращают ссылку на нового делегата. Возвращаемая ссылка принадлежит родительскому классу Delegate, поэтому ее необходимо явно преобразовать к нужному типу, которому принадлежат del1 и del2. Обычное использование этих методов имеет вид:

del1 = (<type>) Combine(del1, del2);
del1 = (<type>) Remove(del1, del2);

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

Два динамических свойства Method и Target полезны для получения подробных сведений о делегате. Чаще всего они используются в процессе отражения, когда делегат поступает извне и необходима метаинформация, поставляемая с делегатом. Свойство Method возвращает объект класса MethodInfo из пространства имен Reflection. Свойство Target возвращает информацию о цели - объекте, вызвавшем делегата, в тех случаях, когда делегат инициируется не статическим методом класса, а динамическим, связанным с вызвавшим его объектом.

У класса Delegate, помимо методов, наследуемых от класса object, есть еще несколько методов, но мы на них останавливаться не будем, они используются не столь часто.

Операции "+" и "-"

Наряду с методами, над делегатами определены и две операции: "+" и "-", которые являются более простой формой записи добавления в список вызовов и удаления из списка. Операции заменяют собой методы Combine и Remove. Выше написанные присваивания объекту del1 с помощью этих операций могут быть переписаны в виде:

del1 +=del2;
del1 -=del2;

Как видите, запись становится проще, исчезает необходимость в задании явного приведения к типу. Ограничения на del1 и del2, естественно, остаются те же, что и для методов Combine и Remove.

Пример "Комбинирование делегатов"

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

Начнем с построения класса с именем Combination. Этот класс будет моделировать работу городских служб, реагирующих на события, происходящие в городе. Каждая служба будет описываться статическим методом. Сигнатуры всех методов будут одинаковыми и будут соответствовать ранее введенному делегату MesToPers. На входе они будут получать сообщение о событии, а на выходе - выдавать результат своих действий.

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

/// <summary>
   /// Класс, статические методы которого
    /// моделируют работу городских служб
   /// </summary>
   class Combination
   {
        // Закрытые сервисы класса
      /// <summary>
      /// Модель действий милиции
      /// </summary>
      /// <param name="mes">
        /// сообщение о возникшем событии</param>
      /// <returns>результат действий</returns>
        static string policeman(string mes)
      {         
         if(mes == "Пожар!")
            return(mes + " Милиция ищет виновных!");
         else   
            return(mes +" Милиция здесь!"); 
      }
      /// <summary>
        /// Модель действий скорой помощи
      /// </summary>
      /// <param name="mes">
        /// </param>сообщение о возникшем событии
        /// <returns>результат действий</returns>
        static string ambulanceman(string mes)
      {
         if(mes == "Пожар!")
            return(mes + " Скорая спасает пострадавших!");
         else
            return(mes + " Скорая помощь здесь!"); 
      }
      /// <summary>
        /// Модель действий пожарных
      /// </summary>
        /// <param name="mes">сообщение о возникшем событии
        /// сообщение о возникшем событии</param>
        /// <returns>результат действий</returns>
        static string fireman(string mes)
      {
         if(mes =="Пожар!")
            return(mes + " Пожарные тушат пожар!");
         else
            return( mes + " Пожарные здесь!"); 
      }        
        //доступ к сервисам через свойства
      public static MesToPers Policeman
      {
         get {return (new MesToPers(policeman));}
      }
      public static MesToPers Fireman
      {
         get {return (new MesToPers(fireman));}
      }
      public static MesToPers Ambulanceman
      {
         get {return (new MesToPers(ambulanceman));}
      }    
   }//class Combination

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

public void TestSomeServices()
      {
         MesToPers Comb;
         Comb = (MesToPers)Delegate.Combine(Combination.Ambulanceman,
            Combination.Policeman);
         Comb = (MesToPers)Delegate.Combine(Comb,Combination.Fireman);
         Console.WriteLine(Comb("Пожар!"));

Вначале объявляется функциональная переменная Comb, которой в следующем операторе присваивается ссылка на экземпляр делегата, созданного методом Combine, чей список вызова содержит ссылки на экземпляры делегатов Ambulanceman и Policeman. Затем к списку вызовов экземпляра Comb присоединяется новый кандидат Fireman. При вызове объекта Comb ему передается сообщение "Пожар!". В результате вызова Comb поочередно запускаются все три экземпляра, входящие в список, каждому из которых передается сообщение.

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

Давайте теперь начнем поочередно отключать делегатов, вызывая затем Comb с новыми сообщениями:

Comb = (MesToPers)Delegate.Remove(Comb,Combination.Fireman);
 //Такое возможно: попытка отключить не существующий элемент 
 Comb = (MesToPers)Delegate.Remove(Comb,Combination.Special);
 Console.WriteLine(Comb("Через 30 минут!"));
 Comb = (MesToPers)Delegate.Remove(Comb,Combination.Policeman);
 Console.WriteLine(Comb("Через час!"));
 Comb = (MesToPers)Delegate.Remove(Comb,Combination.Ambulanceman);
 //Console.WriteLine(Comb("Через два часа!")); // Comb не определен

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

Покажем теперь, что ту же работу можно выполнить, используя операции класса Delegate:

//операции + и -
 Comb = Combination.Ambulanceman;
 Console.WriteLine( Comb.Method.Name);
 Comb+= Combination.Fireman;
 Comb+= Combination.Policeman;
 Console.WriteLine(Comb("День города!"));
 Comb -= Combination.Ambulanceman;
 Comb -= Combination.Fireman;
 Console.WriteLine(Comb("На следующий день!"));
      }//TestSomeServices

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

Службы города

Рис. 6.7. Службы города
< Лекция 6 || Лекция 7: 123456 || Лекция 8 >
Федор Антонов
Федор Антонов

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

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

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

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

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

Добрый день!

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

Дмитрий Штаф
Дмитрий Штаф
Россия
Дмитрий Слапогузов
Дмитрий Слапогузов
Россия, Бийск