# 五、处理委托、事件和 Lambdas

您要做的大部分用户界面工作都是基于事件的，C# 通过称为事件的类型成员来支持这一点。为了让事件正常工作，您需要一些基础设施来指定可以调用的方法，这些方法通过委托和 lambdas 呈现出来。本章将解释委托、事件和 lambdas 如何在 C# 中工作。

## 引用委托的方法

委托在 C# 中有一些功能:引用方法、分派多个方法、异步执行和事件类型。这可能会令人困惑，因为许多其他语言特性只服务于一个目的。区分和比较 C# 委托的所有这些功能增加了您可能不熟悉的复杂性。本讨论将在某种程度上删减特性列表，希望能够阐明委托，并在您推进实际实现时使它们不那么复杂。特别是，我将关注委托作为方法引用和事件类型。

| ![](img/note.png) | 注意:我将避免深入讨论委托多播和异步执行，因为它们很少被使用，并且很大程度上被其他语言特性所取代。例如，事件支持多播调度，C# 5.0 引入了一种称为异步的功能。 |

让我们首先研究委托作为方法引用的作用。为此，委托指定它可以引用的方法的签名，如下所示:

```cs
    public delegate double Add(double num1, double num2);

```

代码清单 75

您可能会注意到委托看起来像一个抽象方法，除了它有`delegate`类型定义关键字。`delegate`定义是引用类型，就像类、结构或接口一样。先前的`delegate`定义是针对名为`Add`的委托类型的，它接受两个`double`参数并返回类型为`double`的结果。和其他类型一样，委托辅助功能只能是`public`或`internal`，默认为`internal`。

委托有一些我不会涉及的深奥用法，但是我确实想关注最实用和最常见的使用委托的方式:作为事件类型。

## 点火事件

事件是类型成员，允许一个类型通知其他类型已经发生的事情。一个非常常见的例子是带有按钮的用户界面。当用户点击那个按钮时，你会想写一些代码。以下是实现这一目标所需的部分:

1.  一个`Button`类，它通常由您正在使用的用户界面技术提供。
2.  `Button`类的事件成员，名为`Clicked`。
3.  UI 技术负责知道`Clicked`事件应该在什么时候触发。我将在接下来的代码清单中使用`SimulateClick`方法。
4.  该事件具有委托类型。只有符合该委托类型签名的方法才能分配给该委托。
5.  您的代码定义了当该事件触发时要调用的方法。
6.  您编写的方法必须具有与事件的委托类型匹配的签名。如果方法签名与事件委托类型签名不匹配，编译器不会让您将该方法分配给委托。

如您所见，代表有很多活动部件。特别要注意#6。委托防止您、任何人或任何事物向事件分配任意方法。下面是一个用委托类型的事件定义委托和类的示例。

```cs
    using System;

    public class ClickEventArgs : EventArgs
    {
        public string Name { get; set; }
    }

    public delegate void ClickHandler(object sender, ClickEventArgs e);

    public class CalculatorButton
    {
        public event ClickHandler Clicked;
    }

```

代码清单 76

事件可以是类、结构或接口的成员。如果它是接口成员，这意味着实现该接口的类或结构的定义中也必须有该事件。一个事件具有`event`修饰符，并遵循与其他类型成员相同的可访问性规则，如方法和属性。

如果一个代表服务于你需要的目的，你可以使用它。事实上，FCL 包含许多可重用的类型，包括可重用的委托类型，您可以在不需要创建自己的委托类型的情况下使用它们。甚至还有一个名为`EventHandler`的. NET 类型，它几乎与`ClickHandler`的签名相匹配，其中`sender`通常是事件的来源，`EventArgs`是一个基类，您可以从中派生出来创建自己的自定义类型，用于在被激发时与方法和事件调用共享信息。带有`ClickEventArgs`的前一个代码源自`EventArgs`，这是一个附带的类型。NET 框架。下面的示例模拟一个事件，演示如何编写在代码中处理该事件的方法。

```cs
    using System;

    public class CalculatorButton
    {
        public event ClickHandler Clicked;
        public void SimulateClick()
        {
            if (Clicked != null)
            {
                ClickEventArgs args = new ClickEventArgs();
                args.Name = "Add";

                Clicked(this, args);
            }
        }
    }

    public class Program
    {
        public static void Main()
        {
            Program prg = new Program();
            CalculatorButton calcBtn = new CalculatorButton();

            calcBtn.Clicked += new ClickHandler(prg.CalculatorBtnClicked);
            calcBtn.Clicked += prg.CalculatorBtnClicked;

            calcBtn.SimulateClick();

            Console.ReadKey();
        }

        public void CalculatorBtnClicked(object sender, ClickEventArgs e)
        {
            Console.WriteLine(
                $"Caller is a CalculatorButton: {sender is CalculatorButton} and is named {e.Name}");
        }
    }

```

代码清单 77

同样，这个例子有很多移动部分，但是它们遵循本节开始时关于定义和使用事件的[列表](#EventList)。首先，注意`CalculateButton`有一个新的方法，`SimulateClick`。由于我们通过避免用户界面来简化代码，我们不得不假装用户点击了一个按钮。也就是说，`SimulateClick`演示了在自己的代码中触发事件的正确方法。在触发事件之前，通过检查`null`，确保用户已经为事件分配了方法。每当没有指定方法时，事件将是`null`。`SimulateClick`设置`ClickEventArgs`参数。在这种情况下，它只是一个`Name`属性，但是您可以提供您正在使用的`EventArgs`类型的任何相关信息，以及接收此事件的方法可能需要的信息。接下来，通过像调用方法一样调用事件来激发事件。这将导致事件按照分配的顺序逐个调用分配给它的每个方法。第一个参数是`this`关键字，它指示包含类型的实例`CalculatorButton`被传递给方法。第二个是`ClickEventArgs`变量，它之前被实例化并设置了`Name`属性。

`Main`方法展示了如何给事件分配方法。请注意，`+=`运算符被两次用于为`CalculateButton`实例、`calcBtn`和`Clicked`事件分配两种方法。`CalculatorBtnClicked`方法是一个实例方法，因此`prg`实例在分配期间提供对该方法的访问。

第一个赋值创建了`ClickHandler`委托的新实例。委托是类型，您可以实例化它们。您可以使用方法实例化委托，该方法将成为委托引用的方法。还记得我如何解释委托是对方法的引用吗？在这种情况下，`ClickHandler`委托的新实例指的是`CalculatorBtnClicked`方法。第二个作业展示了一种更新、更简单的语法来完成与第一个作业相同的任务；它只使用方法名。这称为委托推理，意味着由于分配给事件的方法与事件的委托具有相同的签名，C# 编译器将负责在幕后为您实例化带有该方法的委托。

最后，在`CalculatorButton`实例`calcBtn`上调用`SimulateClick`，如前所述触发事件。无论程序是否两次向事件分配相同的方法，激发事件都会导致所有分配的委托激发，从而调用分配给每个委托的方法来执行。因此`CalculatorBtnClicked`方法将执行两次。

你可能想知道为什么我必须在`CalculatorButton`中定义`SimulateClick`，而不是仅仅从`Main`中触发事件。原因是因为外部代码不能触发事件。事件只能从其包含类型内部激发。

您可以将代码块直接分配给事件，而不是分配命名方法。

## 与兰达斯合作

lambda 是一个无名的方法。有时，您有一个服务于一个特定目的的代码块，您不需要将其定义为一个方法。方法很好，因为它们让你模块化你的代码，并有一些东西可以参考委托和从多个地方调用。但是很多时候你只需要为一个特定的操作运行一些代码。Lambdas 是分配和执行代码块的快速简单的方法。

| ![](img/note.png) | 注意:lambda 也是一个非常复杂的语言特性，允许您在解析树和代码之间进行转换。这是语言集成查询(LINQ)的一个核心特性，我将在[第 7 章](7.html#_Chapter_7_)中详细讨论。对于日常的实际应用，使用 lambdas 作为解析树是很少见的。 |

就像方法一样，lambdas 可以有参数、主体，并且可以返回值。下面的代码清单是 lambda 的一个例子。

```cs
    using System;

    public class Program
    {
        public static void Main()
        {
            Action hello = () => Console.WriteLine("Hello!");
            hello();

            Console.ReadKey();
        }
    }

```

代码清单 78

`Action`是中可重用的委托。NET 框架，`hello`是`Action`委托类型的变量。lambda 以一个空的参数列表开始，这意味着没有参数。`=>`操作符将参数列表和正文分开，称为“这样”或“转到”。接下来，你看到身体，这是一个单一的声明。由于 hello 是一个委托，您可以像调用方法一样调用它，它将执行 lambda，该 lambda 打印“Hello！”到控制台。

对于单个语句，不需要在主体上使用花括号，但是可以使用多个语句，如下例所示。

```cs
    using System;

    public class Program
    {
        public static void Main()
        {
            Predicate<string> validator =
                word =>
                {
                    int count = word.Length;
                    return count > 3;
                };
            ValidateInput(validator);
            ValidateInput(word =>
            {
                int count = word.Length;
                return count > 3;
            });

            Console.ReadKey();
        }

        public static void ValidateInput(Predicate<string> validator)
        {
            string input = "Hello";
            bool isValid = validator(input);
            Console.WriteLine($"Is Valid: {isValid}");
        }
    }

```

代码清单 79

前面的示例将一个 lambda 分配给。NET Framework 的`Predicate`委托，旨在返回一个`bool`。λ有一个类型为`string`的参数`word`。因为 lamba 有多个语句，所以它需要花括号。该示例显示了如何将 lambda 作为变量和整个 lambda 传递。

`Predicate`是泛型委托。类型参数设置为`<string>`，意味着λ参数为类型`string`。您将在[第 6 章](6.html#_Chapter_6_)中了解更多关于仿制药的信息。

`ValidateInput`方法将`string`传递给`validator`，并将结果分配给`isValid`变量。它就像一个方法调用，只是没有方法，只有代码；它写起来很快，而且范围有限。

使用 lambdas 的另一种方法是事件。下面的例子展示了一种不同的方法来为前面的`CalculatorButton`例子中的`Clicked`事件编写事件处理程序方法。

```cs
    using System;

    public class Program
    {
        public static void Main()
        {
            CalculatorButton calcBtn = new CalculatorButton();

            calcBtn.Clicked += (object sender, ClickEventArgs e) =>
            {
                Console.WriteLine(
                    $"Caller is a CalculatorButton: {sender is CalculatorButton} and is named {e.Name}");

                Console.WriteLine(message);
            };

            calcBtn.SimulateClick();

            Console.ReadKey();
        }
    }

```

代码清单 80

在这个例子中，您可能注意到的第一件事是`Clicked`事件的委托分配现在是一个λ。如果您有两个或多个参数，它们必须以逗号分隔列表的形式括在括号中。如果 lambda 的主体包含两行或多行，它们必须以分号结束，并用大括号括起来。请注意，lambda 的签名与前面定义的`ClickEventHandler`相匹配。

## 更多 FCL 代表类型

除了您在前面的示例中看到的`Action`和`Predicate<T>`代表，FCL 还有一组名为`Func<T>`的代表，您可以随意重用。这里有一个例子改写了前面的例子，用`Func<T, TResult>`代替`Predicate<T>`。

```cs
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;

    class Program
    {
        public static void Main()
        {
            Func<string, bool> validator =
                word =>
                {
                    int count = word.Length;
                    return count > 3;
                };
            ValidateInput(validator);
            ValidateInput(word =>
            {
                int count = word.Length;
                return count > 3;
            });

            Console.ReadKey();
        }

        public static void ValidateInput(Func<string, bool> validator)
        {
            string input = "Hello";
            bool isValid = validator(input);
            Console.WriteLine($"Is Valid: {isValid}");
        }
    }

```

代码清单 81

除了使用`Func<string, bool>`代替`Predicate<string>`之外，这与之前的程序几乎相同。如前所述，`Func<T, TResult>`和`Predicate<T>`都是通用代表。尖括号中的类型规范是应用于参数和返回类型的类型的插件。以下列表显示了在 FCL 定义的`Predicate<T>`代表。

```cs
    public delegate bool Predicate<T>(T obj);

```

代码清单 82

它指的是返回`bool`的方法，但接受类型为`T`的参数。因此，`Predicate<string>`意味着所指方法的参数是一个`string`。同样，这是 FCL 对`Func<T, TResult>`的定义。

```cs
    public delegate TResult Func<T, TResult>(T arg);

```

代码清单 83

这表明`Func<T, TResult>`接受类型为`T`的参数，并返回类型为`TResult`的值。在[代码清单 81](#CodeListing81) 中，`Func<string, bool>`是指一个参数类型为`string`的方法，该方法返回类型`bool`。

为了方便起见，FCL 提供了 18 个`Func`委托重载，允许 0 到 16 个输入参数和 1 个返回参数类型。这涵盖了许多场景，您可以重用从 FCL 提供的代表走很长的路。您只能在需要时创建自己的代理。

## 表达体成员

虽然不一定是 lambdas，但是表达式体成员为属性和方法提供了一些简写语法。下面的清单提供了一个例子。

```cs
    using System;

    class Program
    {
        public static string Today => DateTime.Now.ToShortDateString();

        public static void Log(string message) => Console.WriteLine(message);

        public static void Main()
        {
            Log($"{Today} is a good day.");

            Console.ReadKey();
        }
    }

```

代码清单 84

`Program`类将`Today`属性和`Log`方法定义为表达体成员。`Main`显示这些成员如何像普通方法和属性一样使用。

## 总结

您了解了代表、事件和 lambdas。委托是对方法的引用。您可以在代码中传递委托或将其分配给事件。委托引用的方法必须具有与委托相同的签名。事件是用委托类型定义的类型成员。您可以根据需要为一个事件分配任意多的委托，每个委托在事件触发时执行。您只能从定义事件的类型中激发事件。当不需要定义单独的方法时，可以使用 lambdas 来代替方法。您可以使用委托引用 lambda，将 lambda 作为参数传递，或将 lambda 分配给事件。表达式体成员允许您使用速记语法编写属性和方法。