Тверской государственный университет
Опубликован: 02.12.2009 | Доступ: свободный | Студентов: 2376 / 262 | Оценка: 4.47 / 4.24 | Длительность: 14:45:00
Лекция 7:

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

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

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

Одно из наиболее важных применений делегатов связано с функциями высших порядков. Функцией высшего порядка называется такая функция (метод) класса, у которой один или несколько аргументов принадлежат к функциональному типу. Без этих функций в программировании обойтись довольно трудно. Один пример метода 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. Анонимные методы и делегаты
< Лекция 6 || Лекция 7: 123456 || Лекция 8 >
Илья Ардов
Илья Ардов

Добрый день!

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

Дарья Федотова
Дарья Федотова