隐藏

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

发布:2021/10/23 10:39:05作者:管理员 来源:本站 浏览次数:819


前言


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

小结

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


下一篇我们讲讲事件