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

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

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

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

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

О полиморфизме говорилось достаточно много в предыдущих лекциях. Тем не менее позволю напомнить суть дела. Родитель может объявить виртуальный метод 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. Сортировка данных
< Лекция 6 || Лекция 7: 123456 || Лекция 8 >
Илья Ардов
Илья Ардов

Добрый день!

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

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