Основы объектного программирования в классах на C# 3.0

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

Разбить на страницы
Показывать лекцию целиком

Проект к данной лекции Вы можете скачать здесь.

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

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

[<спецификатор доступа>] delegate <тип результата > <имя класса> (<список аргументов>);

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

Спецификатор доступа может быть, как обычно, опущен. Где следует размещать объявление делегата? Как и у всякого класса, есть две возможности:

  • непосредственно в пространстве имен, наряду с объявлениями других классов, структур, интерфейсов;
  • внутри другого класса, наряду с объявлениями методов и свойств. Такое объявление рассматривается как объявление вложенного класса.
  • Так же, как и интерфейсы C#, делегаты не задают реализации. Фактически между некоторыми классами и делегатом заключается контракт на реализацию делегата. Классы, согласные с контрактом, могут объявить у себя статические или динамические функции, сигнатура которых совпадает с сигнатурой делегата. Далее они могут создать экземпляр делегата, присвоив ему в качестве значения функцию, удовлетворяющую контракт. Заметьте, контракт является жестким: не допускается ситуация, при которой у делегата тип параметра - object, а у функции, связываемой с экземпляром, соответствующий параметр имеет тип int, хотя и согласованный с типом object, но не совпадающий с ним.

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

    Содержательный пример работы с делегатами приводился в проекте первой лекции. Читателю есть смысл вернуться к его рассмотрению. Напомню основные детали проекта, связанные с делегатами. Задача состояла в оценке времени работы группы методов, входящих в состав создаваемого класса MyMath, - sin(x), cos(x), tg(x) и им подобные методы, имеющие одинаковую сигнатуру. Для оценки времени был создан специальный класс TimeValue. В этом классе был описан вложенный класс-делегат:

    /// <summary>
     /// Класс спроектирован для получения оценок времени 
     /// выполнения различных методов.
     /// Встроенные делегаты определяют сигнатуры этих методов
     /// </summary>
     public class TimeValue
     {
         public delegate double DToD(double arg1);
     }

    В класс Timevalue включен метод EvalTimeDToD, один из аргументов которого принадлежит функциональному типу, заданному делегатом:

    /// <summary>
    /// Возвращает время в секундах, 
    /// затраченное на вычисление count раз
    /// метода fun с сигнатурой, удовлетворяющей 
    /// делегату DToD (double to double)
    /// </summary>
    /// <param name="count">число повторений</param>
    /// <param name="fun">имя функции</param>
    /// <param name="x">аргумент</param>
    /// <returns>время в милисекундах или тиках</returns>
    public static double EvalTimeDToD(int count, DToD fun, double x)
    {
        DateTime start, finish;
        double res = 0;
        start = DateTime.Now;
            for (int i = 1; i < count; i++)
                fun(x);
        finish = DateTime.Now;
        res = (finish - start).ticks;
        return res;
    }

    Используя этот метод, клиенты класса TimeValue без труда могут оценить время работы любого метода, сигнатура которого задается делегатом DToD. Возможности класса легко расширить, определив новые делегаты.

    Рассмотрим еще несколько более формальных примеров и начнем с объявления трех делегатов. Поместив два из них в пространство имен, третий вложим непосредственно в создаваемый нами класс QwnDel:

    namespace Delegates
    {
       //объявление классов-делегатов
       delegate void Proc(ref int x);
       delegate string 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.
  • Заметьте, поскольку делегаты относятся к ссылочным типам и, соответственно, присваивание является ссылочным присваиванием, то экземпляры делегатов представляют собой ссылки (указатели на функции), а методы тех или иных классов с соответствующей сигнатурой можно рассматривать как объекты, хранимые в динамической памяти. В определенный момент происходит связывание ссылки и объекта (в этой роли выступают не обычные объекты, имеющие поля, а методы, задающие код). Взгляд на делегата как на указатель функции характерен для программистов, привыкших к С++.

    Приведу теперь метод класса Testing - клиента класса OwnDel, тестирующую работу сервисов этого класса:

    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:

    /// <summary>
       /// Класс Person с полями name, id, salary, message.
        /// Позволяет сравнивать объекты Person
        /// по разным критериям.
       /// </summary>
       class Person
       {
            //свойства
            string name;
            int id;
            double salary;
            string message;
            //конструкторы 
          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 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; }
                }
          //методы
          /// <summary>
          /// Передает сообщение объекту
          /// </summary>
          /// <param name="mes">сообщение</param>
          /// <returns>имя с присоединенным сообщением</returns>
            public string ToPerson(string mes)
          {
             this.message = mes;
             return string.Format("{0}, {1}",name, message);
          }      
       }//class Person

    Класс Person будет использоваться и в других примерах данной лекции. При анализе текста этого класса прошу обратить внимание на метод класса ToPerson, сигнатура которого совпадает с сигнатурой, определенной делегатом MesToPers. Посмотрите, как клиент класса может связать этот метод с экземпляром делегата, определенного самим клиентом:

    Person man1 = new Person("Владимир");
             MesToPers mestopers = new MesToPers(man1.ToPerson);
             Console.WriteLine(mestopers("пора работать!"));

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

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

    Последние три строки были добавлены в вышеприведенную тестирующую процедуру TestOwnDel. Взгляните на результаты ее работы.

    (рис 6.1) Объявление делегатов и создание их экземпляров

    Функции высших порядков

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

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

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

    //delegate
          public delegate double DToD(double x);
    
    /// <summary>
       /// Вычисление интеграла.
      /// Сигнатура подынтегральной функции
      /// определяется делегатом 
       /// </summary>
       public class HighOrderIntegral
       {
            /// <summary>
            /// Вычисление интеграла по методу трапеций
            /// </summary>
            /// <param name="a">начало интервала интегрирования</param>
            /// <param name="b">конец интервала интегрирования</param>
            /// <param name="eps">точность вычисления</param>
            /// <param name="sif"> подынтегральная функция</param>
            /// <returns>значение интеграла</returns>
          public double EvalIntegral(double a, double b, 
             double eps, DToD sif)
          {
           const int INITIAL_POINTS = 4;
           const int MAX_POINTS = 2 << 15;
             int n = INITIAL_POINTS;
             double I0=0, I1 = I( a, b, n,sif);
    
             for( n *=2; n < MAX_POINTS; 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, DToD 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 предназначен для работы с функциями. В него вложено описание уже известного нам функционального класса - делегата DToD, который задает класс функций с одним аргументом типа double, возвращающих значение этого же типа.
  • Метод EvalIntegral - основной метод класса позволяет вычислять определенный интеграл. Этот метод есть функция высшего порядка, поскольку одним из его аргументов является подынтегральная функция, принадлежащая классу DToD.
  • Для вычисления интеграла применяется классическая схема. Интервал интегрирования разбивается на n частей, и вычисляется частичная сумма по методу трапеций, представляющая приближенное значение интеграла. Затем n удваивается, и вычисляется новая сумма. Если разность двух приближений по модулю меньше заданной точности eps, то вычисление интеграла заканчивается, иначе процесс повторяется в цикле. Цикл завершается либо по достижении заданной точности, либо когда n достигнет некоторого предельного значения.
  • Вычисление частичной суммы интеграла по методу трапеций реализовано закрытой процедурой.
  • Чтобы продемонстрировать работу с классом HighOrderIntegral, приведу еще класс Functions, где описано несколько функций, удовлетворяющих контракту, который задан классом DToD:

    /// <summary>
     /// Статические методы класса задают простые функции
     /// </summary>
     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()
     {     
      HighOrderIntegral hoi = new HighOrderIntegral();    
      DToD hoisif1 = 
      new DToD(Functions.sif1);    
        double myint1 = hoi.EvalIntegral(2,3,1e-7,hoisif1);
        Console.WriteLine("myintegral1 = {0}",myint1);
        DToD hoisif2 = 
      new DToD(Functions.sif2); 
        myint1= hoi.EvalIntegral(2,3,1e-7,hoisif2);
        Console.WriteLine("myintegral2 = {0}",myint1);
     }//EvalIntegrals

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

    (рис 6.2) Вычисление интеграла с использованием функций высших порядков

    Делегаты и анонимные методы

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

    Хороший стиль программирования требует, чтобы локальная переменная объявлялась как можно ближе к точке ее непосредственного использования. А вот с методами и типами данных все немного сложнее.

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

    HighOrderIntegral hoi = new HighOrderIntegral();         
           DToD hoisif1 = 
                new DToD(Functions.sif1);         
             double myint1 = hoi.EvalIntegral(2,3,1e-7,hoisif1);

    Эти строчки позволяли вычислить интеграл от функции с именем sif1 из класса Functions. Все прекрасно, но для понимания того, какова эта функция, приведенных четырех строк недостаточно - необходимо обращаться к классу Functions. Даже если поместить описание функции в класс Testing, то все равно описание функции отделено от ее использования. Анонимные методы позволяют решить эту проблему. Я вначале приведу пример использования анонимного метода, а затем уже рассмотрим общие свойства этих методов. Итак, как решается задача вычисления интеграла с использованием анонимного метода, задающего подынтегральную функцию? Вот пример решения:

    public void TestAnonymous()
     {
         HighOrderIntegral integral = new HighOrderIntegral();
         double a = 0, b = Math.PI/2, eps = 1e-7;
    
         double result = integral.EvalIntegral(a, b, eps,
             delegate(double x) 
             { return  Math.Sin(x) + Math.Cos(x); });
    
         Console.WriteLine("Integral(Sin(x) + Cos(x)) " +
         "from {0} to {1} = {2} ", a, b, result.ToString());
     }

    По сути, это те же четыре строчки, дополненные печатью результата. Но! Обратите внимание, подынтегральная функция, в данном случае sin(x) + cos(x), задана с помощью делегата непосредственно в точке вызова явным образом своим описанием! Это анонимная функция - она лишена имени, для нее не указан тип, как подобает приличным объектам языка. Во многом анонимные методы напоминают константы, заданные литералами, которые также заданы своими значениями и тип которых определяется по самому значению.

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

    delegate [(<сигнатура метода>)] <тело метода>

    Сигнатура метода может отсутствовать, если у метода нет входных и выходных аргументов. Тело метода - обычный блок.

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

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

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

    Когда для анонимного метода задана сигнатура, применятся следующее правило установления соответствия: сигнатуры источника и цели должны совпадать. Заметьте, речь идет о строгом совпадении типов. Не допускается, чтобы тип цели был double, а у источника - int, или тип цели Father, а тип источника Son - потомок Father.

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

    class Father { }
     class Son : Father { }
     delegate string D1(string s, double x);
     delegate void D2(string s, out string s1);
     delegate string D3(Father item);
     
     public void TestAnonymToDelegate()
     {
         D1 d1 = delegate {return "OK!";};
         D1 d12 = delegate(string s, double x) { return s + x; };
         Console.WriteLine(d1("s",5));
         Console.WriteLine(d12("12", 3));
         string res;     
         D2 d2 = delegate(string s, out string r){r = s+s;};
         d2("Hello ", out res);
         Console.WriteLine(res);
         D3 d3 = delegate(Father item) { return item.ToString(); };
         Console.WriteLine(d3(new Son()));
     }

    К типу делегата D1 приводятся типы двух анонимных методов с разной сигнатурой. Приведение возможно, поскольку оба анонимных метода возвращают строку в качестве результата, что согласуется с типом возвращаемого значения делегата D1. Анонимный метод, используемый для задания переменной d1, не имеет сигнатуры, что позволяет сопоставить его с типом делегата D1, у которого нет выходных аргументов. Сигнатура анонимного метода, используемого для задания переменной d1 2, полностью совпадает с сигнатурой делегата D1.

    Анонимный метод, используемый для задания переменной d2, соответствует по типу делегату D2, требующему, чтобы метод был реализован как процедура и имел выходной аргумент с описателем out. Аналогично устанавливается соответствие между анонимным методом переменной d3 и типом делегата D3. Естественно, что при вызове функциональной переменной d3 в качестве фактического аргумента может передаваться объект класса Son, в то время как тип аргумента анонимного метода полностью соответствует типу аргумента делегата D3 и принадлежит родительскому классу Father. Результаты работы тестирующего метода показаны на рис. 6.3.

    (рис 6.3) Анонимные методы и делегаты

    Анонимные методы и контекст

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

    /// <summary>
        /// Класс с анонимным методом, использующим контекст
        /// </summary>
        class WithAnonymous
        {
            double max;
            double[] array;
            public WithAnonymous(double max, double[] array)
            {
                this.max = max; this.array = array;
            }
            /// <summary>
            /// Обход массива
            /// </summary>
            /// <param name="array"> массив</param>
            /// <param name="res"> результат вычислений</param>
            /// <param name="action">действие над элементами</param>
            void Walk(double[] array, out double res, DToD action )
            {
                int current = 0; res = 0;
                while (current < array.Length)
                    res += action(array[current++]);
            }
            /// <summary>
            /// Конкретизация обхода анонимным методом
            /// </summary>
            /// <param name="min">Нижняя граница отбора</param>
            /// <param name="N">Число отобранных элементов</param>
            /// <param name="res">результат обхода</param>
            public void Traverse(double min, out int N, out double res)
            {
                int n = 0;
                Walk(array, out res, delegate(double x) {
                    if(x > min  x < max) {n++; return x;}
                    else return 0;                      });
                N = n;
            }
        }

    Заметьте, анонимный метод, определенный в методе Traverse, использует аргумент min этого метода, локальную переменную n метода Traverse, поле max класса WithAnonymous. Локальная переменная для анонимного метода является выходной переменной, и метод изменяет ее значение. Анонимный метод может рассматриваться как экземплярный метод класса, поэтому ему доступны поля класса. Если анонимный метод определяется внутри статического метода, то тогда он также рассматривается как статический метод и поля класса ему будут недоступны.

    Для полноты картины приведу тестирующий метод из класса Testing:

    public void TestWithAnonymous()
     {
         WithAnonymous wan = new WithAnonymous(20,
             new double[] { 17, 23, 12, 45, 7 });
         int N;
         double res;
         wan.Traverse(10, out N, out res);
         Console.WriteLine("N= {0}, res = {1}", N, res);
     }

    Результат, как и следовало ожидать, равен 29.

    Анонимные методы и лямбда-выражения

    Анонимные методы прекрасно справляются с поставленной перед ними задачей. Но в C# 3.0 пошли дальше, введя более привычную для математиков форму записи анонимного метода в виде лямбда-выражения. Вместо записи анонимного метода в форме:

    delegate [(<сигнатура метода>)] <тело метода>

    используется форма, задающая лямбда-выражение:

    [(<сигнатура метода>)] =>   <тело метода>

    В методе TestAnonymous анонимный метод определяется следующим образом:

    double result = integral.EvalIntegral(a, b, eps,
             delegate(double x) 
             { return  Math.Sin(x) + Math.Cos(x); });

    Заменим это определение лямбда-выражением:

    double result = integral.EvalIntegral(a, b, eps,
      (double x) =>
      { return Math.Sin(x) + Math.Cos(x); });

    Результат будет тот же. Приведу теперь аналог метода TestAnonymToDelegate, заменив прежние определения анонимных методов лямбда-выражениями.

    public void TestAnonymToLambda()
     {
         D1 d1 = (s, x) => { return "OK!"; };
         D1 d12 = (string s, double x) => { return s + x; };
         Console.WriteLine(d1("s", 5));
         Console.WriteLine(d12("12", 3));
         string res;
         D2 d2 = (string s, out string r) => { r = s + s; };
         d2("Hello ", out res);
         Console.WriteLine(res);
         D3 d3 = item => { return item.ToString(); };
         Console.WriteLine(d3(new Son()));
     }

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

    Анонимный метод, заданный лямбда-выражением, может также использовать контекст. Приведу аналог метода Traverse из класса WithAnonymous:

    public void TraverseL(double min, out int N, out double res)
     {
         int n = 0;
         Walk(array, out res, x =>
         {
             if (x > min  x < max) { n++; return x; }
             else return 0;
         });
         N = n;
     }

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

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

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

    (рис 6.4) Построение системы методом "раскрутки"

    Успех языка С и операционной системы Unix во многом объясняется тем, что в свое время компилятор языка и операционная система были написаны на языке С методом раскрутки. В то время существовало множество разных типов компьютеров, каждый из которых имел свою систему команд. Существовала серьезная проблема переноса операционной системы и компилятора с одного типа компьютера на другой. Благодаря раскрутке, ядро, включающее базовые конструкции языка С и операционной системы, писалось на языке ассемблера данного компьютера, а все остальные слои писались на С. Это позволило написать на 95% на языке С транслятор с языка С и операционную систему, что обеспечивало легкий перенос транслятора и операционной системы на компьютеры с разной системой команд.

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

    Пусть F - функция высшего порядка с параметром G функционального типа. Тогда функцию G, задающую параметр (а иногда и саму функцию F ), называют функцией обратного вызова (callback-функцией). Термин вполне понятен. Если в некотором внешнем слое функция Q вызывает функцию внутреннего слоя F, то, предварительно во внешнем слое следует позаботиться о создании функции G, которая и будет передана F. Таким образом, функция Q внешнего слоя вызывает функцию F внутреннего слоя, которая, в свою очередь (обратный вызов), вызывает функцию G внешнего слоя. Чтобы эта техника работала, должен быть задан контракт. Функция высших порядков, написанная во внутреннем слое, задает следующий контракт: "всякая функция, которая собирается меня вызвать, должна передать мне функцию обратного вызова, принадлежащую определенному мной функциональному классу, следовательно, иметь известную мне сигнатуру".

    Наш пример с вычислением интеграла хорошо демонстрирует функции обратного вызова и технику "раскрутки". Можно считать, что класс HighOrderIntegral - это внутренний слой нашей системы. В нем задан делегат, определяющий контракт, и функция EvalIntegral, требующая задания функции обратного вызова в качестве ее параметра. Функция EvalIntegral вызывается из внешнего слоя (клиентами класса), где и определяются callback -функции. Как показано выше, 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);

    Наследование и полиморфизм - альтернатива обратному вызову

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

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

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

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

    /// <summary>
    /// Класс, в котором определен метод вычисления интеграла
    /// и виртуальный метод, задающий подынтегральную функцию
    /// </summary>
     class FIntegral
    {
       /// <summary>
       /// Вычисление интеграла методом трапеции
       /// </summary>
       /// <param name="a">нижний предел интегрирования</param>
     /// <param name="b">верхний предел интегрирования</param>
        /// <param name="eps">точность вычисления</param>
        /// <returns>значение интеграла</returns>
        public double EvaluateIntegral(double a, double b, double eps)
        {
         const int INITIAL_POINTS = 4;
         const int MAX_POINTS = 2 << 15;
         int n = INITIAL_POINTS;
           double I0=0, I1 = I( a, b, n);
         for (n *= 2; n < MAX_POINTS; 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

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

    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

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

    (рис 6.5) Вычисление интеграла, использующее полиморфизм

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

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

    Рассмотрим пример, демонстрирующий и поясняющий эту возможность при работе с функциями высших порядков. Идея примера такова. Модифицируем уже существующий класс Person и введем новый класс Persons:

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

    Заметьте, можно было бы объявить класс Person наследником интерфейса IComparable, реализовать в классе метод Compare этого интерфейса, перегрузить знаки операций сравнения, что позволяло бы сравнивать персоны по одному критерию. Но в данном случае это было бы методологически неправильно, поскольку персоны, как и другие сложные объекты, следует сравнивать в зависимости от обстоятельств по разным критериям.

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

    //делегат
        delegate int CompareItems(Person obj1, Person obj2);

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

    //делегаты как свойства

    public static CompareItems SortByName
      {
          get
          {
              return (person1, person2) =>
                  {
                      return string.Compare(person1.name, person2.name);
                  };
          }
      }
      public static CompareItems SortById
      {
          get {
              return (person1, person2) =>
                  {
                      if (person1.Id > person2.Id) return 1;
                      else return -1;
                  };
              }
      }
    public static CompareItems SortBySalary
    {
          get
          {
              return (person1, person2) =>
              {
                  if (person1.salary > person2.salary) return 1;
                  else return -1;
              };
          } 
    }
    public static CompareItems SortBySalaryName
    {
          get
          {
              return (person1, person2) =>
              {
                  if (person1.salary > person2.salary) return 1;
                  else if (person1.salary < person2.salary) return -1;
                  else return string.Compare(person1.name, person2.name);
              };
          }
    }

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

    Класс Person полностью определен, и теперь давайте перейдем к определению контейнера, содержащего объекты Person:

    /// <summary>
      /// Контейнер объектов Person
      /// Построен на массиве
      /// </summary>
      class Persons
       {         
          int  n = 100;   //емкость контейнера
            int freeItem;   //свободный для заполнения элемент
            private Person[] persons;   //контейнер 
            //конструкторы
            public Persons()
            {
                n = 100; freeItem = 0;
                persons = new Person[n];
            }
            public Persons(int n)
            {
                this.n = n; freeItem = 0;
                persons = new Person[n];
            }
    
          /// <summary>
          /// Индексатор. 
            /// Доступ к элементам контейнера по индексу
          /// </summary>
          /// <param name="num">индекс</param>
          /// <returns>элемент с заданным индексом</returns>
          public Person this[int num]
          {
             get { return(persons[num-1]); }
             set { persons[num-1] = value; }
          }
          /// <summary>
          /// Добавление элементов в контейнер
          /// </summary>
          /// <param name="pers">добавляемый элемент</param>
          /// <returns>true при успешном добавлении</returns>
          public bool AddPerson(Person pers)
          {
             if(freeItem < n)
             {
                Person p = new Person(pers);
                persons[freeItem++]= p;
                    return true;
             }
             else return false;         
          }
          /// <summary>
          /// Заглушка.
            /// Добавление в контейнер 6 фиксированных элементов
          /// </summary>
            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
          /// <summary>
          /// Вывод на консоль 
          /// </summary>
        public void PrintConsolePersons()
          {         
             for(int i =0; i<freeItem; i++)
             {            
                Console.WriteLine("{0,10}  {1,5}  {2,5}",
                   persons[i].Name, persons[i].Id, persons[i].Salary);
             }
          }//PrintConsolePersons
          
          /// <summary>
          /// Сортировка контейнера методом пузырька
        /// Критерий сортировки задается функцией compare
          /// </summary>
          /// <param name="compare">
          /// функция сравнения элементов контейнера</param>
        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

    Приведу краткие комментарии к этому тексту. Контейнер объектов реализован простейшим образом в виде массива объектов. Три поля класса характеризуют сам массив, его емкость и первый свободный элемент массива. Доступ к элементам контейнера - чтение и запись - реализуются индексатором. Два метода LoadPerson и PrintConsolePersons добавлены в интересах отладки.

    Две основные операции определены над контейнером - добавление нового элемента и сортировка. Код метода сортировки следует внимательно проанализировать. У метода сортировки есть аргумент типа CompareItems, и при вызове метода сортировки клиент может передать в качестве фактического аргумента одну из функций сравнения элементов, определенную в классе Person.

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

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

    Заметьте, клиент не создает никаких экземпляров делегата, он просто при вызове метода сортировки передает методу нужное свойство класса Person.

    Вот как выглядят результаты работы сортировки данных.

    (рис 6.6) Сортировка данных

    Операции над делегатами. Класс 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) Службы города

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

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

    Добавим в класс Combination специальную службу, которая при получении сообщения о пожаре выбрасывает исключение:

    //метод, вызывающий исключительную ситуацию
    /// <summary>
      /// Модель действий специальной службы
      /// </summary>
      /// <param name="mes">
      /// сообщение о возникшем событии</param>
      /// <returns>результат действий</returns>
      static string specialService(string mes)
      {
          if (mes == "Пожар!")
              throw (new Exception("Специальная служба: Кто виноват?"));
          return (mes + " Специальная служба здесь!");
      }
    public static MesToPers Special
        {
     get { return (new MesToPers(specialService)); }
        }

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

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

    Поясню, как будет работать эта процедура при ее вызове. Вначале две службы нормально отработают, но при вызове третьей службы возникнет исключительная ситуация. Универсальный обработчик Exception перехватит эту ситуацию и напечатает как свойство Message объекта e, так и имя метода, вызвавшего исключительную ситуацию, используя свойство Method объекта, вызвавшего ситуацию. После завершения работы блока обработчика ситуации выполнение программы продолжится, выполнится следующий шаг цикла, и милицейская служба благополучно выполнит свою работу. Вот результаты вывода.

    (рис 6.8) "Специальная служба"

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

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

    Проекты

  • Постройте Windows-проект, позволяющий для заданного массива X вычислять некоторую характеристику result, заданную выражением: Метод, вычисляющий значение характеристики result, реализуйте как функцию высшего порядка. Используйте это метод для вычисления таких характеристик массива, как минимум, максимум, сумма и произведение элементов массива.
  • Постройте класс MyMath, являющийся аналогом класса Math из библиотеки FCL. Задайте в этом клаccе общий метод, позволяющий вычислять различные функции ( sin, cos и другие). Реализуйте этот метод как функцию высшего порядка, передавая ему в качестве аргумента функцию, вычисляющую ak по соответствующей рекуррентной формуле.
  • Создайте Windows-проект с классом Functions. Класс должен выполнять различные операции над функциями, принадлежащими функциональному типу, заданному делегатом. В число операций класса включите такие методы, как вычисление интеграла, нахождение максимума, минимума, корней функции на заданном интервале, вычисление значения производной функции в заданной точке. В классе-клиенте, использующем сервисы класса Fnctions, используйте анонимные методы и лямбда-выражения. В интерфейсе проекта предусмотрите возможность задания функции как выражением, так и выбором из фиксированного списка.
  • Создайте Windows-проект с классами Person и Sorting. В классе Sorting предусмотрите разные методы сортировки, заданные функциями высших порядков. Аргументом метода сортировки должна быть функция, позволяющая сравнивать персоны. В клиентском классе предусмотрите построение набора функций, позволяющих сравнивать персоны по разным критериям. Предусмотрите возможность задания таких функций лямбда-выражениями. В интерфейсе проекта предусмотрите возможность сравнения методов сортировки по времени. Постройте метод, вычисляющий время сортировки, как функцию высшего порядка, которой передается в качестве аргумента метод сортировки.
  • Страницы:

    Проект к данной лекции Вы можете скачать здесь.

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

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

    [<спецификатор доступа>] delegate <тип результата > <имя класса> (<список аргументов>);

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

    Спецификатор доступа может быть, как обычно, опущен. Где следует размещать объявление делегата? Как и у всякого класса, есть две возможности:

  • непосредственно в пространстве имен, наряду с объявлениями других классов, структур, интерфейсов;
  • внутри другого класса, наряду с объявлениями методов и свойств. Такое объявление рассматривается как объявление вложенного класса.
  • Так же, как и интерфейсы C#, делегаты не задают реализации. Фактически между некоторыми классами и делегатом заключается контракт на реализацию делегата. Классы, согласные с контрактом, могут объявить у себя статические или динамические функции, сигнатура которых совпадает с сигнатурой делегата. Далее они могут создать экземпляр делегата, присвоив ему в качестве значения функцию, удовлетворяющую контракт. Заметьте, контракт является жестким: не допускается ситуация, при которой у делегата тип параметра - object, а у функции, связываемой с экземпляром, соответствующий параметр имеет тип int, хотя и согласованный с типом object, но не совпадающий с ним.

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

    Содержательный пример работы с делегатами приводился в проекте первой лекции. Читателю есть смысл вернуться к его рассмотрению. Напомню основные детали проекта, связанные с делегатами. Задача состояла в оценке времени работы группы методов, входящих в состав создаваемого класса MyMath, - sin(x), cos(x), tg(x) и им подобные методы, имеющие одинаковую сигнатуру. Для оценки времени был создан специальный класс TimeValue. В этом классе был описан вложенный класс-делегат:

    /// <summary>
     /// Класс спроектирован для получения оценок времени 
     /// выполнения различных методов.
     /// Встроенные делегаты определяют сигнатуры этих методов
     /// </summary>
     public class TimeValue
     {
         public delegate double DToD(double arg1);
     }

    В класс Timevalue включен метод EvalTimeDToD, один из аргументов которого принадлежит функциональному типу, заданному делегатом:

    /// <summary>
    /// Возвращает время в секундах, 
    /// затраченное на вычисление count раз
    /// метода fun с сигнатурой, удовлетворяющей 
    /// делегату DToD (double to double)
    /// </summary>
    /// <param name="count">число повторений</param>
    /// <param name="fun">имя функции</param>
    /// <param name="x">аргумент</param>
    /// <returns>время в милисекундах или тиках</returns>
    public static double EvalTimeDToD(int count, DToD fun, double x)
    {
        DateTime start, finish;
        double res = 0;
        start = DateTime.Now;
            for (int i = 1; i < count; i++)
                fun(x);
        finish = DateTime.Now;
        res = (finish - start).ticks;
        return res;
    }

    Используя этот метод, клиенты класса TimeValue без труда могут оценить время работы любого метода, сигнатура которого задается делегатом DToD. Возможности класса легко расширить, определив новые делегаты.

    Рассмотрим еще несколько более формальных примеров и начнем с объявления трех делегатов. Поместив два из них в пространство имен, третий вложим непосредственно в создаваемый нами класс QwnDel:

    namespace Delegates
    {
       //объявление классов-делегатов
       delegate void Proc(ref int x);
       delegate string 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.
  • Заметьте, поскольку делегаты относятся к ссылочным типам и, соответственно, присваивание является ссылочным присваиванием, то экземпляры делегатов представляют собой ссылки (указатели на функции), а методы тех или иных классов с соответствующей сигнатурой можно рассматривать как объекты, хранимые в динамической памяти. В определенный момент происходит связывание ссылки и объекта (в этой роли выступают не обычные объекты, имеющие поля, а методы, задающие код). Взгляд на делегата как на указатель функции характерен для программистов, привыкших к С++.

    Приведу теперь метод класса Testing - клиента класса OwnDel, тестирующую работу сервисов этого класса:

    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:

    /// <summary>
       /// Класс Person с полями name, id, salary, message.
        /// Позволяет сравнивать объекты Person
        /// по разным критериям.
       /// </summary>
       class Person
       {
            //свойства
            string name;
            int id;
            double salary;
            string message;
            //конструкторы 
          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 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; }
                }
          //методы
          /// <summary>
          /// Передает сообщение объекту
          /// </summary>
          /// <param name="mes">сообщение</param>
          /// <returns>имя с присоединенным сообщением</returns>
            public string ToPerson(string mes)
          {
             this.message = mes;
             return string.Format("{0}, {1}",name, message);
          }      
       }//class Person

    Класс Person будет использоваться и в других примерах данной лекции. При анализе текста этого класса прошу обратить внимание на метод класса ToPerson, сигнатура которого совпадает с сигнатурой, определенной делегатом MesToPers. Посмотрите, как клиент класса может связать этот метод с экземпляром делегата, определенного самим клиентом:

    Person man1 = new Person("Владимир");
             MesToPers mestopers = new MesToPers(man1.ToPerson);
             Console.WriteLine(mestopers("пора работать!"));

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

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

    Последние три строки были добавлены в вышеприведенную тестирующую процедуру TestOwnDel. Взгляните на результаты ее работы.

    (рис 6.1) Объявление делегатов и создание их экземпляров

    Функции высших порядков

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

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

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

    //delegate
          public delegate double DToD(double x);
    
    /// <summary>
       /// Вычисление интеграла.
      /// Сигнатура подынтегральной функции
      /// определяется делегатом 
       /// </summary>
       public class HighOrderIntegral
       {
            /// <summary>
            /// Вычисление интеграла по методу трапеций
            /// </summary>
            /// <param name="a">начало интервала интегрирования</param>
            /// <param name="b">конец интервала интегрирования</param>
            /// <param name="eps">точность вычисления</param>
            /// <param name="sif"> подынтегральная функция</param>
            /// <returns>значение интеграла</returns>
          public double EvalIntegral(double a, double b, 
             double eps, DToD sif)
          {
           const int INITIAL_POINTS = 4;
           const int MAX_POINTS = 2 << 15;
             int n = INITIAL_POINTS;
             double I0=0, I1 = I( a, b, n,sif);
    
             for( n *=2; n < MAX_POINTS; 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, DToD 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 предназначен для работы с функциями. В него вложено описание уже известного нам функционального класса - делегата DToD, который задает класс функций с одним аргументом типа double, возвращающих значение этого же типа.
  • Метод EvalIntegral - основной метод класса позволяет вычислять определенный интеграл. Этот метод есть функция высшего порядка, поскольку одним из его аргументов является подынтегральная функция, принадлежащая классу DToD.
  • Для вычисления интеграла применяется классическая схема. Интервал интегрирования разбивается на n частей, и вычисляется частичная сумма по методу трапеций, представляющая приближенное значение интеграла. Затем n удваивается, и вычисляется новая сумма. Если разность двух приближений по модулю меньше заданной точности eps, то вычисление интеграла заканчивается, иначе процесс повторяется в цикле. Цикл завершается либо по достижении заданной точности, либо когда n достигнет некоторого предельного значения.
  • Вычисление частичной суммы интеграла по методу трапеций реализовано закрытой процедурой.
  • Чтобы продемонстрировать работу с классом HighOrderIntegral, приведу еще класс Functions, где описано несколько функций, удовлетворяющих контракту, который задан классом DToD:

    /// <summary>
     /// Статические методы класса задают простые функции
     /// </summary>
     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()
     {     
      HighOrderIntegral hoi = new HighOrderIntegral();    
      DToD hoisif1 = 
      new DToD(Functions.sif1);    
        double myint1 = hoi.EvalIntegral(2,3,1e-7,hoisif1);
        Console.WriteLine("myintegral1 = {0}",myint1);
        DToD hoisif2 = 
      new DToD(Functions.sif2); 
        myint1= hoi.EvalIntegral(2,3,1e-7,hoisif2);
        Console.WriteLine("myintegral2 = {0}",myint1);
     }//EvalIntegrals

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

    (рис 6.2) Вычисление интеграла с использованием функций высших порядков

    Делегаты и анонимные методы

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

    Хороший стиль программирования требует, чтобы локальная переменная объявлялась как можно ближе к точке ее непосредственного использования. А вот с методами и типами данных все немного сложнее.

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

    HighOrderIntegral hoi = new HighOrderIntegral();         
           DToD hoisif1 = 
                new DToD(Functions.sif1);         
             double myint1 = hoi.EvalIntegral(2,3,1e-7,hoisif1);

    Эти строчки позволяли вычислить интеграл от функции с именем sif1 из класса Functions. Все прекрасно, но для понимания того, какова эта функция, приведенных четырех строк недостаточно - необходимо обращаться к классу Functions. Даже если поместить описание функции в класс Testing, то все равно описание функции отделено от ее использования. Анонимные методы позволяют решить эту проблему. Я вначале приведу пример использования анонимного метода, а затем уже рассмотрим общие свойства этих методов. Итак, как решается задача вычисления интеграла с использованием анонимного метода, задающего подынтегральную функцию? Вот пример решения:

    public void TestAnonymous()
     {
         HighOrderIntegral integral = new HighOrderIntegral();
         double a = 0, b = Math.PI/2, eps = 1e-7;
    
         double result = integral.EvalIntegral(a, b, eps,
             delegate(double x) 
             { return  Math.Sin(x) + Math.Cos(x); });
    
         Console.WriteLine("Integral(Sin(x) + Cos(x)) " +
         "from {0} to {1} = {2} ", a, b, result.ToString());
     }

    По сути, это те же четыре строчки, дополненные печатью результата. Но! Обратите внимание, подынтегральная функция, в данном случае sin(x) + cos(x), задана с помощью делегата непосредственно в точке вызова явным образом своим описанием! Это анонимная функция - она лишена имени, для нее не указан тип, как подобает приличным объектам языка. Во многом анонимные методы напоминают константы, заданные литералами, которые также заданы своими значениями и тип которых определяется по самому значению.

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

    delegate [(<сигнатура метода>)] <тело метода>

    Сигнатура метода может отсутствовать, если у метода нет входных и выходных аргументов. Тело метода - обычный блок.

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

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

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

    Когда для анонимного метода задана сигнатура, применятся следующее правило установления соответствия: сигнатуры источника и цели должны совпадать. Заметьте, речь идет о строгом совпадении типов. Не допускается, чтобы тип цели был double, а у источника - int, или тип цели Father, а тип источника Son - потомок Father.

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

    class Father { }
     class Son : Father { }
     delegate string D1(string s, double x);
     delegate void D2(string s, out string s1);
     delegate string D3(Father item);
     
     public void TestAnonymToDelegate()
     {
         D1 d1 = delegate {return "OK!";};
         D1 d12 = delegate(string s, double x) { return s + x; };
         Console.WriteLine(d1("s",5));
         Console.WriteLine(d12("12", 3));
         string res;     
         D2 d2 = delegate(string s, out string r){r = s+s;};
         d2("Hello ", out res);
         Console.WriteLine(res);
         D3 d3 = delegate(Father item) { return item.ToString(); };
         Console.WriteLine(d3(new Son()));
     }

    К типу делегата D1 приводятся типы двух анонимных методов с разной сигнатурой. Приведение возможно, поскольку оба анонимных метода возвращают строку в качестве результата, что согласуется с типом возвращаемого значения делегата D1. Анонимный метод, используемый для задания переменной d1, не имеет сигнатуры, что позволяет сопоставить его с типом делегата D1, у которого нет выходных аргументов. Сигнатура анонимного метода, используемого для задания переменной d1 2, полностью совпадает с сигнатурой делегата D1.

    Анонимный метод, используемый для задания переменной d2, соответствует по типу делегату D2, требующему, чтобы метод был реализован как процедура и имел выходной аргумент с описателем out. Аналогично устанавливается соответствие между анонимным методом переменной d3 и типом делегата D3. Естественно, что при вызове функциональной переменной d3 в качестве фактического аргумента может передаваться объект класса Son, в то время как тип аргумента анонимного метода полностью соответствует типу аргумента делегата D3 и принадлежит родительскому классу Father. Результаты работы тестирующего метода показаны на рис. 6.3.

    (рис 6.3) Анонимные методы и делегаты

    Анонимные методы и контекст

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

    /// <summary>
        /// Класс с анонимным методом, использующим контекст
        /// </summary>
        class WithAnonymous
        {
            double max;
            double[] array;
            public WithAnonymous(double max, double[] array)
            {
                this.max = max; this.array = array;
            }
            /// <summary>
            /// Обход массива
            /// </summary>
            /// <param name="array"> массив</param>
            /// <param name="res"> результат вычислений</param>
            /// <param name="action">действие над элементами</param>
            void Walk(double[] array, out double res, DToD action )
            {
                int current = 0; res = 0;
                while (current < array.Length)
                    res += action(array[current++]);
            }
            /// <summary>
            /// Конкретизация обхода анонимным методом
            /// </summary>
            /// <param name="min">Нижняя граница отбора</param>
            /// <param name="N">Число отобранных элементов</param>
            /// <param name="res">результат обхода</param>
            public void Traverse(double min, out int N, out double res)
            {
                int n = 0;
                Walk(array, out res, delegate(double x) {
                    if(x > min  x < max) {n++; return x;}
                    else return 0;                      });
                N = n;
            }
        }

    Заметьте, анонимный метод, определенный в методе Traverse, использует аргумент min этого метода, локальную переменную n метода Traverse, поле max класса WithAnonymous. Локальная переменная для анонимного метода является выходной переменной, и метод изменяет ее значение. Анонимный метод может рассматриваться как экземплярный метод класса, поэтому ему доступны поля класса. Если анонимный метод определяется внутри статического метода, то тогда он также рассматривается как статический метод и поля класса ему будут недоступны.

    Для полноты картины приведу тестирующий метод из класса Testing:

    public void TestWithAnonymous()
     {
         WithAnonymous wan = new WithAnonymous(20,
             new double[] { 17, 23, 12, 45, 7 });
         int N;
         double res;
         wan.Traverse(10, out N, out res);
         Console.WriteLine("N= {0}, res = {1}", N, res);
     }

    Результат, как и следовало ожидать, равен 29.

    Анонимные методы и лямбда-выражения

    Анонимные методы прекрасно справляются с поставленной перед ними задачей. Но в C# 3.0 пошли дальше, введя более привычную для математиков форму записи анонимного метода в виде лямбда-выражения. Вместо записи анонимного метода в форме:

    delegate [(<сигнатура метода>)] <тело метода>

    используется форма, задающая лямбда-выражение:

    [(<сигнатура метода>)] =>   <тело метода>

    В методе TestAnonymous анонимный метод определяется следующим образом:

    double result = integral.EvalIntegral(a, b, eps,
             delegate(double x) 
             { return  Math.Sin(x) + Math.Cos(x); });

    Заменим это определение лямбда-выражением:

    double result = integral.EvalIntegral(a, b, eps,
      (double x) =>
      { return Math.Sin(x) + Math.Cos(x); });

    Результат будет тот же. Приведу теперь аналог метода TestAnonymToDelegate, заменив прежние определения анонимных методов лямбда-выражениями.

    public void TestAnonymToLambda()
     {
         D1 d1 = (s, x) => { return "OK!"; };
         D1 d12 = (string s, double x) => { return s + x; };
         Console.WriteLine(d1("s", 5));
         Console.WriteLine(d12("12", 3));
         string res;
         D2 d2 = (string s, out string r) => { r = s + s; };
         d2("Hello ", out res);
         Console.WriteLine(res);
         D3 d3 = item => { return item.ToString(); };
         Console.WriteLine(d3(new Son()));
     }

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

    Анонимный метод, заданный лямбда-выражением, может также использовать контекст. Приведу аналог метода Traverse из класса WithAnonymous:

    public void TraverseL(double min, out int N, out double res)
     {
         int n = 0;
         Walk(array, out res, x =>
         {
             if (x > min  x < max) { n++; return x; }
             else return 0;
         });
         N = n;
     }

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

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

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

    (рис 6.4) Построение системы методом "раскрутки"

    Успех языка С и операционной системы Unix во многом объясняется тем, что в свое время компилятор языка и операционная система были написаны на языке С методом раскрутки. В то время существовало множество разных типов компьютеров, каждый из которых имел свою систему команд. Существовала серьезная проблема переноса операционной системы и компилятора с одного типа компьютера на другой. Благодаря раскрутке, ядро, включающее базовые конструкции языка С и операционной системы, писалось на языке ассемблера данного компьютера, а все остальные слои писались на С. Это позволило написать на 95% на языке С транслятор с языка С и операционную систему, что обеспечивало легкий перенос транслятора и операционной системы на компьютеры с разной системой команд.

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

    Пусть F - функция высшего порядка с параметром G функционального типа. Тогда функцию G, задающую параметр (а иногда и саму функцию F ), называют функцией обратного вызова (callback-функцией). Термин вполне понятен. Если в некотором внешнем слое функция Q вызывает функцию внутреннего слоя F, то, предварительно во внешнем слое следует позаботиться о создании функции G, которая и будет передана F. Таким образом, функция Q внешнего слоя вызывает функцию F внутреннего слоя, которая, в свою очередь (обратный вызов), вызывает функцию G внешнего слоя. Чтобы эта техника работала, должен быть задан контракт. Функция высших порядков, написанная во внутреннем слое, задает следующий контракт: "всякая функция, которая собирается меня вызвать, должна передать мне функцию обратного вызова, принадлежащую определенному мной функциональному классу, следовательно, иметь известную мне сигнатуру".

    Наш пример с вычислением интеграла хорошо демонстрирует функции обратного вызова и технику "раскрутки". Можно считать, что класс HighOrderIntegral - это внутренний слой нашей системы. В нем задан делегат, определяющий контракт, и функция EvalIntegral, требующая задания функции обратного вызова в качестве ее параметра. Функция EvalIntegral вызывается из внешнего слоя (клиентами класса), где и определяются callback -функции. Как показано выше, 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);

    Наследование и полиморфизм - альтернатива обратному вызову

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

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

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

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

    /// <summary>
    /// Класс, в котором определен метод вычисления интеграла
    /// и виртуальный метод, задающий подынтегральную функцию
    /// </summary>
     class FIntegral
    {
       /// <summary>
       /// Вычисление интеграла методом трапеции
       /// </summary>
       /// <param name="a">нижний предел интегрирования</param>
     /// <param name="b">верхний предел интегрирования</param>
        /// <param name="eps">точность вычисления</param>
        /// <returns>значение интеграла</returns>
        public double EvaluateIntegral(double a, double b, double eps)
        {
         const int INITIAL_POINTS = 4;
         const int MAX_POINTS = 2 << 15;
         int n = INITIAL_POINTS;
           double I0=0, I1 = I( a, b, n);
         for (n *= 2; n < MAX_POINTS; 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

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

    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

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

    (рис 6.5) Вычисление интеграла, использующее полиморфизм

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

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

    Рассмотрим пример, демонстрирующий и поясняющий эту возможность при работе с функциями высших порядков. Идея примера такова. Модифицируем уже существующий класс Person и введем новый класс Persons:

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

    Заметьте, можно было бы объявить класс Person наследником интерфейса IComparable, реализовать в классе метод Compare этого интерфейса, перегрузить знаки операций сравнения, что позволяло бы сравнивать персоны по одному критерию. Но в данном случае это было бы методологически неправильно, поскольку персоны, как и другие сложные объекты, следует сравнивать в зависимости от обстоятельств по разным критериям.

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

    //делегат
        delegate int CompareItems(Person obj1, Person obj2);

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

    //делегаты как свойства

    public static CompareItems SortByName
      {
          get
          {
              return (person1, person2) =>
                  {
                      return string.Compare(person1.name, person2.name);
                  };
          }
      }
      public static CompareItems SortById
      {
          get {
              return (person1, person2) =>
                  {
                      if (person1.Id > person2.Id) return 1;
                      else return -1;
                  };
              }
      }
    public static CompareItems SortBySalary
    {
          get
          {
              return (person1, person2) =>
              {
                  if (person1.salary > person2.salary) return 1;
                  else return -1;
              };
          } 
    }
    public static CompareItems SortBySalaryName
    {
          get
          {
              return (person1, person2) =>
              {
                  if (person1.salary > person2.salary) return 1;
                  else if (person1.salary < person2.salary) return -1;
                  else return string.Compare(person1.name, person2.name);
              };
          }
    }

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

    Класс Person полностью определен, и теперь давайте перейдем к определению контейнера, содержащего объекты Person:

    /// <summary>
      /// Контейнер объектов Person
      /// Построен на массиве
      /// </summary>
      class Persons
       {         
          int  n = 100;   //емкость контейнера
            int freeItem;   //свободный для заполнения элемент
            private Person[] persons;   //контейнер 
            //конструкторы
            public Persons()
            {
                n = 100; freeItem = 0;
                persons = new Person[n];
            }
            public Persons(int n)
            {
                this.n = n; freeItem = 0;
                persons = new Person[n];
            }
    
          /// <summary>
          /// Индексатор. 
            /// Доступ к элементам контейнера по индексу
          /// </summary>
          /// <param name="num">индекс</param>
          /// <returns>элемент с заданным индексом</returns>
          public Person this[int num]
          {
             get { return(persons[num-1]); }
             set { persons[num-1] = value; }
          }
          /// <summary>
          /// Добавление элементов в контейнер
          /// </summary>
          /// <param name="pers">добавляемый элемент</param>
          /// <returns>true при успешном добавлении</returns>
          public bool AddPerson(Person pers)
          {
             if(freeItem < n)
             {
                Person p = new Person(pers);
                persons[freeItem++]= p;
                    return true;
             }
             else return false;         
          }
          /// <summary>
          /// Заглушка.
            /// Добавление в контейнер 6 фиксированных элементов
          /// </summary>
            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
          /// <summary>
          /// Вывод на консоль 
          /// </summary>
        public void PrintConsolePersons()
          {         
             for(int i =0; i<freeItem; i++)
             {            
                Console.WriteLine("{0,10}  {1,5}  {2,5}",
                   persons[i].Name, persons[i].Id, persons[i].Salary);
             }
          }//PrintConsolePersons
          
          /// <summary>
          /// Сортировка контейнера методом пузырька
        /// Критерий сортировки задается функцией compare
          /// </summary>
          /// <param name="compare">
          /// функция сравнения элементов контейнера</param>
        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

    Приведу краткие комментарии к этому тексту. Контейнер объектов реализован простейшим образом в виде массива объектов. Три поля класса характеризуют сам массив, его емкость и первый свободный элемент массива. Доступ к элементам контейнера - чтение и запись - реализуются индексатором. Два метода LoadPerson и PrintConsolePersons добавлены в интересах отладки.

    Две основные операции определены над контейнером - добавление нового элемента и сортировка. Код метода сортировки следует внимательно проанализировать. У метода сортировки есть аргумент типа CompareItems, и при вызове метода сортировки клиент может передать в качестве фактического аргумента одну из функций сравнения элементов, определенную в классе Person.

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

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

    Заметьте, клиент не создает никаких экземпляров делегата, он просто при вызове метода сортировки передает методу нужное свойство класса Person.

    Вот как выглядят результаты работы сортировки данных.

    (рис 6.6) Сортировка данных

    Операции над делегатами. Класс 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) Службы города

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

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

    Добавим в класс Combination специальную службу, которая при получении сообщения о пожаре выбрасывает исключение:

    //метод, вызывающий исключительную ситуацию
    /// <summary>
      /// Модель действий специальной службы
      /// </summary>
      /// <param name="mes">
      /// сообщение о возникшем событии</param>
      /// <returns>результат действий</returns>
      static string specialService(string mes)
      {
          if (mes == "Пожар!")
              throw (new Exception("Специальная служба: Кто виноват?"));
          return (mes + " Специальная служба здесь!");
      }
    public static MesToPers Special
        {
     get { return (new MesToPers(specialService)); }
        }

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

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

    Поясню, как будет работать эта процедура при ее вызове. Вначале две службы нормально отработают, но при вызове третьей службы возникнет исключительная ситуация. Универсальный обработчик Exception перехватит эту ситуацию и напечатает как свойство Message объекта e, так и имя метода, вызвавшего исключительную ситуацию, используя свойство Method объекта, вызвавшего ситуацию. После завершения работы блока обработчика ситуации выполнение программы продолжится, выполнится следующий шаг цикла, и милицейская служба благополучно выполнит свою работу. Вот результаты вывода.

    (рис 6.8) "Специальная служба"

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

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

    Проекты

  • Постройте Windows-проект, позволяющий для заданного массива X вычислять некоторую характеристику result, заданную выражением: Метод, вычисляющий значение характеристики result, реализуйте как функцию высшего порядка. Используйте это метод для вычисления таких характеристик массива, как минимум, максимум, сумма и произведение элементов массива.
  • Постройте класс MyMath, являющийся аналогом класса Math из библиотеки FCL. Задайте в этом клаccе общий метод, позволяющий вычислять различные функции ( sin, cos и другие). Реализуйте этот метод как функцию высшего порядка, передавая ему в качестве аргумента функцию, вычисляющую ak по соответствующей рекуррентной формуле.
  • Создайте Windows-проект с классом Functions. Класс должен выполнять различные операции над функциями, принадлежащими функциональному типу, заданному делегатом. В число операций класса включите такие методы, как вычисление интеграла, нахождение максимума, минимума, корней функции на заданном интервале, вычисление значения производной функции в заданной точке. В классе-клиенте, использующем сервисы класса Fnctions, используйте анонимные методы и лямбда-выражения. В интерфейсе проекта предусмотрите возможность задания функции как выражением, так и выбором из фиксированного списка.
  • Создайте Windows-проект с классами Person и Sorting. В классе Sorting предусмотрите разные методы сортировки, заданные функциями высших порядков. Аргументом метода сортировки должна быть функция, позволяющая сравнивать персоны. В клиентском классе предусмотрите построение набора функций, позволяющих сравнивать персоны по разным критериям. Предусмотрите возможность задания таких функций лямбда-выражениями. В интерфейсе проекта предусмотрите возможность сравнения методов сортировки по времени. Постройте метод, вычисляющий время сортировки, как функцию высшего порядка, которой передается в качестве аргумента метод сортировки.
  • Вернуться к учебному плану