Skip to content

Files

Latest commit

c70125a · Jan 8, 2022

History

History
312 lines (214 loc) · 15.5 KB

5.md

File metadata and controls

312 lines (214 loc) · 15.5 KB

五、处理委托、事件和 Lambdas

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

引用委托的方法

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

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

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

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

代码清单 75

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

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

点火事件

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

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

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

    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 框架。下面的示例模拟一个事件,演示如何编写在代码中处理该事件的方法。

    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

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

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

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

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

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

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

与兰达斯合作

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

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

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

    using System;

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

            Console.ReadKey();
        }
    }

代码清单 78

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

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

    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 章中了解更多关于仿制药的信息。

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

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

    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 代表类型

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

    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>代表。

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

代码清单 82

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

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

代码清单 83

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

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

表达体成员

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

    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 分配给事件。表达式体成员允许您使用速记语法编写属性和方法。