您要做的大部分用户界面工作都是基于事件的,C# 通过称为事件的类型成员来支持这一点。为了让事件正常工作,您需要一些基础设施来指定可以调用的方法,这些方法通过委托和 lambdas 呈现出来。本章将解释委托、事件和 lambdas 如何在 C# 中工作。
委托在 C# 中有一些功能:引用方法、分派多个方法、异步执行和事件类型。这可能会令人困惑,因为许多其他语言特性只服务于一个目的。区分和比较 C# 委托的所有这些功能增加了您可能不熟悉的复杂性。本讨论将在某种程度上删减特性列表,希望能够阐明委托,并在您推进实际实现时使它们不那么复杂。特别是,我将关注委托作为方法引用和事件类型。
| | 注意:我将避免深入讨论委托多播和异步执行,因为它们很少被使用,并且很大程度上被其他语言特性所取代。例如,事件支持多播调度,C# 5.0 引入了一种称为异步的功能。 |
让我们首先研究委托作为方法引用的作用。为此,委托指定它可以引用的方法的签名,如下所示:
public delegate double Add(double num1, double num2);
代码清单 75
您可能会注意到委托看起来像一个抽象方法,除了它有delegate
类型定义关键字。delegate
定义是引用类型,就像类、结构或接口一样。先前的delegate
定义是针对名为Add
的委托类型的,它接受两个double
参数并返回类型为double
的结果。和其他类型一样,委托辅助功能只能是public
或internal
,默认为internal
。
委托有一些我不会涉及的深奥用法,但是我确实想关注最实用和最常见的使用委托的方式:作为事件类型。
事件是类型成员,允许一个类型通知其他类型已经发生的事情。一个非常常见的例子是带有按钮的用户界面。当用户点击那个按钮时,你会想写一些代码。以下是实现这一目标所需的部分:
- 一个
Button
类,它通常由您正在使用的用户界面技术提供。 Button
类的事件成员,名为Clicked
。- UI 技术负责知道
Clicked
事件应该在什么时候触发。我将在接下来的代码清单中使用SimulateClick
方法。 - 该事件具有委托类型。只有符合该委托类型签名的方法才能分配给该委托。
- 您的代码定义了当该事件触发时要调用的方法。
- 您编写的方法必须具有与事件的委托类型匹配的签名。如果方法签名与事件委托类型签名不匹配,编译器不会让您将该方法分配给委托。
如您所见,代表有很多活动部件。特别要注意#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
,确保用户已经为事件分配了方法。每当没有指定方法时,事件将是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 是分配和执行代码块的快速简单的方法。
| | 注意: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 框架,hello
是Action
委托类型的变量。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
相匹配。
除了您在前面的示例中看到的Action
和Predicate<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 分配给事件。表达式体成员允许您使用速记语法编写属性和方法。