C#中的委托和事件(0) delegate

前言

来说一说委托(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 条件运算符( ?. )会短路,这意味着不会尝试调用委托方法。

小结

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

下一篇我们讲讲事件

posted @ 2019-12-16 17:03  CrazyJack  阅读(1031)  评论(0编辑  收藏  举报