C#中的委托和事件(0) delegate
前言
来说一说委托(delegate)和事件(event),本篇采取的形式是翻译微软Delegate的docs中的重要部分(不要问我为什么微软的docs有中文还要读英文,因为读中文感觉自己有阅读障碍- -)+ 自己理解总结,适合不会或没有使用过delegate的小白。
为什么要把委托和事件放在一起,因为委托Delegate是事件Event的基础,并且他们容易被混淆。
原docs中对委托进行了一个定位:委托在.Net中提供后期绑定(Late Binding)机制。
System.Delegate和delegate关键字
定义委托类型
我们从delegate
关键字开始,因为它是你使用委托的主要方式。当你使用关键字delegate
时,编译器生成的代码将映射到一些方法,这些方法调用了Delegate
和MulticastDelegate
类的成员。
定义委托的语法跟定义方法签名比较类似,你只需要在返回类型和访问权限之间加上关键字delegate
。
继续使用List.Sort()
方法(docs前面一直使用的例子)作为我们的例子,第一步是为Comparison委托创建一个类型:
public delegate int Comparison<in T>(T left, T right);
通过上述语句,编译器生成了一个Comparison
类,该类派生自System.Delegate
。该类包含一个方法,该方法返回1个int,有2个参数(即和签名相同)。
你可以在类内部、命名空间内、全局命名空间中定义委托。(当然,不建议在全局命名空间中定义委托)
编译器同时会为该类生成添加、删除程序,该类的使用者可以从1个实例的调用列表中添加、删除方法。编译器强制添加、删除的方法的签名与声明该方法时使用的签名匹配。
声明委托的实例
定义委托类型之后,你就可以创建委托的实例了。实例的创建和其他变量的创建没有区别。
public Comparison<T> comparator;
变量comparator
的类型是我们之前定义的委托类型Comparison<T>
。跟变量一样,我们可以声明局部委托变量,把委托变量当做方法参数等。
分配、添加和移除方法
每个委托实例包含1个调用列表,调用列表包含所有分配给委托实例的方法。
想要将方法分配给委托实例,首先需要定义签名与委托类型定义匹配的方法。可以看到下面这个CompareLength
方法的签名与委托类型的定义相同,而其内部是个string
类的方法。
//这是一种用lambda表达式定义的方法
private static int CompareLength(string left, string right) =>
left.Length.CompareTo(right.Length);
通过将该方法传递给 List.Sort() 方法来创建该关系:
//使用上述定义的方法名。
phrases.Sort(CompareLength);
//这里不用纠结为什么是这样传入,它只是docs的一个例子,其内部肯定有
//comparator = CompareLength;
//这样的形式
这里 将方法名用作参数会告知编译器将方法引用 转换为可以用作委托调用目标的引用,并将该方法作为调用目标进行附加。其核心如下
//左边是委托变量,右边是方法名称
comparator = CompareLength;
声明Comparison
类型的变量并进行分配的操作就是下面这样:
public Comparison<string> comparer = CompareLength;
private static int CompareLength(string left, string right) =>
left.Length.CompareTo(right.Length);
当然如果委托目标的方法是很短的方法 ,你也可以使用lambda
public Comparison<string> comparer = (left, right) =>
left.Length.CompareTo(right.Length);
这里看到的都是单个目标方法添加到委托变量,但委托支持将多个方法添加到委托变量的调用列表。
调用委托
通过下面这种委托变量名+参数的形式,我们调用了附加到委托的方法列表中的方法。
int result = comparator(left, right);
如果并没有任何附加到comparator
变量的方法,上面代码将应发NullReferenceException
。
MulticastDelegate
System.MulticastDelegate
是System.Delegate
的单个直接子类。C#禁止从Delegate
和MulticastDelegate
。当使用delegate
关键字定义、声明委托类型时,C#编译器会创建从MulticastDelegate
派生的实例。为了类型安全的考虑,编译器创建了具体的委托类。
与委托实例一起使用的最多的方法时Invoke()
和BeginInvoke()/EndInvoke()
,Invoke()
调用已附加到特定委托实例上的所有方法。
强类型委托
上一节中我们看到可以用delegate
关键字创建特定的委托类型。
当你需要不同的方法签名时,你将创建新的委托类型。一段时间后这项工作可能会变得乏味,因为每个新功能都需要新的委托类型。
幸运的是,.NET Core框架包含几种类型,你可以在需要委托类型时重用它们。
这些类型中第一个是Action
:
public delegate void Action();
public delegate void Action<in T>(T arg);
public delegate void Action<in T1, in T2>(T1 arg1, T2 arg2);
Action
委托有多种变体,最多包含16个参数。Action
没有返回值。
第二个常用的是Func
:
public delegate TResult Func<out TResult>();
public delegate TResult Func<in T1, out TResult>(T1 arg);
public delegate TResult Func<in T1, in T2, out TResult>(T1 arg1, T2 arg2);
Func
委托最多包含16个输入参数,结果类型始终是最后一个类型参数。Func
有返回值。
还有一种是Predicate<T>
:
public delegate bool Predicate<in T>(T obj);
那么你可以注意到,对于任何Predicate
委托类型都有一个相等的Func
委托类型:
Func<string, bool> TestForString;
Predicate<string> AnotherTestForString
现在你不需要为任何新功能定义新的委托类型,关于这些特殊的委托类型的用法我将在另外一篇博客中罗列,但现在我们可以想象到,Action
的用法应该如下:
Action showMethod = SomeMethod();
showMethod();
委托的常用模式
委托提供了一种机制,它使软件设计涉及的组件之间的耦合最小。
LINQ
是这种设计的一个很好的例子。LINQ
查询表达式模式的所有功能都依赖于委托。考虑下面这样一个简单的例子:
var smallNumbers = numbers.Where(n => n < 10);
//括号中是Func的lambda写法,Action和Func的用法我将在另一篇博客中介绍,这里你只需要知道括号中传入的是一个已经赋值的委托实例。
上述例子将序列过滤为仅小于10的数字。Where
方法使用委托来确定序列中哪些元素被过滤出来。
Where
方法的原型是:
public static IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> souce, Func<TSource, bool> predicate);
这个示例说明了委托是如何减少组件之间的耦合的,你可以无需创建派生自特定积累的类,你也不需要实现特定接口。你唯一要做的是提供实现手头任务的方法。
使用代理创建你自己的组件
(这里开始docs举了一个例子来说明如何在实际中使用委托)
让我们来定义一个可用于大型系统中的日志消息组件,该组件中有很多常用功能,它接收来自系统中任何地方的消息。这些消息将具有不同的优先级。
首次实现
原始的实现是这样的:我们接收一个message,然后使用委托将消息写到控制台。
public static class Logger
{
//Action委托实例
public static Action<string> WriteMessage;
//对外接口
public static void LogMessage(string msg)
{
//调用委托上的方法
WriteMessage(msg);
}
}
public static class LoggingMethods{
//将信息打印到控制台的方法
public static void LogToConsole(string message)
{
Console.Error.WriteLine(message);
}
}
//委托实例赋值,这句话一般发生在LoggingMethods的构造器中
Logger.WriteMessage += LoggingMethods.LogToConsole;
附加到委托实例上的方法,可以是实例方法,也可以具有任何访问权限。
格式化输出
向LogMessage
方法中添加一些参数,以便日志类创建更多结构化消息。
public enum Severity{
Verbose,
Trace,
Information,
Warning,
Error,
Critical
}
利用Severity
过滤打印的消息。
public static class Logger
{
public static Action<string> WriteMessage;
public static Severity LogLevel {get;set;} = Severity.Warning;
public static void LogMessage(Severity s, string component, string
msg)
{
//继续增加筛选功能
if (s < LogLevel)
return;
var outputMsg = $"{DateTime.Now}\t{s}\t{component}\t{msg}";
WriteMessage(outputMsg);
}
}
这里我们可以看到,Logger
与任何输出类的耦合非常松散,当我们改变Logger
的打印条件时,具体的委托实现完全不需要改动。在实际中,日志输出类可能位于不同的程序集中,利用委托进行耦合,它们完全不需要被重建。
第二个输出引擎
让我们在添加一个将消息记录到文件的输出引擎。这稍微有点复杂,这是一个封装文件操作的类,并要确保每次写入后始终关闭文件(这样可以确保在生成每条消息后将所有数据刷新到磁盘)。
public class FileLogger
{
private readonly string logPath;
public FileLogger(string path)
{
logPath = path;
Logger.WriteMessage += LogMessage;
}
public void DetachLog() => Logger.WriteMessage -= LogMessage;
// make sure this can't throw.
private void LogMessage(string msg)
{
try
{
using (var log = File.AppendText(logPath))
{
log.WriteLine(msg);
log.Flush();
}
}
catch (Exception)
{
// Hmm. We caught an exception while
// logging. We can't really log the
// problem (since it's the log that's failing).
// So, while normally, catching an exception
// and doing nothing isn't wise, it's really the
// only reasonable option here.
}
}
}
创建此类后,可将它进行实例化,然后它会将其LogMessage
方法附加到Logger
中:
var file = new FileLogger("log.txt");
也就是说你可以同时附加这两种输出日志的方法(向控制台和文件输出)。
var fileOutput = new FileLogger("log.txt");
Logger.WriteMessage += LogToConsole;
以后,即使在同一个应用程序中,也可删除其中一个方法,而不会对系统造成任何其他问题:
Logger.WriteMessage -= LogToConsole;
再次提醒一下,你无需构建任何其他基础结构即可支持多种输出方法,这些被添加到委托实例的方法只是调用列表上的一种方法而已。
请注意,一定要确保委托方法不会引发任何异常,如果委托实例的调用列表中的任何一个方法抛出异常,则调用列表上其他方法都不会被调用。
Null 委托
当WriteMessage
未附加方法时,调用其将引发NullReferenceException
最后,让我们更新LogMessage
方法,以确保它在没有任何委托方法的时候具有鲁棒性。
public static void LogMessage(string msg)
{
WriteMessage?.Invoke(msg);
}
当左操作数(本例中为 WriteMessage )为 null 时,null 条件运算符( ?. )会短路,这意味着不会尝试调用委托方法。
小结
通过在设计中使用委托,不同的组件可以非常松散地耦合在一起。 这样可提供多种优势。 可轻松创建新的输出机制并将它们附加到日志系统中。这些机制只需要一种方法:编写日志消息的方法。这种设计在添加新功能时有非常强的弹性。任何编写者只需要实现同一种参数和返回值的方法。该方法可以是静态方法或实例方法。可以是公共的,私有的或其他任何合法的访问权限。
下一篇我们讲讲事件