zoukankan      html  css  js  c++  java
  • 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}	{s}	{component}	{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 条件运算符( ?. )会短路,这意味着不会尝试调用委托方法。

    小结

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

    下一篇我们讲讲事件

  • 相关阅读:
    【洛谷P3389】【模板】高斯消元
    【NOIP2016】提高组
    【NOIP2013】提高组
    【NOIP2012】提高组
    【NOIP2011】提高组
    【NOIP2010】提高组
    【NOIP2009】提高组
    【NOIP2008】提高组
    【NOIP2007】提高组
    【51nod 1189】阶乘分数——阶乘质因数分解
  • 原文地址:https://www.cnblogs.com/czjk/p/12050035.html
Copyright © 2011-2022 走看看