Добавить в цитаты Настройки чтения

Страница 322 из 371

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

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

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

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

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

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

Начнем примеры этой лекции с объявления трех делегатов. Поместив два из них в пространство имен, третий вложим непосредственно в создаваемый нами класс:

namespace Delegates {

//объявление классов — делегатов

delegate void Proc(ref int x);

delegate void MesToPers(string s);

class OwnDel

{

    public delegate int Fun1(int x);

    int Plus1(int x){return(x+100);}//Plus1

    int Minus1(int x){return(x-100);}//Minus1

    void Plus(ref int x){x+= 100;}

    void Minus(ref int x){x-=100;}

    //поля класса

    public Proc p1;

    public Fun1 f1;

    char sign;

    //конструктор

    public OwnDel(char sign)

    {

         this.sign = sign;

         if (sign == '+')

         {p1 = new Proc(Plus);f1 = new Fun1(Plus1);}

         else

         {p1 = new Proc(Minus);f1 = new Fun1(Minus1);}

     }

}//class OwnDel

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

• Первым делом объявлены три функциональных класса — три делегата: Proc, MesToPers, Fun1. Каждый из них описывает множество функций фиксированной сигнатуры.

• В классе OwnDel описаны четыре метода: Plus, Minus, Plus1, Minus1, сигнатуры которых соответствуют сигнатурам, задаваемых классами рrос и Fun1.

• Поля p1 и f1 класса OwnDel являются экземплярами классов рrос и Fun1.

• В конструкторе класса поля p1 и f1 связываются с конкретными методами Plus или Minus, Plus1 или Minus1. Связывание с той или иной функцией в данном случае определяется значением поля sign.

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

Приведу теперь процедуру, тестирующую работу созданного класса:

public void TestOwnDel()

{

     int account = 1000, account1=0;

     OwnDel oda = new OwnDel('+');

     Console.WriteLine("account = {0}, account1 = {1}",

            account, account1);

     oda.pl(ref account); account1=oda.f1 (account);





     Console.WriteLine("account = {0}, accountl = {1}",

            account, account1);

}

Клиент класса OwnDel создает экземпляр класса, передавая конструктору знак той операции, которую он хотел бы выполнить над своими счетами — account и account1. Вызов p1 и f1, связанных к моменту вызова с закрытыми методами класса, приводит к выполнению нужных функций.

В нашем примере объявление экземпляров делегатов и связывание их с внутренними методами класса происходило в самом классе. Клиенту оставалось лишь вызывать уже созданные экземпляры, но эту работу можно выполнять и на стороне клиентского класса, чем мы сейчас и займемся. Рассмотрим многократно встречавшийся класс Person, слегка изменив его определение:

class Person

{

    //конструкторы

    public Person(){name =""; id=0; salary=0.0;}

    public Person(string name){this.name = name;}

    public Person (string name, int id, double salary)

    {this.name = name; this.id=id; this.salary = salary;}

    public Person (Person pers)

    {this.name = pers.name; this.id = pers.id;

          this.salary = pers.salary;}

     //методы

     public void ToPerson(string mes)

     {

         this.message = mes;

         Console.WriteLine("{0}, {l}",name, message);

      }

      //свойства

      private string name;

      private int id;

      private double salary;

      private string message;

      //доступ к свойствам

      public string Name

      {get {return(name);} set {name = value;}}

      public double Salary

      {get {return(salary);} set {salary = value;}}

      public int Id

      {get {return(id);} set {id = value;}}

}//class Person

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

Person man2 = new Person("Владимир");

    MesToPers mestopers = new MesToPers(man2.ToPerson);

    mestopers("пора работать!");

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

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

Рис. 20.1. Объявление делегатов и создание их экземпляров

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

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