учебники, программирование, основы, введение в,

 

Функциональный тип в C#. Делегаты

Как определяется функциональный тип и как появляются его экземпляры
Слово делегат (delegate) используется в C# для обозначения хорошо известного понятия. Делегат задает определение функционального типа (класса) данных. Экземплярами класса являются функции. Описание делегата в языке C# представляет собой описание еще одного частного случая класса. Каждый делегат описывает множество функций с заданной сигнатурой. Каждая функция (метод), сигнатура которого совпадает с сигнатурой делегата, может рассматриваться как экземпляр класса, заданного делегатом. Синтаксис объявления делегата имеет следующий вид:
[<спецификатор доступа>] delegate <тип результата > <имя класса> (<список аргументов>);
Этим объявлением класса задается функциональный тип - множество функций с заданной сигнатурой, у которых аргументы определяются списком, заданным в объявлении делегата, и тип возвращаемого значения определяется типом результата делегата.
Спецификатор доступа может быть, как обычно, опущен. Где следует размещать объявление делегата? Как и у всякого класса, есть две возможности:

  • непосредственно в пространстве имен, наряду с объявлениями других классов, структур, интерфейсов;
  • внутри другого класса, наряду с объявлениями методов и свойств. Такое объявление рассматривается как объявление вложенного класса.

Так же, как и интерфейсы C#, делегаты не задают реализации. Фактически между некоторыми классами и делегатом заключается контракт на реализацию делегата. Классы, согласные с контрактом, должны объявить у себя статические или динамические функции, сигнатура которых совпадает с сигнатурой делегата. Если контракт выполняется, то можно создать экземпляры делегата, присвоив им в качестве значений функции, удовлетворяющие контракту. Заметьте, контракт является жестким: не допускается ситуация, при которой у делегата тип параметра - object, а у экземпляра соответствующий параметр имеет тип, согласованный с object, например, int.
Начнем примеры этой лекции с объявления трех делегатов. Поместив два из них в пространство имен, третий вложим непосредственно в создаваемый нами класс:
namespace Delegates
{
//объявление классов - делегатов
delegate void Proc(ref int x);
delegate void MesToPers(string s);
class OwnDel
{
public delegate int Fun1(int x);
int Plus1( int x){return(x+100);}//Plus1
int Minus1(int x){return(x-100);}//Minus1
void Plus(ref int x){x+= 100;}
void Minus(ref int x){x-=100;}
//поля класса
public Proc p1;
public Fun1 f1;
char sign;
//конструктор
public OwnDel(char sign)
{
this.sign = sign;
if (sign == '+')
{p1 = new Proc(Plus);f1 = new Fun1(Plus1);}
else
{p1 = new Proc(Minus);f1 = new Fun1(Minus1);}
}
}//class OwnDel
Прокомментирую этот текст.

  • Первым делом объявлены три функциональных класса - три делегата: Proc, MesToPers, Fun1. Каждый из них описывает множество функций фиксированной сигнатуры.
  • В классе OwnDel описаны четыре метода: Plus, Minus, Plus1, Minus1, сигнатуры которых соответствуют сигнатурам, задаваемых классами Proc и Fun1.
  • Поля p1 и f1 класса OwnDel являются экземплярами классов Proc и Fun1.
  • В конструкторе класса поля p1 и f1 связываются с конкретными методами Plus или Minus, Plus1 или Minus1. Связывание с той или иной функцией в данном случае определяется значением поля sign.

Заметьте, экземпляры делегатов можно рассматривать как ссылки (указатели на функции), а методы тех или иных классов с соответствующей сигнатурой можно рассматривать как объекты, хранимые в динамической памяти. В определенный момент происходит связывание ссылки и объекта (в этой роли выступают не обычные объекты, имеющие поля, а методы, задающие код). Взгляд на делегата как на указатель функции характерен для программистов, привыкших к С++.
Приведу теперь процедуру, тестирующую работу созданного класса:
public void TestOwnDel()
{                     
int account = 1000, account1=0;
OwnDel oda = new OwnDel('+');
Console.WriteLine("account = {0}, account1 = {1}",
account, account1);
oda.p1(ref account); account1=oda.f1(account);
Console.WriteLine("account = {0}, account1 = {1}",
account, account1);
}
Клиент класса OwnDel создает экземпляр класса, передавая конструктору знак той операции, которую он хотел бы выполнить над своими счетами - account и account1. Вызов p1 и f1, связанных к моменту вызова с закрытыми методами класса, приводит к выполнению нужных функций.
В нашем примере объявление экземпляров делегатов и связывание их с внутренними методами класса происходило в самом классе. Клиенту оставалось лишь вызывать уже созданные экземпляры, но эту работу можно выполнять и на стороне клиентского класса, чем мы сейчас и займемся. Рассмотрим многократно встречавшийся класс Person, слегка изменив его определение:
class Person
{
//конструкторы
public Person(){name =""; id=0; salary=0.0;}
public Person(string name){this.name = name;}
public Person (string name, int id, double salary)
{this.name = name; this.id=id; this.salary = salary;}
public Person (Person pers)
{this.name = pers.name; this.id = pers.id;
this.salary = pers.salary;}
//методы
public void ToPerson(string mes)
{
this.message = mes;
Console.WriteLine("{0}, {1}",name, message);
}
//свойства
private string name;
private int id;
private double salary;
private string message;
//доступ к свойствам
public string Name
{get {return(name);}        set {name = value;}}
public double Salary
{get {return(salary);}      set {salary = value;}}
public int Id
{get {return(id);}          set {id = value;}}
}//class Person
Класс Person устроен обычным способом: у него несколько перегруженных конструкторов, закрытые поля и процедуры-свойства для доступа к ним. Особо обратить внимание прошу на метод класса ToPerson, сигнатура которого совпадает с сигнатурой класса, определенной введенным ранее делегатом MesToPers. Посмотрите, как клиент класса может связать этот метод с экземпляром делегата, определенного самим клиентом:
Person man1 = new Person("Владимир");
MesToPers mestopers = new MesToPers(man1.ToPerson);
mestopers("пора работать!");
Обратите внимание, что поскольку метод ToPerson не является статическим методом, то при связывании необходимо передать и объект, вызывающий метод. Более того, переданный объект становится доступным экземпляру делегата. Отсюда сразу же становится ясным, что экземпляры делегата - это не просто указатели на функцию, а более сложно организованные структуры. Они, по крайней мере, содержат пару указателей на метод и на объект, вызвавший метод. Вызываемый метод в своей работе использует как информацию, передаваемую ему через аргументы метода, так и информацию, хранящуюся в полях объекта. В данном примере переданное сообщение "пора работать" присоединится к имени объекта, и результирующая строка будет выдана на печать. В тех случаях, когда метод, связываемый с экземпляром делегата, не использует информацию объекта, этот метод может быть объявлен как статический метод класса. Таким образом, инициализировать экземпляры делегата можно как статическими, так и динамическими методами, связанными с конкретными объектами.
Последние три строки были добавлены в вышеприведенную тестирующую процедуру. Взгляните на результаты ее работы.

http://localhost:3232/img/empty.gifФункции высших порядков

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

Вычисление интеграла

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

public class HighOrderIntegral
{
   //delegate
   public delegate double SubIntegralFun(double x);
   public double EvalIntegral(double a, double b,
      double eps,SubIntegralFun sif)
   {
      int n=4;
      double I0=0, I1 = I( a, b, n,sif);
      for( n=8; n < Math.Pow(2.0,15.0); n*=2)
      {
         I0 =I1; I1=I(a,b,n,sif);
         if(Math.Abs(I1-I0)<eps) break;
      }
      if(Math.Abs(I1-I0)< eps)
         Console.WriteLine("Требуемая точность достигнута! "+
            " eps = {0}, достигнутая точность ={1}, n= {2}",
            eps,Math.Abs(I1-I0),n);
      else
         Console.WriteLine("Требуемая точность не достигнута! "+
            " eps = {0}, достигнутая точность ={1}, n= {2}",
            eps,Math.Abs(I1-I0),n);
      return(I1);
   }
   private double I(double a, double b, int n, 
      SubIntegralFun sif)
   {
      //Вычисляет частную сумму по методу трапеций
      double x = a, sum = sif(x)/2, dx = (b-a)/n;
      for (int i= 2; i <= n; i++)
      {
         x += dx;      sum += sif(x);
      }
      x = b; sum += sif(x)/2;
      return(sum*dx);
   }
}//class HighOrderIntegral

Прокомментирую этот текст:

  • Класс HighOrderIntegral предназначен для работы с функциями. В него вложено описание функционального класса - делегата SubIntegralFun, задающего класс функций с одним аргументом типа double и возвращающих значение этого же типа.
  • Метод EvalIntegral - основной метод класса позволяет вычислять определенный интеграл. Этот метод есть функция высшего порядка, поскольку одним из его аргументов является подынтегральная функция, принадлежащая классу SubIntegralFun.
  • Для вычисления интеграла применяется классическая схема. Интервал интегрирования разбивается на n частей, и вычисляется частичная сумма по методу трапеций, представляющая приближенное значение интеграла. Затем n удваивается, и вычисляется новая сумма. Если разность двух приближений по модулю меньше заданной точности eps, то вычисление интеграла заканчивается, иначе процесс повторяется в цикле. Цикл завершается либо по достижении заданной точности, либо когда n достигнет некоторого предельного значения (в нашем случае - 215).
  • Вычисление частичной суммы интеграла по методу трапеций реализовано закрытой процедурой I.
  • Впоследствии класс может быть расширен, и помимо вычисления интеграла он может вычислять и другие характеристики функций.

Чтобы продемонстрировать работу с классом HighOrderIntegral, приведу еще класс Functions, где описано несколько функций, удовлетворяющих контракту, который задан классом SubIntegralFun:

class functions
{
   //подынтегральные функции 
   static public double sif1(double x)
   {
      int k = 1; int b = 2;
      return (double)(k*x +b);
   }
   static public double sif2(double x)
   {
      double a = 1.0; double b = 2.0; double c= 3.0;
      return (double)(a*x*x +b*x +c);
   }
}//class functions

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

public  void TestEvalIntegrals()
{
   double myint1=0.0;
   HighOrderIntegral.SubIntegralFun hoisif1 = 
      new HighOrderIntegral.SubIntegralFun(functions.sif1);
   HighOrderIntegral hoi = new HighOrderIntegral();
   myint1 = hoi.EvalIntegral(2,3,0.1e-5,hoisif1);
   Console.WriteLine("myintegral1 = {0}",myint1);
   HighOrderIntegral.SubIntegralFun hoisif2 =
      new HighOrderIntegral.SubIntegralFun(functions.sif2);
   myint1= hoi.EvalIntegral(2,3,0.1e-5,hoisif2);
   Console.WriteLine("myintegral2 = {0}",myint1);
}//EvalIntegrals

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

Построение программных систем методом "раскрутки". Функции обратного вызова

Метод "раскрутки" является одним из основных методов функционально-ориентированного построения сложных программных систем. Суть его состоит в том, что программная система создается слоями. Вначале пишется ядро системы - нулевой слой, реализующий базовый набор функций. Затем пишется первый слой с новыми функциями, которые интенсивно вызывают в процессе своей работы функции ядра. Теперь система обладает большим набором функций. Каждый новый слой расширяет функциональность системы. Процесс продолжается, пока не будет достигнута заданная функциональность. На рис.20.3, изображающем схему построения системы методом раскрутки, стрелками показано, как функции внешних слоев вызывают функции внутренних слоев.
Успех языка С и операционной системы Unix во многом объясняется тем, что в свое время они были созданы методом раскрутки. Это позволило написать на 95% на языке С транслятор с языка С и операционную систему. Благодаря этому, обеспечивался легкий перенос транслятора и операционной системы на компьютеры с разной системой команд. Замечу, что в те времена мир компьютеров отличался куда большим разнообразием, чем в нынешнее время. Для переноса системы на новый тип компьютера достаточно было написать ядро системы в соответствии с машинным кодом данного компьютера, далее работала раскрутка.
При построении систем методом раскрутки возникает одна проблема. Понятно, что функциям внешнего слоя известно все о внутренних слоях и они без труда могут вызывать функции внутренних слоев. Но как быть, если функциям внутреннего слоя необходимо вызывать функции внешних, еще не написанных и, возможно, еще не спроектированных слоев? Возможна ли симметрия вызовов? На первый взгляд, это кажется невозможным. Но программисты придумали, по крайней мере, два способа этой проблемы. Оба они используют контракты. Один основан на функциях обратного вызова, другой - на наследовании и полиморфизме. Мы разберем оба способа, но начнем с функций обратного вызова.
Пусть F - функция высшего порядка с параметром G функционального типа. Тогда функцию G, задающую параметр (а иногда и саму функцию F), называют функцией обратного вызова (callback функцией). Термин вполне понятен. Если в некотором внешнем слое функция Q вызывает функцию внутреннего слоя F, то предварительно во внешнем слое следует позаботиться о создании функции G, которая и будет передана F. Таким образом, функция Q внешнего слоя вызывает функцию F внутреннего слоя, которая, в свою очередь (обратный вызов) вызывает функцию G внешнего слоя. Чтобы эта техника работала, должен быть задан контракт. Функция высших порядков, написанная во внутреннем слое, задает следующий контракт: "всякая функция, которая собирается меня вызвать, должна передать мне функцию обратного вызова, принадлежащую определенному мной функциональному классу, следовательно, иметь известную мне сигнатуру".
Наш пример с вычислением интеграла хорошо демонстрирует функции обратного вызова и технику "раскрутки". Можно считать, что класс HighOrderIntegral - это внутренний слой нашей системы. В нем задан делегат, определяющий контракт, и функция EvalIntegral, требующая задания функции обратного вызова в качестве ее параметра. Функция EvalIntegral вызывается из внешнего слоя, где и определяются callback функции из класса Functions.
Многие из функций операционной системы Windows, входящие в состав Win API 32, требуют при своем вызове задания callback-функций. Примером может служить работа с объектом операционной системы Timer. Конструктор этого объекта является функцией высшего порядка, и ей в момент создания объекта необходимо в качестве параметра передать callback-функцию, вызываемую для обработки событий, которые поступают от таймера.
Пример работы с таймером приводить сейчас не буду, ограничусь лишь сообщением синтаксиса объявления конструктора объекта Timer:

public Timer(TimerCallback callback,object state, int dueTime, int period);

Первым параметром конструктора является функция обратного вызова callback, которая принадлежит функциональному классу TimerCallback, заданному делегатом:

public delegate void TimerCallback(object state);

http://localhost:3232/img/empty.gifНаследование и полиморфизм - альтернатива обратному вызову

Сегодня многие программные системы проектируются и разрабатываются не в функциональном, а в объектно-ориентированном стиле. Такая система представляет собой одно или несколько семейств интерфейсов и классов, связанных отношением наследования. Классы-потомки наследуют методы своих родителей, могут их переопределять и добавлять новые методы. Переопределив метод родителя, потомки без труда могут вызывать как собственный метод, так и метод родителя; все незакрытые методы родителя им известны и доступны. Но может ли родитель вызывать методы, определенные потомком, учитывая, что в момент создания родительского метода потомок не только не создан, но еще, скоре всего, и не спроектирован? Тем не менее, ответ на этот вопрос положителен. Достигается такая возможность опять-таки благодаря контрактам, заключаемым при реализации полиморфизма.
О полиморфизме говорилось достаточно много в предыдущих лекциях. Тем не менее, позволю напомнить суть дела. Родитель может объявить свой метод виртуальным, в этом случае в контракте на метод потомку разрешается переопределить реализацию, но он не имеет права изменять сигнатуру виртуального метода. Когда некоторый метод родителя Q вызывает виртуальный метод F, то, благодаря позднему связыванию, реализуется полиморфизм и реально будет вызван не метод родителя F, а метод F, который реализован потомком, вызвавшим родительский метод Q. Ситуация в точности напоминает раскрутку и вызов обратных функций. Родительский метод Q находится во внутреннем слое, а потомок с его методом F определен во внешнем слое. Когда потомок вызывает метод Q из внутреннего слоя, тот, в свою очередь, вызывает метод F из внешнего слоя. Сигнатура вызываемого метода F в данном случае задается не делегатом, а сигнатурой виртуального метода, которую, согласно контракту, потомок не может изменить. Давайте вернемся к задаче вычисления интеграла и создадим реализацию, основанную на наследовании и полиморфизме.
Идея примера такова. Вначале построим родительский класс, метод которого будет вычислять интеграл от некоторой подынтегральной функции, заданной виртуальным методом класса. Далее построим класс-потомок, наследующий родительский метод вычисления интеграла и переопределяющий виртуальный метод, в котором потомок задаст собственную подынтегральную функцию. При такой технологии, всякий раз, когда нужно вычислить интеграл, нужно создать класс-потомок, в котором переопределяется виртуальный метод. Приведу пример кода, следующего этой схеме:

class FIntegral
{
   //базовый класс, в котором определен метод вычисления 
   //интеграла и виртуальный метод, задющий базовую
   //подынтегральную функцию 
   public double EvaluateIntegral(double a, double b, 
      double eps)
   {
      int n=4;
      double I0=0, I1 = I( a, b, n);
      for( n=8; n < Math.Pow(2.0,15.0); n*=2)
      {
         I0 =I1; I1=I(a,b,n);
         if(Math.Abs(I1-I0)<eps)break;
      }
      if(Math.Abs(I1-I0)< eps)
         Console.WriteLine("Требуемая точность достигнута! "+
            " eps = {0}, достигнутая точность ={1}, n= {2}",
            eps,Math.Abs(I1-I0),n);
      else
         Console.WriteLine("Требуемая точность не достигнута! "+
            " eps = {0}, достигнутая точность ={1}, n= {2}",
            eps,Math.Abs(I1-I0),n);
      return(I1);                     
   }
   private double I(double a, double b, int n)
   {
      //Вычисляет частную сумму по методу трапеций
      double x = a, sum = sif(x)/2, dx = (b-a)/n;
      for (int i= 2; i <= n; i++)
      {
         x += dx; sum += sif(x);
      }
      x = b; sum += sif(x)/2;
      return(sum*dx);
   }
   protected virtual double sif(double x)
   {return(1.0);}
   
}//FIntegral

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

class FIntegralSon:FIntegral
{
   protected override double sif(double x)
   {
      double a = 1.0; double b = 2.0; double c= 3.0;
      return (double)(a*x*x +b*x +c);
   }
}//FIntegralSon

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

public void TestPolymorphIntegral()
{
   FIntegral integral1 = new FIntegral();
   FIntegralSon integral2 = new FIntegralSon();
   double res1 = integral1.EvaluateIntegral(2.0,3.0,0.1e-5);
   double res2 = integral2.EvaluateIntegral(2.0,3.0,0.1e-5);
   Console.WriteLine("Father = {0}, Son = {1}", res1,res2);
}//PolymorphIntegral

Взгляните на результаты вычислений.

Делегаты как свойства

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

  • класс объектов Person с полями: имя, идентификационный номер, зарплата. В этом классе определим различные реализации функции Compare, позволяющие сравнивать два объекта по имени, по номеру, по зарплате, по нескольким полям. Самое интересное, ради чего и строится данный пример: для каждой реализации Compare будет построена процедура-свойство, которая задает реализацию делегата, определенного в классе Persons;
  • класс Persons будет играть роль контейнера объектов Person.

В этом классе будут определены операции над объектами. Среди операций нас, прежде всего, будет интересовать сортировка объектов, реализованная в виде функции высших порядков. Функциональный параметр будет задавать класс функций сравнения объектов, реализации которых находятся в классе Person. Делегат, определяющий класс функций сравнения, будет задан в классе Persons.
Теперь, когда задача ясна, приступим к ее реализации. Класс Person уже появлялся в наших примерах, поэтому он просто дополнен до нужной функциональности. Добавим методы сравнения двух объектов Person:

//методы сравнения
private static int CompareName(Person obj1, Person obj2)
{
   return(string.Compare(obj1.name,obj2.name));
}
private static int CompareId(Person obj1, Person obj2)
{                      
   if( obj1.id > obj2.id) return(1);
   else return(-1);
}
private static int CompareSalary(Person obj1, Person obj2)
{                      
   if( obj1.salary > obj2.salary) return(1);
      else if(obj1.salary < obj2.salary)return(-1);
         else return(0);
}
private static int CompareSalaryName(Person obj1, Person obj2)
{
   if( obj1.salary > obj2.salary) return(1);
   else if(obj1.salary < obj2.salary)return(-1);
   else return(string.Compare(obj1.name,obj2.name));
}

Заметьте, методы закрыты и, следовательно, недоступны извне. Их четыре, но могло бы быть и больше, при возрастании сложности объекта растет число таких методов. Все методы имеют одну и ту же сигнатуру и удовлетворяют контракту, заданному делегатом, который будет описан чуть позже. Для каждого метода необходимо построить экземпляр делегата, который будет задавать ссылку на метод. Поскольку не все экземпляры нужны одновременно, то хотелось бы строить их динамически, в тот момент, когда они понадобятся. Это можно сделать, причем непосредственно в классе Person. Закрытые методы будем рассматривать как закрытые свойства и для каждого из них введем статическую процедуру-свойство, возвращающую в качестве результата экземпляр делегата со ссылкой на метод. Проще написать, чем объяснить на словах:

//делегаты как свойства
public static Persons.CompareItems SortByName
{
   get  {return(new Persons.CompareItems(CompareName));}
}
public static Persons.CompareItems SortById
{
   get  {return(new Persons.CompareItems(CompareId));}
}
public static Persons.CompareItems SortBySalary
{
   get  {return(new Persons.CompareItems(CompareSalary));}
}
public static Persons.CompareItems SortBySalaryName
{
   get  {return(new Persons.CompareItems(CompareSalaryName));}
}

Всякий раз, когда будет запрошено, например, свойство SortByName класса Person, будет возвращен объект функционального класса Persons.CompareItems, задающий ссылку на метод CompareName класса Person. Объект будет создаваться динамически в момент запроса.
Класс Person полностью определен, и теперь давайте перейдем к определению контейнера, содержащего объекты Person. Начну с определения свойств класса Persons:

class Persons
{       //контейнер объектов Person
   //делегат 
   public delegate int CompareItems(Person obj1, Person obj2);
   private int freeItem = 0;
   const int  n = 100; 
   private Person[]persons = new Person[n];
}

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

//индексатор 
public Person this[int num]
{
   get { return(persons[num-1]); }
   set { persons[num-1] = value; }
}

Добавим классический для контейнеров набор методов - добавление нового элемента, загрузка элементов из базы данных и печать элементов:

public void AddPerson(Person pers)
{
   if(freeItem < n)
   {
      Person p = new Person(pers);
      persons[freeItem++]= p;
   }
   else Console.WriteLine("Не могу добавить Person");
}
public void LoadPersons()
{
   //реально загрузка должна идти из базы данных
   AddPerson(new Person("Соколов",123, 750.0));
   AddPerson(new Person("Синицын",128, 850.0));
   AddPerson(new Person("Воробьев",223, 750.0));
   AddPerson(new Person("Орлов",129, 800.0));
   AddPerson(new Person("Соколов",133, 1750.0));
   AddPerson(new Person("Орлов",119, 750.0));
}//LoadPersons
public void PrintPersons()
{
   for(int i =0; i<freeItem; i++)
   {
      Console.WriteLine("{0,10} {1,5} {2,5}",
      persons[i].Name, persons[i].Id, persons[i].Salary);
   }
}//PrintPersons

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

//сортировка 
public void SimpleSortPerson(CompareItems compare)
{
   Person temp = new Person();
   for(int i = 1; i<freeItem;i++)
      for(int j = freeItem -1; j>=i; j--)
         if (compare(persons[j],persons[j-1])==-1)
         {
            temp = persons[j-1];
            persons[j-1]=persons[j];
            persons[j] = temp;
         }
}//SimpleSortObject
}//Persons

Единственный аргумент метода SimpleSortPerson принадлежит классу CompareItems, заданному делегатом. Что касается метода сортировки, то реализован простейший алгоритм пузырьковой сортировки, со своей задачей он справляется. На этом проектирование классов закончено, нужная цель достигнута, показано, как можно в классе экземпляры делегатов задавать как свойства. Для завершения обсуждения следует продемонстрировать, как этим нужно пользоваться. Зададим, как обычно, тестирующую процедуру, в которой будут использоваться различные критерии сортировки:

public void TestSortPersons()
{
   Persons persons = new Persons();
   persons.LoadPersons();
   Console.WriteLine ("        Сортировка по имени: ");
   persons.SimpleSortPerson(Person.SortByName);
   persons.PrintPersons();
   Console.WriteLine ("        Сортировка по идентификатору: ");
   persons.SimpleSortPerson(Person.SortById);
   persons.PrintPersons();
   Console.WriteLine ("        Сортировка по зарплате: ");
   persons.SimpleSortPerson(Person.SortBySalary);
   persons.PrintPersons();
   Console.WriteLine ("        Сортировка по зарплате и имени: ");
   persons.SimpleSortPerson(Person.SortBySalaryName);
   persons.PrintPersons();
}//SortPersons

Результаты работы сортировки данных изображены на рис. 20.5.

http://localhost:3232/img/empty.gifОперации над делегатами. Класс 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, где, следуя уже описанной технологии, введем делегатов как закрытые свойства, доступ к которым идет через процедуру-свойство get. Три делегата одного класса будут описывать действия трех городских служб. Класс будет описываться ранее введенным делегатом MesToPers, размещенным в пространстве имен проекта. Вот программный код, в котором описаны функции, задающие действия служб:

class Combination
{
   private static void policeman(string mes)
   {
      //анализ сообщения
      if(mes =="Пожар!")
         Console.WriteLine(mes + " Милиция ищет виновных!");
      else     
         Console.WriteLine(mes +" Милиция здесь!");
   }
   private static void ambulanceman(string mes)
   {
      if(mes =="Пожар!")
         Console.WriteLine(mes + " Скорая спасает пострадавших!");
      else
         Console.WriteLine(mes + " Скорая помощь здесь!");
   }
   private static void fireman(string mes)
   {
      if(mes =="Пожар!")
         Console.WriteLine(mes + " Пожарные тушат пожар!");
      else
         Console.WriteLine( mes + " Пожарные здесь!");
   }
}

Как видите, все три функции имеют не только одинаковую сигнатуру, но и устроены одинаково. Они анализируют приходящее к ним сообщение, переданное через параметр 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));}
}

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

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

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

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

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

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

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

Пример "Плохая служба"

Как быть, если в списке вызовов есть "плохой" экземпляр, при вызове которого возникает ошибка, приводящая к выбрасыванию исключительной ситуации? Тогда стоящие за ним в очереди экземпляры не будут вызваны, хотя они вполне могли бы выполнить свою часть работы. В этом случае полезно использовать метод GetInvocationList и в цикле поочередно вызывать делегатов. Вызов делегата следует поместить в охраняемый блок, тогда при возникновении исключительной ситуации в обработчике ситуации можно получить и выдать пользователю всю информацию о нарушителе, а цикл продолжит выполнение очередных делегатов из списка вызова.
Добавим в класс Combination "плохого" кандидата, который пытается делить на ноль:

//метод, вызывающий исключительную ситуацию
public static void BadService(string mes)
{
   int i =7, j=5, k=0;
   Console.WriteLine("Bad Service: Zero Divide");
   j=i/k;
}

Создадим процедуру, в которой в списке вызовов есть хорошие и плохие кандидаты. Эта процедура использует управление исключительными ситуациями, о которых подробнее будет рассказано в последующих лекциях.

public void TestBadJob()
{
   MesToPers Comb;
   Comb = (MesToPers)Delegate.Combine(Combination.Ambulanceman,
      Combination.Policeman);
   Comb = (MesToPers)Delegate.Combine(Comb,
      new MesToPers(Combination.BadService));
   Comb = (MesToPers)Delegate.Combine(Comb,Combination.Fireman);
   foreach(MesToPers currentJob in Comb.GetInvocationList())
   {
      try
      {
         currentJob("Пожар!");
      }
      catch(Exception e)
      {
         Console.WriteLine(e.Message);
         Console.WriteLine(currentJob.Method.Name);
      }
   }
}//BadJob

Поясню, как будет работать эта процедура при ее вызове. Вначале две службы нормально отработают, но при вызове третьей службы возникнет исключительная ситуация "деление на ноль". Универсальный обработчик Exception перехватит эту ситуацию и напечатает как свойство Message объекта e, так и имя метода, вызвавшего исключительную ситуацию, используя свойство Method объекта, вызвавшего ситуацию. После завершения работы блока обработчика ситуации выполнение программы продолжится, выполнится следующий шаг цикла, и служба пожарных благополучно выполнит свою работу. Вот результаты вывода:
Разговор о делегатах еще не закончен. Он будет продолжен в следующей лекции, в которой рассмотрим классы с событиями. События основываются на делегатах.
http://localhost:3232/img/empty.gif

 
На главную | Содержание | < Назад....Вперёд >
С вопросами и предложениями можно обращаться по nicivas@bk.ru. 2013 г.Яндекс.Метрика