前言

来说一说委托(delegate)和事件(event),本篇采取的形式是翻译微软Delegate的docs中的重要部分(不要问我为什么微软的docs有中文还要读英文,因为读中文感觉自己有阅读障碍- -)+ 自己理解总结,适合不会或没有使用过delegate的小白。

为什么要把委托和事件放在一起,因为委托Delegate是事件Event的基础,并且他们容易被混淆。

原docs中对委托进行了一个定位:委托在.Net中提供后期绑定(Late Binding)机制。

System.Delegate和delegate关键字

定义委托类型

我们从delegate关键字开始,因为它是你使用委托的主要方式。当你使用关键字delegate时,编译器生成的代码将映射到一些方法,这些方法调用了DelegateMulticastDelegate类的成员。

定义委托的语法跟定义方法签名比较类似,你只需要在返回类型和访问权限之间加上关键字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.MulticastDelegateSystem.Delegate的单个直接子类。C#禁止从DelegateMulticastDelegate。当使用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 条件运算符( ?. )会短路,这意味着不会尝试调用委托方法。

小结

通过在设计中使用委托,不同的组件可以非常松散地耦合在一起。 这样可提供多种优势。 可轻松创建新的输出机制并将它们附加到日志系统中。这些机制只需要一种方法:编写日志消息的方法。这种设计在添加新功能时有非常强的弹性。任何编写者只需要实现同一种参数和返回值的方法。该方法可以是静态方法或实例方法。可以是公共的,私有的或其他任何合法的访问权限。

C#中的委托和事件(一)——delegate的更多相关文章

  1. C# 中的委托和事件

    觉得这篇文章写的非常好,大神之作,由简入繁,对我这种初学者来说帮忙很大,特此留存下. 摘自:http://tracefact.net/CSharp-Programming/Delegates-and- ...

  2. C# 中的委托和事件(转)

    引言 委托 和 事件在 .Net Framework中的应用非常广泛,然而,较好地理解委托和事件对很多接触C#时间不长的人来说并不容易.它们就像是一道槛儿,过了这个槛的人,觉得真是太容易了,而没有过去 ...

  3. C# 中的委托和事件(转载)

    引言 委托 和 事件在 .Net Framework中的应用非常广泛,然而,较好地理解委托和事件对很多接触C#时间不长的人来说并不容易.它们就像是一道槛儿,过了这个槛的人,觉得真是太容易了,而没有过去 ...

  4. 【转】C# 中的委托和事件

    阅读目录 C# 中的委托和事件 引言 将方法作为方法的参数 将方法绑定到委托 事件的由来 事件和委托的编译代码 委托.事件与Observer设计模式 .Net Framework中的委托与事件 总结 ...

  5. C#的委托和事件(delegate)

    using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Dele ...

  6. 第3章 C#中的委托和事件

    .NET框架中的委托和事件 using System; using System.Collections.Generic; using System.Linq; using System.Text; ...

  7. 分分钟用上C#中的委托和事件之窗体篇

    上次以鸿门宴的例子写了一篇名为<分分钟用上C#中的委托和事件>的博文,旨在帮助C#初学者迈过委托和事件这道坎,能够用最快的速度掌握如何使用它们.如果觉得意犹未尽,或者仍然不知如何在实际应用 ...

  8. 《C#高级编程》学习笔记------C#中的委托和事件(续)

    本文转载自张子阳 目录 为什么要使用事件而不是委托变量? 为什么委托定义的返回值通常都为void? 如何让事件只允许一个客户订阅?(事件访问器) 获得多个返回值与异常处理 委托中订阅者方法超时的处理 ...

  9. c#中的委托和事件(转)

    引言 委托 和 事件在 .Net Framework中的应用非常广泛,然而,较好地理解委托和事件对很多接触C#时间不长的人来说并不容易.它们就像是一道槛儿,过了这个槛的人,觉得真是太容易了,而没有过去 ...

  10. C#中的委托和事件(续)

    转自张子阳的博客http://www.tracefact.net/CSharp-Programming/Delegates-and-Events-Advanced.aspx 引言 如果你看过了 C#中 ...

随机推荐

  1. jQuery、JS读取xml文件里的内容(JS先通过document.implementation.createDocument方法将xml转换成document对象,jQuery将读取到的xml转成table)

    xml文件:test.xml <?xml version="1.0"?> <note> <to>George</to> <fr ...

  2. xgboost调参过程

    from http://blog.csdn.net/han_xiaoyang/article/details/52665396

  3. 线程优先级队列( Queue)

    Python的Queue模块中提供了同步的.线程安全的队列类,包括FIFO(先入先出)队列Queue,LIFO(后入先出)队列LifoQueue,和优先级队列PriorityQueue.这些队列都实现 ...

  4. PHP+FLASH大文件断点续传功能分享

    1.使用PHP的创始人 Rasmus Lerdorf 写的APC扩展模块来实现(http://pecl.php.net/package/apc) APC实现方法: 安装APC,参照官方文档安装,可以使 ...

  5. 容器————map

    序列容器是管理数据的宝贵工具,但对大多数应用程序而言,序列容器不提供方便的数据访问机制.一种典型的方法是通过名称来寻找地址.如果记录保存在序列容器中,就只能通过搜索得到这些数据.相比而言,map 容器 ...

  6. MySQL_DDL操作

    简单的来说DLL就是对数据库的C(Create)R(Retrieve)U(Update)D(Delete) 1.数据库的创建 (1)创建:create database 数据库名:当数据库已经存在则会 ...

  7. easyui 功能栏onclick传递object参数

    { field: 'Delete', title: '操作', width: 60, formatter: function (value, row, index) { var jrow = []; ...

  8. linux命令之查找find &grep

    区别:find找目录下的文件:find+目录路径+条件表达式,grep找文件中的行:grep+匹配正则表达式+文件名 find命令 find命令的一般形式 find命令的常用选项及实例 find与xa ...

  9. rsync aws ec2 pem

    How to use aws ec2 pem http://www.anthonychambers.co.uk/blog/rsync-to-aws-ec2-using-.pem-key/9 方法如下: ...

  10. @清晰掉 sprintf sscanf双胞胎

    sprintf() 格式化输出函数(图形) 功能: 函数sprintf()用来作格式化的输出.用法: 此函数调用方式为int sprintf(char *string,char *format,arg ...