Skip to content

Latest commit

 

History

History
2010 lines (1776 loc) · 121 KB

c15.md

File metadata and controls

2010 lines (1776 loc) · 121 KB

ГЛАВА 15. Делегаты, события и лямбда-выражения

В этой главе рассматриваются три новых средства С#: делегаты, события и лямбда-выражения. Делегат предоставляет возможность инкапсулировать метод, а событие уведомляет о том, что произошло некоторое действие. Делегаты и события тесно связаны друг с другом, поскольку событие основывается на делегате. Оба средства расширяют круг прикладных задача, решаемых при про­ граммировании на С#. А лямбда-выражение представляет собой новое синтаксическое средство, обеспечивающее упрощенный, но в то же время эффективный способ опре­ деления того, что по сути является единицей исполняемого кода. Лямбда-выражения обычно служат для работы с деле­ гатами и событиями, поскольку делегат может ссылаться на лямбда-выражение. (Кроме того, лямбда-выражения очень важны для языка LINQ, описываемого в главе 19.) В данной главе рассматриваются также анонимные методы, ковари­ антность, контравариантность и групповые преобразования методов.

Делегаты

Начнем с определения понятия делегата. Попросту го­ воря, делегат представляет собой объект, который может ссылаться на метод. Следовательно, когда создается делегат, то в итоге получается объект, содержащий ссылку на метод. Более того, метод можно вызывать по этой ссылке. Иными словами, делегат позволяет вызывать метод, на который он ссылается. Ниже будет показано, насколько действенным оказывается такой принцип.

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

ПРИМЕЧАНИЕ Если у вас имеется опыт программирования на C/C++, то вам полезно будет знать, что делегат в C# подобен указателю на функцию в C/C++.

Тип делегата объявляется с помощью ключевого слова delegate. Ниже приведена общая форма объявления делегата:

delegate возвращаемый_тип имя(список_параметров);

где возвращаемый_тип обозначает тип значения, возвращаемого методами, которые будут вызываться делегатом; имя — конкретное имя делегата; список_параметров — параметры, необходимые для методов, вызываемых делегатом. Как только будет соз­ дан экземпляр делегата, он может вызывать и ссылаться на те методы, возвращаемый тип и параметры которых соответствуют указанным в объявлении делегата.

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

Для того чтобы показать делегат в действии, рассмотрим для начала простой при­ мер его применения.

// Простой пример применения делегата.
using System;

// Объявить тип делегата.
delegate string StrMod(string str);

class DelegateTest {
    // Заменить пробелы дефисами.
    static string ReplaceSpaces(string s) {
        Console.WriteLine("Замена пробелов дефисами.");
        return s.Replace(' ', '-');
    }

    // Удалить пробелы.
    static string RemoveSpaces(string s) {
        string temp = "";
        int i;
        Console.WriteLine("Удаление пробелов.");
        for(i=0; i < s.Length; i++)
            if(s[i] != ' ') temp += s[i];
        return temp;
    }

    // Обратить строку.
    static string Reverse(string s) {
        string temp = "";
        int i, j;
        Console.WriteLine("Обращение строки.");
        for(j=0, i=s.Length-1; i >= 0; i--, j++)
            temp += s[i];
        return temp;
    }

    static void Main() {
        // Сконструировать делегат.
        StrMod strOp = new StrMod(ReplaceSpaces);
        string str;

        // Вызвать методы с помощью делегата.
        str = strOp("Это простой тест.");
        Console.WriteLine("Результирующая строка: " + str);
        Console.WriteLine();

        strOp = new StrMod(RemoveSpaces);
        str = strOp("Это простой тест.");
        Console.WriteLine("Результирующая строка: " + str);
        Console.WriteLine();

        strOp = new StrMod(Reverse);
        str = strOp("Это простой тест.");
        Console.WriteLine("Результирующая строка: " + str);
    }
}

Вот к какому результату приводит выполнение этого кода.

Замена пробелов дефисами.
Результирующая строка: Это-простой-тест.

Удаление пробелов.
Результирующая строка: Этопростойтест.

Обращение строки.
Результирующая строка: .тсет йотсорп отЭ

Рассмотрим данный пример более подробно. В его коде сначала объявляется деле­ гат StrMod типа string, как показано ниже.

delegate string StrMod(string str);

Как видите, делегат StrMod принимает один параметр типа string и возвращает одно значение того же типа.

Далее в классе DelegateTest объявляются три статических метода с одним пара­ метром типа string и возвращаемым значением того же типа. Следовательно, они со­ ответствуют делегату StrMod. Эти методы видоизменяют строку в той или иной фор­ ме. Обратите внимание на то, что в методе ReplaceSpaces() для замены пробелов дефисами используется один из методов типа string — Replace().

В методе Main() создается переменная экземпляра strOp ссылочного типа StrMod и затем ей присваивается ссылка на метод ReplaceSpaces(). Обратите особое внима­ ние на следующую строку кода.

StrMod strOp = new StrMod(ReplaceSpaces);

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

Далее метод ReplaceSpaces() вызывается с помощью экземпляра делегата strOp, как показано ниже.

str = strOp("Это простой тест.");

Экземпляр делегата strOp ссылается на метод ReplaceSpaces(), и поэтому вы­ зывается именно этот метод.

Затем экземпляру делегата strOp присваивается ссылка на метод RemoveSpaces(), и с его помощью вновь вызывается указанный метод — на этот раз RemoveSpaces(). И наконец, экземпляру делегата strOp присваивается ссылка на метод Reverse(). А в итоге вызывается именно этот метод.

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

Групповое преобразование делегируемых методов

Еще в версии C# 2.0 было внедрено специальное средство, существенно упрощаю­ щее синтаксис присваивания метода делегату. Это так называемое групповое преобразо­ вание методов, позволяющее присвоить имя метода делегату, не прибегая к оператору new или явному вызову конструктора делегата.

Ниже приведен метод Main() из предыдущего примера, измененный с целью про­ демонстрировать групповое преобразование методов.

static void Main() {
    // Сконструировать делегат, используя групповое преобразование методов.
    StrMod strOp = ReplaceSpaces; // использовать групповое преобразование методов
    string str;

    // Вызвать методы с помощью делегата,
    str = strOp("Это простой тест.");
    Console.WriteLine("Результирующая строка: " + str);
    Console.WriteLine();

    strOp = RemoveSpaces; // использовать групповое преобразование методов
    str = strOp("Это простой тест.");
    Console.WriteLine("Результирующая строка: " + str);
    Console.WriteLine();

    strOp = Reverse; // использовать групповое преобразование методов
    str = strOp("Это простой тест.");
    Console.WriteLine("Результирующая строка: " + str);
    Console.WriteLine();
}

Обратите особое внимание на то, как создается экземпляр делегата strOp и как ему присваивается метод ReplaceSpaces в следующей строке кода.

strOp = RemoveSpaces; // использовать групповое преобразование методов

В этой строке кода имя метода присваивается непосредственно экземпляру деле­ гата strOp, а все заботы по автоматическому преобразованию метода в тип делегата "возлагаются" на средства С#. Этот синтаксис может быть распространен на любую ситуацию, в которой метод присваивается или преобразуется в тип делегата.

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

Применение методов экземпляра в качестве делегатов

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

// Делегаты могут ссылаться и на методы экземпляра.
using System;

// Объявить тип делегата.
delegate string StrMod(string str);

class StringOps {
    // Заменить пробелы дефисами.
    public string ReplaceSpaces (string s) {
        Console.WriteLine("Замена пробелов дефисами.");
        return s.Replace(' ', '-');
    }

    // Удалить пробелы.
    public string RemoveSpaces(string s) {
        string temp = "";
        int i;
        Console.WriteLine("Удаление пробелов.");
        for(i=0; i < s.Length; i++)
            if(s[i] != ' ') temp += s[i];
        return temp;
    }

    // Обратить строку.
    public string Reverse(string s) {
        string temp = "";
        int i, j;
        Console.WriteLine("Обращение строки.");
        for(j=0, i=s.Length-1; i >= 0; i--, j++)
            temp += s[i];
        return temp;
    }
}

class DelegateTest {
    static void Main() {
        StringOps so = new StringOps(); // создать экземпляр
        // объекта класса StringOps
        // Инициализировать делегат.
        StrMod strOp = so.ReplaceSpaces;
        string str;

        // Вызвать методы с помощью делегатов.
        str = strOp("Это простой тест.");
        Console.WriteLine("Результирующая строка: " + str);
        Console.WriteLine();

        strOp = so.RemoveSpaces;
        str = strOp("Это простой тест.");
        Console.WriteLine("Результирующая строка: " + str);
        Console.WriteLine();

        strOp = so.Reverse;
        str = strOp("Это простой тест.");
        Console.WriteLine("Результирующая строка: " + str);
    }
}

Результат выполнения этого кода получается таким же, как и в предыдущем при­ мере, но на этот раз делегат обращается к методам по ссылке на экземпляр объекта класса StringOps.

Групповая адресация

Одним из самых примечательных свойств делегата является поддержка групповой адресации. Попросту говоря, групповая адресация — это возможность создать список, или цепочку вызовов, для методов, которые вызываются автоматически при обращении к делегату. Создать такую цепочку нетрудно. Для этого достаточно получить экзем­ пляр делегата, а затем добавить методы в цепочку с помощью оператора + или +=. Для удаления метода из цепочки служит оператор - или -=. Если делегат возвращает значение, то им становится значение, возвращаемое последним методом в списке вы­ зовов. Поэтому делегат, в котором используется групповая адресация, обычно имеет возвращаемый тип void.

Ниже приведен пример групповой адресации. Это переработанный вариант предыдущих примеров, в котором тип значений, возвращаемых методами манипули­ рования строками, изменен на void, а для возврата измененной строки в вызывающую часть кода служит параметр типа ref. Благодаря этому методы оказываются более приспособленными для групповой адресации.

// Продемонстрировать групповую адресацию.
using System;

// Объявить тип делегата.
delegate void StrMod(ref string str);

class MultiCastDemo {
    // Заменить пробелы дефисами.
    static void ReplaceSpaces(ref string s) {
        Console.WriteLine("Замена пробелов дефисами.");
        s = s.Replace(' ', '-');
    }

    // Удалить пробелы.
    static void RemoveSpaces(ref string s) {
        string temp = "";
        int i;
        Console.WriteLine("Удаление пробелов.");
        for(i=0; i < s.Length; i++)
            if(s[i] != ' ') temp += s[i];
        s = temp;
    }

    // Обратить строку.
    static void Reverse(ref string s) {
        string temp = "";
        int i, j;
        Console.WriteLine("Обращение строки.");
        for(j=0, i=s.Length-1; i >= 0; i--, j++)
            temp += s[i];
        s = temp;
    }

    static void Main() {
        // Сконструировать делегаты.
        StrMod strOp;
        StrMod replaceSp = ReplaceSpaces;
        StrMod removeSp = RemoveSpaces;
        StrMod reverseStr = Reverse;
        string str = "Это простой тест.";

        // Организовать групповую адресацию.
        strOp = replaceSp;
        strOp += reverseStr;

        // Обратиться к делегату с групповой адресацией.
        strOp(ref str);
        Console.WriteLine("Результирующая строка: " + str);
        Console.WriteLine();

        // Удалить метод замены пробелов и добавить метод удаления пробелов.
        strOp -= replaceSp;
        strOp += removeSp;
        str = "Это простой тест."; // восстановить исходную строку

        // Обратиться к делегату с групповой адресацией.
        strOp (ref str);
        Console.WriteLine("Результирующая строка: " + str);
        Console.WriteLine();
    }
}

Выполнение этого кода приводит к следующему результату.

Замена пробелов дефисами.
Обращение строки.
Результирующая строка: .тсет-йотсорп-отЭ

Обращение строки.
Удаление пробелов.
Результирующая строка: .тсетйотсорпотЭ

В методе Main() из рассматриваемого здесь примера кода создаются четыре экзем­ пляра делегата. Первый из них, strOp, является пустым, а три остальных ссылаются на конкретные методы видоизменения строки. Затем организуется групповая адресация для вызова методов RemoveSpaces() и Reverse(). Это делается в приведенных ниже строках кода.

strOp = replaceSp;
strOp += reverseStr

Сначала делегату strOp присваивается ссылка replaceSp, а затем с помощью опе­ ратора += добавляется ссылка reverseStr. При обращении к делегату strOp вызы­ ваются оба метода, заменяя пробелы дефисами и обращая строку, как и показывает приведенный выше результат.

Далее ссылка replaceSp удаляется из цепочки вызовов в следующей строке кода:

strOp -= replaceSp;

и добавляется ссылка removeSp в строке кода.

strOp += removeSp;

После этого вновь происходит обращение к делегату strOp. На этот раз обращает­ ся строка с удаленными пробелами.

Цепочки вызовов являются весьма эффективным механизмом, поскольку они по­ зволяют определить ряд методов, выполняемых единым блоком. Благодаря этому улучшается структура некоторых видов кода. Кроме того, цепочки вызовов имеют осо­ бое значение для обработки событий, как станет ясно в дальнейшем.

Ковариантность и контравариантность

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

Ниже приведен пример, демонстрирующий ковариантность и контравариант­ ность.

// Продемонстрировать ковариантность и контравариантность.
using System;

class X {
    public int Val;
}

// Класс Y, производный от класса X.
class Y : X { }

// Этот делегат возвращает объект класса X и
// принимает объект класса Y в качестве аргумента.
delegate X ChangeIt(Y obj);

class CoContraVariance {
    // Этот метод возвращает объект класса X и
    // имеет объект класса X в качестве параметра.
    static X IncrA(X obj) {
        X temp = new X();
        temp.Val = obj.Val + 1;
        return temp;
    }

    // Этот метод возвращает объект класса Y и
    // имеет объект класса Y в качестве параметра.
    static Y IncrB(Y obj) {
        Y temp = new Y();
        temp.Val = obj.Val + 1;
        return temp;
    }

    static void Main() {
        Y Yob = new Y();
        // В данном случае параметром метода IncrA является объект класса X,
        // а параметром делегата ChangeIt — объект класса Y. Но благодаря
        // контравариантности следующая строка кода вполне допустима.
        Changelt change = IncrA;
        X Xob = change(Yob);
        Console.WriteLine("Xob: " + Xob.Val);

        // В этом случае возвращаемым типом метода IncrB служит объект класса Y,
        // а возвращаемым типом делегата ChangeIt — объект класса X. Но благодаря
        // ковариантности следующая строка кода оказывается вполне допустимой.
        change = IncrB;
        Yob = (Y) change (Yob);
        Console.WriteLine("Yob: " + Yob.Val);
    }
}

Вот к какому результату приводит выполнение этого кода.

Xob: 1
Yob: 1

В данном примере класс Y является производным от класса X. А делегат ChangeIt объявляется следующим образом.

delegate X ChangeIt(Y obj);

Делегат возвращает объект класса X и принимает в качестве параметра объект клас­ са Y. А методы IncrA() и IncrB() объявляются следующим образом.

static X IncrA(X obj)
static Y IncrB(Y obj)

Метод IncrA() принимает объект класса X в качестве параметра и возвращает объект того же класса. А метод IncrB() принимает в качестве параметра объект клас­ са Y и возвращает объект того же класса. Но благодаря ковариантности и контравари­ антности любой из этих методов может быть передан делегату ChangeIt, что и демон­ стрирует рассматриваемый здесь пример.

Таким образом, в строке

ChangeIt change = IncrA;

метод IncrA() может быть передан делегату благодаря контравариантности, так как объект класса X служит в качестве параметра метода IncrA(), а объект класса Y — в качестве параметра делегата ChangeIt. Но метод и делегат оказываются совмести­ мыми в силу контравариантности, поскольку типом параметра метода, передаваемого делегату, служит класс, являющийся базовым для класса, указываемого в качестве типа параметра делегата.

Приведенная ниже строка кода также является вполне допустимой, но на этот раз благодаря ковариантности.

change = IncrB;

В данном случае возвращаемым типом для метода IncrB() служит класс Y, а для делегата — класс X. Но поскольку возвращаемый тип метода является производным классом от возвращаемого типа делегата, то оба оказываются совместимыми в силу ковариантности.

Класс System.Delegate

Все делегаты и классы оказываются производными неявным образом от класса System.Delegate. Как правило, членами этого класса не пользуются непосред­ ственно, и это не делается явным образом в данной книге. Но члены класса System. Delegate могут оказаться полезными в ряде особых случаев.

Назначение делегатов

В предыдущих примерах был наглядно продемонстрирован внутренний механизм действия делегатов, но эти примеры не показывают их истинное назначение. Как пра­ вило, делегаты применяются по двум причинам. Во-первых, как упоминалось ранее в этой главе, делегаты поддерживают события. И во-вторых, делегаты позволяют вы­ зывать методы во время выполнения программы, не зная о них ничего определенно­ го в ходе компиляции. Это очень удобно для создания базовой конструкции, допу­ скающей подключение отдельных программных компонентов. Рассмотрим в качестве примера графическую программу, аналогичную стандартной сервисной программе Windows Paint. С помощью делегата можно предоставить пользователю возможность подключать специальные цветные фильтры или анализаторы изображений. Кроме того, пользователь может составлять из этих фильтров или анализаторов целые по­ следовательности. Подобные возможности программы нетрудно обеспечить, исполь­ зуя делегаты.

Анонимные функции

Метод, на который ссылается делегат, нередко используется только для этой цели. Иными словами, единственным основанием для существования метода служит то об­ стоятельство, что он может быть вызван посредством делегата, но сам он не вызывается вообще. В подобных случаях можно воспользоваться анонимной функцией, чтобы не создавать отдельный метод. Анонимная функция, по существу, представляет собой безымянный кодовый блок, передаваемый конструктору делегата. Преимущество ано­ нимной функции состоит, в частности, в ее простоте. Благодаря ей отпадает необходи­ мость объявлять отдельный метод, единственное назначение которого состоит в том, что он передается делегату.

Начиная с версии 3.0, в C# предусмотрены две разновидности анонимных функ­ ций: анонимные методы и лямбда-выражения. Анонимные методы были внедрены в C# еще в версии 2.0, а лямбда-выражения — в версии 3.0. В целом лямбда-выражение со­ вершенствует принцип действия анонимного метода и в настоящее время считается более предпочтительным для создания анонимной функции. Но анонимные методы широко применяются в существующем коде С# и поэтому по-прежнему являются важной составной частью С#. А поскольку анонимные методы предшествовали по­ явлению лямбда-выражений, то ясное представление о них позволяет лучше понять особенности лямбда-выражений. К тому же анонимные методы могут быть использо­ ваны в целом ряде случаев, где применение лямбда-выражений оказывается невозмож­ ным. Именно поэтому в этой главе рассматриваются и анонимные методы, и лямбда- выражения.

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

Анонимный метод — один из способов создания безымянного блока кода, связан­ ного с конкретным экземпляром делегата. Для создания анонимного метода достаточ­ но указать кодовый блок после ключевого слова delegate. Покажем, как это делается, на конкретном примере. В приведенной ниже программе анонимный метод служит для подсчета от 0 до 5.

// Продемонстрировать применение анонимного метода.
using System;

// Объявить тип делегата.
delegate void CountIt();

class AnonMethDemo {
    static void Main() {
        // Далее следует код для подсчета чисел, передаваемый делегату
        // в качестве анонимного метода.
        CountIt count = delegate {
            // Этот кодовый блок передается делегату.
            for(int i=0; i <= 5; i++)
                Console.WriteLine(i);
        }; // обратите внимание на точку с запятой
        count();
    }
}

В данной программе сначала объявляется тип делегата CountIt без параметров и с возвращаемым типом void. Далее в методе Main() создается экземпляр count делегата CountIt, которому передается кодовый блок, следующий после ключевого слова delegate. Именно этот кодовый блок и является анонимным методом, кото­ рый будет выполняться при обращении к делегату count. Обратите внимание на то, что после кодового блока следует точка с запятой, фактически завершающая оператор объявления. Ниже приведен результат выполнения данной программы.

0
1
2
3
4
5

Передача аргументов анонимному методу

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

// Продемонстрировать применение анонимного метода, принимающего аргумент.
using System;

// Обратите внимание на то, что теперь у делегата CountIt имеется параметр.
delegate void CountIt (int end);

class AnonMethDemo2 {
    static void Main() {

        // Здесь конечное значение для подсчета передается анонимному методу.
        CountIt count = delegate (int end) {
            for(int i=0; i <= end; i++)
            Console.WriteLine(i);
        };

        count(3);
        Console.WriteLine();
        count(5);
    }
}

В этом варианте программы делегат CountIt принимает целочисленный аргумент. Обратите внимание на то, что при создании анонимного метода список параметров указывается после ключевого слова delegate. Параметр end становится доступным для кода в анонимном методе таким же образом, как и при создании именованного метода. Ниже приведен результат выполнения данной программы.

0
1
2
3
0
1
2
3
4
5

Возврат значения из анонимного метода

Анонимный метод может возвращать значение. Для этой цели служит оператор return, действующий в анонимном методе таким же образом, как и в именованном методе. Как и следовало ожидать, тип возвращаемого значения должен быть совме­ стим с возвращаемым типом, указываемым в объявлении делегата. В качестве при­ мера ниже приведен код, выполняющий подсчет с суммированием и возвращающий результат.

// Продемонстрировать применение анонимного метода, возвращающего значение.
using System;

// Этот делегат возвращает значение.
delegate int CountIt(int end);

class AnonMethDemo3 {
    static void Main() {
        int result;
        // Здесь конечное значение для подсчета перелается анонимному методу.
        // А возвращается сумма подсчитанных чисел.
        CountIt count = delegate (int end) {
            int sum = 0;
            for(int i=0; i <= end; i++) {
                Console.WriteLine (i);
                sum += i;
            }
            return sum; // возвратить значение из анонимного метода
        };
        result = count(3);
        Console.WriteLine("Сумма 3 равна " + result);
        Console.WriteLine();

        result = count (5);
        Console.WriteLine("Сумма 5 равна " + result);
    }
}

В этом варианте кода суммарное значение возвращается кодовым блоком, связан­ ным с экземпляром делегата count. Обратите внимание на то, что оператор return применяется в анонимном методе таким же образом, как и в именованном методе. Ниже приведен результат выполнения данного кода.

0
1
2
3
Сумма 3 равна 6

0
1
2
3
4
5
Сумма 5 равна 15

Применение внешних переменных в анонимных методах

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

Захват локальной переменной может привести к неожиданным результатам. В ка­ честве примера рассмотрим еще один вариант программы подсчета с суммированием чисел. В данном варианте объект CountIt конструируется и возвращается статическим методом Counter(). Этот объект использует переменную sum, объявленную в охваты­ вающей области действия метода Counter(), а не самого анонимного метода. Поэто­ му переменная sum захватывается анонимным методом. Метод Counter() вызывается в методе Main() для получения объекта CountIt, а следовательно, переменная sum не уничтожается до самого конца программы.

// Продемонстрировать применение захваченной переменной.
using System;

// Этот делегат возвращает значение типа int и принимает аргумент типа int.
delegate int CountIt(int end);

class VarCapture {
    static CountIt Counter() {
        int sum = 0;
        // Здесь подсчитанная сумма сохраняется в переменной sum.
        CountIt ctObj = delegate (int end) {
            for(int i=0; i <= end; i++) {
                Console.WriteLine(i);
                sum += i;
            }
            return sum;
        };
        return ctObj;
    }

    static void Main() {
        // Получить результат подсчета.
        CountIt count = Counter();

        int result;

        result = count(3);
        Console.WriteLine("Сумма 3 равна " + result);
        Console.WriteLine();

        result = count (5);
        Console.WriteLine("Сумма 5 равна " + result);
    }
}

Ниже приведен результат выполнения этой программы. Обратите особое внима­ ние на суммарное значение.

0
1
2
3
Сумма 3 равна 6

0
1
2
3
4
5
Сумма 5 равна 21

Как видите, подсчет по-прежнему выполняется как обычно. Но обратите внимание на то, что сумма 5 теперь равна 21, а не 15! Дело в том, что переменная sum захваты­ вается объектом ctObj при его создании в методе Counter(). Это означает, что она продолжает существовать вплоть до уничтожения делегата count при "сборке мусо­ ра" в самом конце программы. Следовательно, ее значение не уничтожается после воз­ врата из метода Counter() или при каждом вызове анонимного метода, когда проис­ ходит обращение к делегату count в методе Main().

Несмотря на то что применение захваченных переменных может привести к до­ вольно неожиданным результатам, как в приведенном выше примере, оно все же логически обоснованно. Ведь когда анонимный метод захватывает переменную, она продолжает существовать до тех пор, пока используется захватывающий ее делегат. В противном случае захваченная переменная оказалась бы неопределенной, когда она могла бы потребоваться делегату.

Лямбда-выражения

Несмотря на всю ценность анонимных методов, им на смену пришел более со­ вершенный подход: лямбда-выражение. Не будет преувеличением сказать, что лямбда- выражение относится к одним из самых важных нововведений в С#, начиная с выпуска исходной версии 1.0 этого языка программирования. Лямбда-выражение основывается на совершенно новом синтаксическом элементе и служит более эффективной альтер­ нативой анонимному методу. И хотя лямбда-выражения находят применение главным образом в работе с LINQ (подробнее об этом — в главе 19), они часто используются и вместе с делегатами и событиями. Именно об этом применении лямбда-выражений и пойдет речь в данном разделе.

Лямбда-выражение — это другой собой создания анонимной функции. (Первый ее способ, анонимный метод, был рассмотрен в предыдущем разделе.) Следовательно, лямбда-выражение может быть присвоено делегату. А поскольку лямбда-выражение считается более эффективным, чем эквивалентный ему анонимный метод то в боль­ шинстве случаев рекомендуется отдавать предпочтение именно ему.

Лямбда-оператор

Во всех лямбда-выражениях применяется новый лямбда-оператор =>, который раз­ деляет лямбда-выражение на две части. В левой его части указывается входной пара­ метр (или несколько параметров), а в правой части — тело лямбда-выражения. Опера­ тор => иногда описывается такими словами, как "переходит" или "становится".

В C# поддерживаются две разновидности лямбда-выражений в зависимости от тела самого лямбда-выражения. Так, если тело лямбда-выражения состоит из одного вы­ ражения, то образуется одиночное лямбда-выражение. В этом случае тело выражения не заключается в фигурные скобки. Если же тело лямбда-выражения состоит из блока операторов, заключенных в фигурные скобки, то образуется блочное лямбда-выражение. При этом блочное лямбда-выражение может содержать целый ряд операторов, в том числе циклы, вызовы методов и условные операторы if. Обе разновидности лямбда- выражений рассматриваются далее по отдельности.

Одиночные лямбда-выражения

В одиночном лямбда-выражении часть, находящаяся справа от оператора =>, воз­ действует на параметр (или ряд параметров), указываемый слева. Возвращаемым результатом вычисления такого выражения является результат выполнения лямбда- оператора.

Ниже приведена общая форма одиночного лямбда-выражения, принимающего единственный параметр.

параметр => выражение

Если же требуется указать несколько параметров, то используется следующая форма.

(список_параметров) => выражение

Таким образом, когда требуется указать два параметра или более, их следует за­ ключить в скобки. Если же выражение не требует параметров, то следует использовать пустые скобки.

Ниже приведен простой пример одиночного лямбда-выражения.

count- => count + 2

В этом выражении count служит параметром, на который воздействует выраже­ ние count + 2. В итоге значение параметра count увеличивается на 2. А вот еще один пример одиночного лямбда-выражения.

n => n % 2 == 0

В данном случае выражение возвращает логическое значение true, если числовое значение параметра n оказывается четным, а иначе — логическое значение false.

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

В приведенном ниже примере программы демонстрируется применение двух оди­ ночных лямбда-выражений. Сначала в этой программе объявляются два типа делега­ тов. Первый из них, Incr, принимает аргумент типа int и возвращает результат того же типа. Второй делегат, IsEven, также принимает аргумент типа int, но возвращает результат типа bool. Затем экземплярам этих делегатов присваиваются одиночные лямбда-выражения. И наконец, лямбда-выражения вычисляются с помощью соответ­ ствующих экземпляров делегатов.

// Применить два одиночных лямбда-выражения.
using System;

// Объявить делегат, принимающий аргумент типа int и
// возвращающий результат типа int.
delegate int Incr(int v);

// Объявить делегат, принимающий аргумент типа int и
// возвращающий результат типа bool.
delegate bool IsEven(int v);

class SimpleLambdaDemo {
    static void Main() {
        // Создать делегат Incr, ссылающийся на лямбда-выражение.
        // увеличивающее свой параметр на 2.
        Incr incr = count => count + 2;

        // А теперь использовать лямбда-выражение incr.
        Console.WriteLine("Использование лямбда-выражения incr: ");
        int x = -10;
        while(x <= 0) {
            Console.Write(x + " ");
            x = incr(x); // увеличить значение x на 2
        }

        Console.WriteLine ("\n");

        // Создать экземпляр делегата IsEven, ссылающийся на лямбда-выражение,
        // возвращающее логическое значение true, если его параметр имеет четное
        // значение, а иначе — логическое значение false.
        IsEven isEven = n => n % 2 == 0;

        // А теперь использовать лямбда-выражение isEven.
        Console.WriteLine("Использование лямбда-выражения isEven: ");
        for(int i=l; i <= 10; i++)
            if(isEven(i)) Console.WriteLine(i + " четное.");
    }
}

Вот к какому результату приводит выполнение этой программы.

Использование лямбда-выражения incr:
-10 -8 -6 -4 -2 0

Использование лямбда-выражения isEven:
2 четное.
4 четное.
6 четное.
8 четное.
10 четное.

Обратите в данной программе особое внимание на следующие строки объявлений.

Incr incr = count => count + 2;
IsEven isEven = n => n % 2 == 0;

В первой строке объявления экземпляру делегата incr присваивается одиночное лямбда-выражение, возвращающее результат увеличения на 2 значения параметра count. Это выражение может быть присвоено делегату Incr, поскольку оно совмести­ мо с объявлением данного делегата. Аргумент, указываемый при обращении к экзем­ пляру делегата incr, передается параметру count, который и возвращает результат вычисления лямбда-выражения. Во второй строке объявления делегату isEven при­ сваивается выражение, возвращающее логическое значение true, если передаваемый ему аргумент оказывается четным, а иначе — логическое значение false. Следователь­ но, это лямбда-выражение совместимо с объявлением делегата IsEven.

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

Несмотря на всю полезность логического заключения о типе данных, в некоторых случаях приходится явно указывать тип параметра лямбда-выражения. Для этого до­ статочно ввести конкретное название типа данных. В качестве примера ниже приведен другой способ объявления экземпляра делегата incr.

Incr incr = (int count) => count + 2;

Как видите, count теперь явно объявлен как параметр типа int. Обратите также внимание на использование скобок. Теперь они необходимы. (Скобки могут быть опу­ щены только в том случае, если задается лишь один параметр, а его тип явно не ука­ зывается.)

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

(low, high, val) => val >= low && val <= high;

А вот как объявляется тип делегата, совместимого с этим лямбда-выражением.

delegate bool InRange(int lower, int upper, int v);

Следовательно, экземпляр делегата InRange может быть создан следующим об­ разом.

InRange rangeOK = (low, high, val) => val >= low && val <= high;

После этого одиночное лямбда-выражение может быть выполнено так, как показа­ но ниже.

if(rangeOK(1, 5, 3)) Console.WriteLine(
                            "Число 3 находится в пределах от 1 до 5.");

И последнее замечание: внешние переменные могут использоваться и захватывать­ ся в лямбда-выражениях таким же образом, как и в анонимных методах.

Блочные лямбда-выражения

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

Ниже приведен пример использования блочного лямбда-выражения для вычисле­ ния и возврата факториала целого значения.

// Продемонстрировать применение блочного лямбда-выражения.
using System;

// Делегат IntOp принимает один аргумент типа int
// и возвращает результат типа int.
delegate int IntOp(int end);

class StatementLambdaDemo {
    static void Main() {
        // Блочное лямбда-выражение возвращает факториал
        // передаваемого ему значения.
        IntOp fact = n => {
                        int r = 1;
                        for(int i=1; i <= n; i++)
                            r = i * r;
                        return r;
        };
        Console.WriteLine("Факториал 3 равен " + fact(3));
        Console.WriteLine("Факториал 5 равен " + fact(5));
    }
}

При выполнении этого кода получается следующий результат.

Факториал 3 равен 6
Факториал 5 равен 120

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

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

// Первый пример применения делегатов, переделанный с
// целью использовать блочные лямбда-выражения.
using System;

// Объявить тип делегата.
delegate string StrMod(string s);

class UseStatementLambdas {
    static void Main() {
        // Создать делегаты, ссылающиеся на лямбда- выражения,
        // выполняющие различные операции с символьными строками.
        // Заменить пробелы дефисами.
        StrMod ReplaceSpaces = s => {
            Console.WriteLine("Замена пробелов дефисами.");
            return s.Replace(' ', '-');
        };

        // Удалить пробелы.
        StrMod RemoveSpaces = s => {
            string temp =
            int i;

            Console.WriteLine("Удаление пробелов.");
            for(i=0; i < s.Length; i++)
                if (s[i] != ' ') temp += s[i];
            return temp;
        };

        // Обратить строку.
        StrMod Reverse = s => {
            string temp = "";
            int i, j;

            Console.WriteLine("Обращение строки.");
            for(j=0, i=s.Length-1; i >= 0; i--, j++)
                temp += s [i];
            return temp;
        };

        string str;
        // Обратиться к лямбда-выражениям с помощью делегатов.
        StrMod strOp = ReplaceSpaces;
        str = strOp("Это простой тест.");
        Console.WriteLine("Результирующая строка: " + str);
        Console.WriteLine();

        strOp = RemoveSpaces;
        str = strOp("Это простой тест.");
        Console.WriteLine("Результирующая строка: " + str);
        Console.WriteLine();

        strOp = Reverse;
        str = strOp("Это простой тест.");
        Console.WriteLine("Результирующая строка: " + str);
    }
}

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

Замена пробелов дефисами.
Результирующая строка: Это-простой-тест.

Удаление пробелов.
Результирующая строка: Этопростойтест.

Обращение строки.
Результирующая строка: .тсет йотсорп отЭ

События

Еще одним важным средством С#, основывающимся на делегатах, является собы­ тие. Событие, по существу, представляет собой автоматическое уведомление о том, что произошло некоторое действие. События действуют по следующему принципу: объект, проявляющий интерес к событию, регистрирует обработчик этого события. Когда же событие происходит, вызываются все зарегистрированные обработчики это­ го события. Обработчики событий обычно представлены делегатами.

События являются членами класса и объявляются с помощью ключевого слова event. Чаще всего для этой цели используется следующая форма:

event делегат_события имя_события;

где делегат_события обозначает имя делегата, используемого для поддержки собы­ тия, а имя_события — конкретный объект объявляемого события.

Рассмотрим для начала очень простой пример.

// Очень простой пример, демонстрирующий событие.
using System;

// Объявить тип делегата для события.
delegate void MyEventHandler();

// Объявить класс, содержащий событие.
class MyEvent {
    public event MyEventHandler SomeEvent;
    // Этот метод вызывается для запуска события.
    public void OnSomeEvent() {
        if(SomeEvent != null)
            SomeEvent();
    }
}

class EventDemo {
    // Обработчик события.
    static void Handler() {
        Console.WriteLine("Произошло событие");
    }

    static void Main() {
        MyEvent evt = new MyEvent();

        // Добавить метод Handler() в список событий.
        evt.SomeEvent += Handler;

        // Запустить событие.
        evt.OnSomeEvent();
    }
}

Вот какой результат получается при выполнении этого кода.

Произошло событие

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

delegate void MyEventHandler();

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

Далее создается класс события MyEvent. В этом классе объявляется событие SomeEvent в следующей строке кода.

public event MyEventHandler SomeEvent;

Обратите внимание на синтаксис этого объявления. Ключевое слово event уведом­ ляет компилятор о том, что объявляется событие.

Кроме того, в классе MyEvent объявляется метод OnSomeEvent(), вызываемый для сигнализации о запуске события. Это означает, что он вызывается, когда происходит событие. В методе OnSomeEvent() вызывается обработчик событий с помощью деле­ гата SomeEvent.

if(SomeEvent != null)
    SomeEvent();

Как видите, обработчик вызывается лишь в том случае, если событие SomeEvent не является пустым. А поскольку интерес к событию должен быть зарегистрирован в дру­ гих частях программы, чтобы получать уведомления о нем, то метод OnSomeEvent() может быть вызван до регистрации любого обработчика события. Но во избежание вызова по пустой ссылке делегат события должен быть проверен, чтобы убедиться в том, что он не является пустым.

В классе EventDemo создается обработчик событий Handler(). В данном простом примере обработчик событий просто выводит сообщение, но другие обработчики могут выполнять более содержательные функции. Далее в методе Main() создается объект класса события MyEvent, a Handler() регистрируется как обработчик этого события, добавляемый в список.

MyEvent evt = new MyEvent();

// Добавить метод Handler() в список событий.
evt.SomeEvent += Handler;

Обратите внимание на то, что обработчик добавляется в список с помощью опера­ тора +=. События поддерживают только операторы += и -=. В данном случае метод Handler() является статическим, но в качестве обработчиков событий могут также служить методы экземпляра.

И наконец, событие запускается, как показано ниже.

// Запустить событие.
evt.OnSomeEvent();

Вызов метода OnSomeEvent() приводит к вызову всех событий, зарегистрирован­ ных обработчиком. В данном случае зарегистрирован только один такой обработчик, но их может быть больше, как поясняется в следующем разделе.

Пример групповой адресации события

Как и делегаты, события поддерживают групповую адресацию. Это дает возмож­ ность нескольким объектам реагировать на уведомление о событии. Ниже приведен пример групповой адресации события.

// Продемонстрировать групповую адресацию события.
using System;

// Объявить тип делегата для события.
delegate void MyEventHandler();

// Объявить делегат, содержащий событие.
class MyEvent {
    public event MyEventHandler SomeEvent;

    // Этот метод вызывается для запуска события.
    public void OnSomeEvent() {
        if(SomeEvent != null)
           SomeEvent();
    }
}

class X {
    public void Xhandler() {
        Console.WriteLine("Событие получено объектом класса X");
    }
}

class Y {
    public void Yhandler() {
       Console.WriteLine("Событие получено объектом класса Y");
    }
}

class EventDemo2 {
    static void Handler() {
        Console.WriteLine("Событие получено объектом класса EventDemo");
    }

    static void Main() {
        MyEvent evt = new MyEvent();
        X xOb = new X();
        Y yOb = new Y();

        // Добавить обработчики в список событий.
        evt.SomeEvent += Handler;
        evt.SomeEvent += xOb.Xhandler;
        evt.SomeEvent += yOb.Yhandler;

        // Запустить событие.
        evt.OnSomeEvent();
        Console.WriteLine();

        // Удалить обработчик.
        evt.SomeEvent -= xOb.Xhandler;
        evt.OnSomeEvent();
    }
}

При выполнении кода этого примера получается следующий результат.

Событие получено объектом класса EventDemo
Событие получено объектом класса X
Событие получено объектом класса Y

Событие получено объектом класса EventDemo
Событие получено объектом класса Y

В данном примере создаются два дополнительных класса, X и Y, в которых также определяются обработчики событий, совместимые с делегатом MyEventHandler. Поэтому эти обработчики могут быть также включены в цепочку событий. Обратите внимание на то, что обработчики в классах X и Y не являются статическими. Это озна­ чает, что сначала должны быть созданы объекты каждого из этих классов, а затем в цепочку событий должны быть введены обработчики, связанные с их экземплярами. Об отличиях между обработчиками экземпляра и статическими обработчиками речь пойдет в следующем разделе.

Методы экземпляра в сравнении со статическими методами в качестве обработчиков событий

Методы экземпляра и статические методы могут быть использованы в качестве обработчиков событий, но между ними имеется одно существенное отличие. Когда статический метод используется в качестве обработчика, уведомление о событии рас­ пространяется на весь класс. А когда в качестве обработчика используется метод эк­ земпляра, то события адресуются конкретным экземплярам объектов. Следовательно, каждый объект определенного класса, которому требуется получить уведомление о со­ бытии, должен быть зарегистрирован отдельно. На практике большинство обработ­ чиков событий представляет собой методы экземпляра, хотя это, конечно, зависит от конкретного приложения. Рассмотрим применение каждой из этих двух разновидно­ стей методов в качестве обработчиков событий на конкретных примерах.

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

/* Уведомления о событиях получают отдельные объекты, когда метод экземпляра
используется в качестве обработчика событий. */
using System;

// Объявить тип делегата для события.
delegate void MyEventHandler();

// Объявить класс, содержащий событие.
class MyEvent {
    public event MyEventHandler SomeEvent;

    // Этот метод вызывается для запуска события.
    public void OnSomeEvent() {
        if(SomeEvent != null)
            SomeEvent();
    }
}

class X {
    int id;
    public X(int x) { id = x; }

    // Этот метод экземпляра предназначен в качестве обработчика событий.
    public void Xhandler() {
        Console.WriteLine("Событие получено объектом " + id);
    }
}

class EventDemo3 {
    static void Main() {
        MyEvent evt = new MyEvent();
        X o1 = new X(1);
        X o2 = new X(2);
        X o3 = new X(3);
        evt.SomeEvent += o1.Xhandler;
        evt.SomeEvent += o2.Xhandler;
        evt.SomeEvent += o3.Xhandler;
        // Запустить событие.
        evt.OnSomeEvent();
    }
}

Выполнение кода из этого примера приводит к следующему результату.

Событие получено объектом 1
Событие получено объектом 2
Событие получено объектом 3

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

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

/* Уведомления о событии получает класс, когда статический метод
    используется в качестве обработчика событий. */
using System;

// Объявить тип делегата для события.
delegate void MyEventHandler();

// Объявить класс, содержащий событие.
class MyEvent {
    public event MyEventHandler SomeEvent;

    // Этот метод вызывается для запуска события.
    public void OnSomeEvent() {
        if(SomeEvent != null)
            SomeEvent();
    }
}

class X {
    /* Этот статический метод предназначен в качестве
        обработчика событий. */
    public static void Xhandler() {
        Console.WriteLine("Событие получено классом.");
    }
}

class EventDemo4 {
    static void Main() {
        MyEvent evt = new MyEvent();

        evt.SomeEvent += X.Xhandler;

        // Запустить событие.
        evt.OnSomeEvent();
    }
}

При выполнение кода этого примера получается следующий результат.

Событие получено классом.

Обратите в данном примере внимание на то, что объекты класса X вообще не создаются. Но поскольку Xhandler() является статическим методом класса X, то он может быть привязан к событию SomeEvent и выполнен при вызове метода OnSomeEvent().

Применение аксессоров событий

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

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

event делегат_события имя_события {
    add {
        // Код добавления события в цепочку событий.
    }

    remove {
        // Код удаления события из цепочки событий.
    }
}

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

Когда вызывается аксессор add или remove, он принимает в качестве параметра добавляемый или удаляемый обработчик. Как и в других разновидностях аксессоров, этот неявный параметр называется value. Реализовав аксессоры add или remove, можно организовать специальную схему хранения обработчиков событий. Например, обработчики событий можно хранить в массиве, стеке или очереди.

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

// Создать специальные средства для управления списками
// вызова обработчиков событий.
using System;

// Объявить тип делегата для события.
delegate void MyEventHandler();

// Объявить класс для хранения максимум трех событий.
class MyEvent {
    MyEventHandler[] evnt = new MyEventHandler[3];
    public event MyEventHandler SomeEvent {
        // Добавить событие в список.
        add {
            int i;
            for(i=0; i < 3; i++)
                if(evnt[i] == null) {
                    evnt[i] = value;
                    break;
                }
            if (i == 3) Console.WriteLine("Список событий заполнен.");
        }
        // Удалить событие из списка.
        remove {
            int i;
            for(i=0; i < 3; i++)
                if(evnt[i] == value) {
                    evnt[i] = null;
                    break;
                }
            if (i == 3) Console.WriteLine("Обработчик событий не найден.");
        }
    }

    // Этот метод вызывается для запуска событий.
    public void OnSomeEvent() {
        for(int i=0; i < 3; i++)
           if(evnt[i] != null) evnt[i]();
    }
}

// Создать ряд классов, использующих делегат MyEventHandler.
class W {
    public void Whandler() {
        Console.WriteLine("Событие получено объектом W");
    }
}

class X {
    public void Xhandler() {
        Console.WriteLine("Событие получено объектом X");
    }
}

class Y {
    public void Yhandler() {
        Console.WriteLine("Событие получено объектом Y");
    }
}

class Z {
    public void Zhandler() {
        Console.WriteLine("Событие получено объектом Z");
    }
}

class EventDemo5 {
    static void Main() {
        MyEvent evt = new MyEvent();
        W wOb = new W();
        X xOb = new X();
        Y yOb = new Y();
        Z zOb = new Z();

        // Добавить обработчики в цепочку событий.
        Console.WriteLine("Добавление событий. ");
        evt.SomeEvent += wOb.Whandler;
        evt.SomeEvent += xOb.Xhandler;
        evt.SomeEvent += yOb.Yhandler;

        // Сохранить нельзя - список заполнен.
        evt.SomeEvent += zOb.Zhandler;
        Console.WriteLine();

        // Запустить события.
        evt.OnSomeEvent();
        Console.WriteLine();

        // Удалить обработчик.
        Console.WriteLine("Удаление обработчика xOb.Xhandler.");
        evt.SomeEvent -= xOb.Xhandler;
        evt.OnSomeEvent();
        Console.WriteLine();

        // Попробовать удалить обработчик еще раз.
        Console.WriteLine("Попытка удалить обработчик " +
                        "xOb.Xhandler еще раз.");
        evt.SomeEvent -= xOb.Xhandler;
        evt.OnSomeEvent();
        Console.WriteLine();

        // А теперь добавить обработчик Zhandler.
        Console.WriteLine("Добавление обработчика zOb.Zhandler.");
        evt.SomeEvent += zOb.Zhandler;
        evt.OnSomeEvent();
    }
}

Ниже приведен результат выполнения этой программы:

Добавление событий.
Список событий заполнен.
Событие получено объектом W
Событие получено объектом X
Событие получено объектом Y

Удаление обработчика xOb.Xhandler.
Событие получено объектом W
Событие получено объектом Y
Попытка удалить обработчик xOb.Xhandler еще раз.

Обработчик событий не найден.
Событие получено объектом W
Событие получено объектом Y

Добавление обработчика zOb.Zhandler.
Событие получено объектом W
Событие получено объектом X
Событие получено объектом Y

Рассмотрим данную программу более подробно. Сначала в ней определяется деле­ гат обработчиков событий MyEventHandler. Затем объявляется класс MyEvent. В са­ мом его начале определяется массив обработчиков событий evnt, состоящий из трех элементов.

MyEventHandler[] evnt = new MyEventHandler[3];

Этот массив служит для хранения обработчиков событий, добавляемых в цепочку событий. По умолчанию элементы массива evnt инициализируются пустым значе­ нием (null).

Далее объявляется событие SomeEvent. В этом объявлении используется приведен­ ная ниже аксессорная форма оператора event.

public event MyEventHandler SomeEvent {
    // Добавить событие в список.
    add {
        int i;
        for(i=0; i < 3; i++)
            if(evnt[i] == null) {
                evnt[i] = value;
                break;
            }
        if (i == 3) Console.WriteLine("Список событий заполнен.");
    }
    // Удалить событие из списка.
    remove {
        int i;
        for(i=0; i < 3; i++)
            if(evnt[i] == value) {
                evnt[i] = null;
                break;
            }
        if (i == 3) Console.WriteLine("Обработчик событий не найден.");
    }
}

Когда в цепочку событий добавляется обработчик событий, вызывается аксессор add, и в первом неиспользуемом (т.е. пустом) элементе массива evnt запоминается ссылка на этот обработчик, содержащаяся в неявно задаваемом параметре value. Если в массиве отсутствуют свободные элементы, то выдается сообщение об ошибке. (Разу­ меется, в реальном коде при переполнении списка лучше сгенерировать соответствую­ щее исключение.) Массив evnt состоит всего из трех элементов, поэтому в нем можно сохранить только три обработчика событий. Когда же обработчик событий удаляется из цепочки событий, то вызывается аксессор remove и в массиве evnt осуществляется поиск ссылки на этот обработчик, передаваемой в качестве параметра value. Если ссылка найдена, то соответствующему элементу массива присваивается пустое значе­ ние (null), а значит, обработчик удаляется из цепочки событий.

При запуске события вызывается метод OnSomeEvent(). В этом методе происходит циклическое обращение к элементам массива evnt для вызова по очереди каждого об­ работчика событий.

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

ПРИМЕЧАНИЕ В многопоточных приложениях обычно приходится синхронизировать доступ к аксессо­ рам событий. Подробнее о многопоточном программировании речь пойдет в главе 23.

Разнообразные возможности событий

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

Применение анонимных методов и лямбда-выражений вместе с событиями

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

Как упоминалось выше, лямбда-выражениям теперь отдается большее предпо­ чтение по сравнению с анонимными методами, поэтому начнем именно с них. Ниже приведен пример программы, в которой лямбда-выражение используется в качестве обработчика событий.

// Использовать лямбда-выражение в качестве обработчика событий.
using System;

// Объявить тип делегата для события.
delegate void MyEventHandler (int n);

// Объявить класс, содержащий событие.
class MyEvent {
    public event MyEventHandler SomeEvent;
    // Этот метод вызывается для запуска события.
    public void OnSomeEvent(int n) {
        if (SomeEvent != null)
            SomeEvent(n);
    }
}

class LambdaEventDemo {
    static void Main() {
        MyEvent evt = new MyEvent();

        // Использовать лямбда-выражение в качестве обработчика событий.
        evt.SomeEvent += (n) =>
                Console.WriteLine("Событие получено. Значение равно " + n);

        // Запустить событие.
        evt.OnSomeEvent(1);
        evt.OnSomeEvent(2);
    }
}

Вот к какому результату приводит выполнение этой программы.

Событие получено. Значение равно 1
Событие получено. Значение равно 2

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

evt.SomeEvent += (n) =>
        Console.WriteLine("Событие получено. Значение равно " + n);

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

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

// Использовать анонимный метод в качестве обработчика событий.
evt.SomeEvent += delegate(int n) {
        Console.WriteLine("Событие получено. Значение равно " + n);
};

Как видите, синтаксис использования анонимного метода в качестве обработчика событий остается таким же, как и для его применения вместе с любым другим типом делегата.

Рекомендации по обработке событий в среде .NET Framework

В C# разрешается формировать какие угодно разновидности событий. Но ради со­ вместимости программных компонентов со средой .NET Framework следует придер­ живаться рекомендаций, установленных для этой цели корпорацией Microsoft. Эти рекомендации, по существу, сводятся к следующему требованию: у обработчиков со­ бытий должны быть два параметра. Первый из них — ссылка на объект, формирую­ щий событие, второй — параметр типа EventArgs, содержащий любую дополнитель­ ную информацию о событии, которая требуется обработчику. Таким образом, .NET- совместимые обработчики событий должны иметь следующую общую форму.

void обработчик(object отправитель, EventArgs е) {
    // ...
}

Как правило, отправитель — это параметр, передаваемый вызывающим кодом с помощью ключевого слова this. А параметр е типа EventArgs содержит дополни­ тельную информацию о событии и может быть проигнорирован, если он не нужен. Сам класс EventArgs не содержит поля, которые могут быть использованы для передачи дополнительных данных обработчику. Напротив, EventArgs служит в ка­ честве базового класса, от которого получается производньгй класс, содержащий все необходимые поля. Тем не менее в классе EventArgs имеется одно поле Empty типа static, которое представляет собой объект типа EventArgs без данных. Ниже приведен пример программы, в которой формируется .NET-совместимое событие.

// Пример формирования .NET-совместимого события.
using System;

// Объявить класс, производный от класса EventArgs.
class MyEventArgs : EventArgs {
    public int EventNum;
}

// Объявить тип делегата для события.
delegate void MyEventHandler(object source, MyEventArgs arg);

// Объявить класс, содержащий событие.
class MyEvent {
    static int count = 0;
    public event MyEventHandler SomeEvent;

    // Этот метод запускает событие SomeEvent.
    public void OnSomeEvent() {
        MyEventArgs arg = new MyEventArgs();
        if(SomeEvent != null) {
            arg.EventNum = count++;
            SomeEvent(this, arg);
        }
    }
}

class X {
    public void Handler(object source, MyEventArgs arg) {
        Console.WriteLine("Событие " + arg.EventNum +
                        " получено объектом класса X.");
        Console.WriteLine("Источник: " + source);
        Console.WriteLine();
    }
}

class Y {
    public void Handler(object source, MyEventArgs arg) {
        Console.WriteLine("Событие " + arg.EventNum +
                        " получено объектом класса Y.");
        Console.WriteLine("Источник: " + source);
        Console.WriteLine();
    }
}

class EventDemo6 {
    static void Main() {
        X ob1 = new X();
        Y ob2 = new Y();
        MyEvent evt = new MyEvent();

        // Добавить обработчик Handler() в цепочку событий.
        evt.SomeEvent += ob1.Handler;
        evt.SomeEvent += ob2.Handler;

        // Запустить событие.
        evt.OnSomeEvent();
        evt.OnSomeEvent();
    }
}

Ниже приведен результат выполнения этой программы.

Событие 0 получено объектом класса X
Источник: MyEvent

Событие 0 получено объектом класса Y
Источник: MyEvent

Событие 1 получено объектом класса X
Источник: MyEvent

Событие 1 получено объектом класса Y
Источник: MyEvent

В данном примере создается класс MyEventArgs, производный от класса EventArgs. В классе MyEventArgs добавляется лишь одно его собственное поле: EventNum. Затем объявляется делегат MyEventHandler, принимающий два параметра, требующиеся для среды .NET Framework. Как пояснялось выше, первый параметр содержит ссыл­ ку на объект, формирующий событие, а второй параметр — ссылку на объект класса EventArgs или производного от него класса. Обработчики событий Handler(), опре­ деляемые в классах X и Y, принимают параметры тех же самых типов.

В классе MyEvent объявляется событие SomeEvent типа MyEventHandler. Это событие запускается в методе OnSomeEvent() с помощью делегата SomeEvent, ко­ торому в качестве первого аргумента передается ссылка this, а вторым аргумен­ том служит экземпляр объекта типа MyEventArgs. Таким образом, делегату типа MyEventHandler передаются надлежащие аргументы в соответствии с требованиями совместимости со средой .NET.

Применение делегатов EventHandler<TEventArgs> и EventHandler

В приведенном выше примере программы объявлялся собственный делегат со­ бытия. Но как правило, в этом не никакой необходимости, поскольку в среде .NET Framework предоставляется встроенный обобщенный делегат под названием EventHandler. (Более подробно обобщенные типы рассматриваются в главе 18.) В данном случае тип TEventArgs обозначает тип аргумента, передаваемого параметру EventArgs события. Например, в приведенной выше программе событие SomeEvent может быть объявлено в классе MyEvent следующим образом.

public event EventHandler<MyEventArgs> SomeEvent;

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

Для обработки многих событий параметр типа EventArgs оказывается ненуж­ ным. Поэтому с целью упростить создание кода в подобных ситуациях в среду .NET Framework внедрен необобщенный делегат типа EventHandler. Он может быть ис­ пользован для объявления обработчиков событий, которым не требуется дополни­ тельная информация о событиях. Ниже приведен пример использования делегата EventHandler.

// Использовать встроенный делегат EventHandler.
using System;

// Объявить класс, содержащий событие,
class MyEvent {
    public event EventHandler SomeEvent; // использовать делегат EventHandler

    // Этот метод вызывается для запуска события.
    public void OnSomeEvent() {
        if(SomeEvent != null)
            SomeEvent(this, EventArgs.Empty);
    }
}

class EventDemo7 {
    static void Handler(object source, EventArgs arg) {
        Console.WriteLine("Произошло событие");
        Console.WriteLine("Источник: " + source);
    }

    static void Main() {
        MyEvent evt = new MyEvent();

        // Добавить обработчик Handler() в цепочку событий.
        evt.SomeEvent += Handler;

        // Запустить событие.
        evt.OnSomeEvent();
    }
}

В данном примере параметр типа EventArgs не используется, поэтому в качестве этого параметра передается объект-заполнитель EventArgs.Empty. Результат выпол­ нения кода из данного примера следующий.

Произошло событие
Источник: MyEvent

Практический пример обработки событий

События нередко применяются в таких ориентированных на обмен сообщениями средах, как Windows. В подобной среде программа просто ожидает до тех пор, пока не будет получено конкретное сообщение, а затем она предпринимает соответствую­ щее действие. Такая архитектура вполне пригодна для обработки событий средствами С#, поскольку дает возможность создавать обработчики событий для реагирования на различные сообщения и затем просто вызывать обработчик при получении кон­ кретного сообщения. Так, щелчок левой кнопкой мыши может быть связан с событием LButtonClick. При получении сообщения о щелчке левой кнопкой мыши вызывает­ ся метод OnLButtonClick(), и об этом событии уведомляются все зарегистрирован­ ные обработчики.

Разработка программ для Windows, демонстрирующих такой подход, выходит за рамки этой главы, тем не менее, рассмотрим пример, дающий представление о принципе, по которому действует данный подход. В приведенной ниже програм­ ме создается обработчик событий, связанных с нажатием клавиш. Всякий раз, когда на клавиатуре нажимается клавиша, запускается событие KeyPress при вызове ме­ тода OnKeyPress(). Следует заметить, что в этой программе формируются .NET- совместимые события и что их обработчики предоставляются в лямбда-выражениях.

// Пример обработки событий, связанных с нажатием клавиш на клавиатуре.
using System;

// Создать класс, производный от класса EventArgs и
// хранящий символ нажатой клавиши.
class KeyEventArgs : EventArgs {
    public char ch;
}

// Объявить класс события, связанного с нажатием клавиш на клавиатуре.
class KeyEvent {
    public event EventHandler <KeyEventArgs> KeyPress;

    // Этот метод вызывается при нажатии клавиши.
    public void OnKeyPress(char key) {
        KeyEventArgs k = new KeyEventArgs();
        if(KeyPress != null) {
            k.ch = key;
            KeyPress (this, k);
        }
    }
}

// Продемонстрировать обработку события типа KeyEvent.
class KeyEventDemo {
    static void Main() {
        KeyEvent kevt = new KeyEvent();
        ConsoleKeyInfo key;
        int count = 0;

        // Использовать лямбда-выражение для отображения факта нажатия клавиши.
        kevt.KeyPress += (sender, е) =>
        Console.WriteLine(" Получено сообщение о нажатии клавиши: " + e.ch);

        // Использовать лямбда-выражение для подсчета нажатых клавиш.
        kevt.KeyPress += (sender, е) =>
        count++; // count — это внешняя переменная
        Console.WriteLine("Введите несколько символов. " +
                        "По завершении введите точку.");

        do {
            key = Console.ReadKey();
            kevt.OnKeyPress(key.KeyChar);
        } while(key.KeyChar != '.');

        Console.WriteLine("Было нажато " + count + " клавиш.");
    }
}

Вот, например, к какому результату приводит выполнение этой программы.

Введите несколько символов. По завершении введите точку.
t Получено сообщение о нажатии клавиши: t
е Получено сообщение о нажатии клавиши: е
s Получено сообщение о нажатии клавиши: s
t Получено сообщение о нажатии клавиши: t
. Получено сообщение о нажатии клавиши: .
Было нажато 5 клавиш.

В самом начале этой программы объявляется класс KeyEventArgs, производный от класса EventArgs и служащий для передачи сообщения о нажатии клавиши об­ работчику событий. Затем объявляется обобщенный делегат EventHandler, опреде­ ляющий обработчик событий, связанных с нажатием клавиш. Эти события инкапсу­ лируются в классе KeyEvent, где определяется событие KeyPress.

В методе Main() сначала создается объект kevt класса KeyEvent. Затем в це­ почку событий kevt.KeyPress добавляется обработчик, предоставляемый лямбда- выражением. В этом обработчике отображается факт каждого нажатия клавиши, как показано ниже.

kevt.KeyPress += (sender, е) =>
    Console.WriteLine(" Получено сообщение о нажатии клавиши: " + e.ch);

Далее в цепочку событий kevt.KeyPress добавляется еще один обработчик, пре­ доставляемый лямбда-выражением. В этом обработчике подсчитывается количество нажатых клавиш, как показано ниже.

kevt.KeyPress += (sender, е) =>
    count++; // count — это внешняя переменная

Обратите внимание на то, что count является локальной переменной, объявленной в методе Main() и инициализированной нулевым значением.

Далее начинает выполняться цикл, в котором метод kevt.OnKeyPress() вызыва­ ется при нажатии клавиши. Об этом событии уведомляются все зарегистрированные обработчики событий. По окончании цикла отображается количество нажатых кла­ виш. Несмотря на всю свою простоту, данный пример наглядно демонстрирует саму суть обработки событий средствами С#. Аналогичный подход может быть использован и для обработки других событий. Безусловно, в некоторых случаях анонимные обра­ ботчики событий могут оказаться непригодными, и тогда придется внедрить имено­ ванные методы.