C# 精通之路 —— [从0到1] 探索委托技术,这些细节知识你知多少?

这里是引用
作者:浪子花梦,一个有趣的程序员 ~
此文详细的讲解委托的各方面技术,文章可能有点长,希望你能看完 ^ _ ^


文章目录


在这里插入图片描述

初识委托(示例代码讲解)

开始讲解之前,让我们看看什么是回调函数,下面是我在度娘上面Copy的一段话:

当程序跑起来时,一般情况下,应用程序(application program)会时常通过API调用库里所预先备好的函数。但是有些库函数(library function)却要求应用先传给它一个函数,好在合适的时候调用,以完成目标任务。这个 被传入 的、后 又被调用 的函数就称为 回调函数(callback function)。

回调函数是一种非常有用的编程机制,.net 平台通过 委托 来提供回调函数机制。

委托有许多有用的功能,比如下面几种做些:

  1. 委托确保回调方法是类型安全的(CLR 最重要的目标之一)
  2. 顺序调用多个方法
  3. 支持调用静态方法、实例方法

为了使我们快速的理解委托,让我们先来看看如何的使用它。下面的代码演示了我们如何的声明委托创建委托使用委托

// 定义一个委托类型,接收 Int32,返回 void
internal delegate void Feedback(Int32 value);

class Program
{
    static void Main(string[] args)
    {
        StaticDelegateDemo();           // 静态方法委托调用示例
        InstanceDelegateDemo();         // 实例方法委托调用示例
    }

    private static void StaticDelegateDemo()
    {
        Console.WriteLine("----- Static Delegate Demo -----");
        Counter(1, 3, null);

        // 回调一个静态方法
        Counter(1, 3, new Feedback(Program.FeedbackToConsole));
        Console.WriteLine();
    }

    private static void InstanceDelegateDemo()
    {
        Console.WriteLine("----- Static Instance Demo -----");
        Program p = new Program();

        // 回调一个实例方法
        Counter(1, 3, new Feedback(p.FeedbackToFile));
        Console.WriteLine();
    }



    private static void Counter(Int32 from, Int32 to, Feedback fb)
    {
        for (Int32 val = from; val <= to; val++)
        {
            // 判断是否有回调函数存在
            if (fb != null)
            {
                fb(val);    // == fb.Invoke(val)
            }
        }
    }

    private static void FeedbackToConsole(Int32 value)
    {
        Console.WriteLine("Item = " + value);
    }

    private void FeedbackToFile(Int32 value)
    {
        // 向一个文件中写入数据
        using (StreamWriter sw = new StreamWriter("Status.txt", true))
        {
            sw.WriteLine("Item = " + value);
        }
    }
}

上面的代码中,我们演示了委托的最常用的方法,其中的委托定义如下所示:

// 定义一个委托类型,接收 Int32,返回 void
internal delegate void Feedback(Int32 value);

它的回调方法接收一个 Int32类型的参数,并返回 void型 .

其中有两处地方进行了委托的创建,如下所示:

// 回调一个静态方法
Counter(1, 3, new Feedback(Program.FeedbackToConsole));
// 回调一个实例方法
Counter(1, 3, new Feedback(p.FeedbackToFile));

红色部分就是创建了委托的对象,并指定了一个回调方法,他们分别是静态方法,实例方法 . . .

委托的调用方法如下所示:
在这里插入图片描述
首先,我们判断委托对象是否有回调函数的存在,如果有的话,我们直接利用上面这种方式进行调用回调函数,也就是用那本身的样子进行调用:

fb.Invoke(val);

其中 Main主函数中第一个方法的结果如下所示:

----- Static Delegate Demo -----
Item = 1
Item = 2
Item = 3

第二个方法是在一个文件中查看,结果如下所示:
在这里插入图片描述

回调方法的逆变与协变

将方法绑定到委托时,C# 和 CLR 允许引用类型的逆变与协变性,规则如下所示:

  • 逆变性: 方法获取的参数可以是委托参数类型的基类
  • 协变性: 方法返回的类型可以是委托返回类型的派生类

示例代码如下所示:

// 定义一个委托类型,接收 Int32,返回 void
internal delegate Object Feedback(StreamWriter stm);

class Program
{
    static void Main(string[] args)
    {
        Feedback fb = new Feedback(SayHello);

        String str = fb.Invoke(null).ToString();

        Console.WriteLine(str);
    }

    // TextWriter是StreamWriter 的基类  String是Object的派生类
    private static String SayHello(TextWriter sw)
    {
        using (sw = new StreamWriter("huameng.txt", false))
        {
            sw.WriteLine("langzihuameng");
        }

        return "数据存储成功!";
    }
}

文件中的结果如下所示:
在这里插入图片描述


委托深层探索

我们学习一门语言,不只是简单的会用就行了,我们还需要了解语言背后发生的一些事,下面我们就来看看编译器和CLR在委托背后做的一些事吧 . . .

首先,让我们看看下面这一句代码:

internal delegate void Feedback(Int32 value);

表面上看它只是一行简单的代码,但编译器是定义了一个完整的类,我们打开 ILDASM工具查看这个类如下所示,里面有一个构造器InvokeBeginInvokeEndInvoke
在这里插入图片描述

此文章讲解 构造方法Invoke,另外两个方法以后再说(主要书上没讲,^ _ ^,异步操作 . . .)

我们从上面的反编译工具可以看出来,这个类是从 MulticastDelegate 这个类派生而来,而这个类有三个非常重要的非公共字段:

  1. _target:回调方法的对象,如果是静态方法,则字段为 null
  2. _methodPtr:标识回调方法
  3. _invocationList:构造委托链时用它引用一个数组(下节所讲)

下面我们来看看委托对象在创建时,这些字段的值发生了什么变化,如下所示创建两个委托对象:

Feedback fbStatic = new Feedback(Program.FeedbackToConsole);

Feedback fbInstance = new Feedback(new Program().FeedbackToFile);

一个是以静态方法构造,另一个是以实例方法进行构造,我们来看看字段的值发生了什么:
在这里插入图片描述
当委托对象被构造时,他的内部结构就是这样变化 . . .

我们再来看看 Invoke方法,之前调用委托方法的方式是下面这样的:

private static void Counter(Int32 from, Int32 to, Feedback fb)
{
	for (Int32 val = from; val <= to; val++)
	{
		// 判断是否有回调函数存在
		if (fb != null)
		{
			fb(val);   
		}
	}
}

其中 fb(val); 的本身是一个语法糖,他等价于 fb.Invoke(val); 这一行代码 . . .

下面我们再讲讲委托其它方面的知识 . . .


在这里插入图片描述


委托链详解

文章开头我们说过委托的几个好处之一:顺序调用多个方法

委托链就是委托对象的集合,我们先来看看委托对象的集合方式是如何操作的,代码如下所示:

internal delegate void Feedback(Int32 value);

class Program
{
    static void Main(string[] args)
    {
        ChainDelegateDemo1(new Program());
        ChainDelegateDemo2(new Program());
    }

    private static void ChainDelegateDemo1(Program p)
    {
        Console.WriteLine("------- 委托链演示 1 -------");

        Feedback fb1 = new Feedback(FeedbackToConsole);
        Feedback fb2 = new Feedback(p.FeedbackToFile);

        Feedback fbChain = null;
        fbChain = (Feedback)Delegate.Combine(fbChain, fb1);
        fbChain = (Feedback)Delegate.Combine(fbChain, fb2);

        // 从委托链中 删除一个回调方法
        //fbChain = (Feedback)Delegate.Remove(fbChain, fb1);

        Counter(1, 2, fbChain);

    }

    private static void ChainDelegateDemo2(Program p)
    {
        Console.WriteLine("------- 委托链演示 2 -------");

        Feedback fb1 = new Feedback(FeedbackToConsole);
        Feedback fb2 = new Feedback(p.FeedbackToFile);

        Feedback fbChain = null;
        fbChain += fb1;
        fbChain += fb2;

        // 从委托链中 删除一个回调方法
        //fbChain -= fb2;

        Counter(1, 2, fbChain);
    }

    private static void Counter(Int32 from, Int32 to, Feedback fb)
    {
        for (Int32 val = from; val <= to; val++)
        {
            // 判断是否有回调函数存在
            if (fb != null)
            {
                fb(val);    // == fb.Invoke(val)
            }
        }
    }

    private static void FeedbackToConsole(Int32 value)
    {
        Console.WriteLine("Item = " + value);
    }

    private void FeedbackToFile(Int32 value)
    {
        // 向一个文件中写入数据
        using (StreamWriter sw = new StreamWriter("Status.txt", true))
        {
            sw.WriteLine("Item = " + value);
        }
    }
}

其中的ChainDelegateDemo1 和 ChainDelegateDemo2 方法是我们新定义的,它们用来演示委托链是怎么构建的,这两个方法没有什么区别,只是第二种方法更加简单了而已,我们将在下下节的语法糖中讲解 . . .

这两个方法中的 fb1 与 fb2创建之后,它们在底层中代表的数据如下所示:
在这里插入图片描述
以 ChainDelegateDemo1 方法为例,当我们将 fbChain(此时为 null) 与 fb1 中的委托对象链接在一起之后,关系是如下这样的(就像是 fbChain 指向了 fb1一样):
在这里插入图片描述
当我们再将 fbChain 与 fb2 中的委托对象链接在一起之后,关系是如下这样的:
在这里插入图片描述
此时委托链已经形成, fbChain 已经指向了两个委托对象,它们分别是 fb1 与 fb2 . . .

对委托链的控制

当我们使用 Invoke 访问这个委托链时,有着许多的局限性,那么这个局限性到底是什么呢?
比如说这些回调方法都是有一个返回值的,但是访问委托链时,最终结果只能返回最后一个委托对象的返回值,而其它的返回值我们就无法的掌控了,还有一个问题,当某一个委托对象出问题之后,比如说异常、阻塞等,后面的所以委托对象都将无法访问 . . .
.
还好, MulticastDelegate 类为我们提供了一个实例方法:GetInvocationList,我们可以用它来显式的调用链中的每一个委托对象,可以对它们进行任何有意义的操作,下面我们就来看看在实例中,这个方法是如何的使用吧 . . .

首先,我们先定义如下的几个类,每个类中都有一个方法,这些方法用作回调方法测试:

// 电话
internal sealed class Phone
{
    public String SayHello()
    {
        return "I'm is Phone";
    }
}

// 苹果
internal sealed class Apple
{
    // 这个苹果是坏的(引发异常,用于测试委托链)
    public String SayHello()
    {
        throw new InvalidOperationException("This apple is bad");
    }
}

internal sealed class Girl
{
    public String SayHello()
    {
        return "My Name Is XiaoMei!";
    }
}

当我们像下面这样调用委托链的时候,会引发异常:

ChainDemo chainDemo = null;
chainDemo += new ChainDemo(new Phone().SayHello);    
chainDemo += new ChainDemo(new Apple().SayHello);    // 会引发异常
chainDemo += new ChainDemo(new Girl().SayHello);

chainDemo.Invoke();		// 访问所有的委托对象

效果如下所示:
在这里插入图片描述
因为委托链中的某个委托对象出问题了!在这个示例中我们很明显的知道是谁,但我们不知道的情况下呢?这种结果岂不是很糟糕? 所以我们使用 GetInvocationList 方法就很好的解决了这个问题,使用情况如下所示:

ChainDemo chainDemo = null;
chainDemo += new ChainDemo(new Phone().SayHello);    
chainDemo += new ChainDemo(new Apple().SayHello);    // 会引发异常
chainDemo += new ChainDemo(new Girl().SayHello);

Delegate[] arrDelegates = chainDemo.GetInvocationList();

foreach (ChainDemo p in arrDelegates)
{
	try
	{
		Console.WriteLine(p.Invoke());                  // 如果没有问题,就输出结果
		Console.WriteLine();
	}
	catch(InvalidOperationException e)
	{
		Console.WriteLine();
		Console.WriteLine(p.Target.GetType().Name);     // 获取委托中的 _object
		Console.WriteLine(p.Method.Name);               // 获取委托回调方法名称
		Console.WriteLine(e.Message);                   // 异常消息信息
		Console.WriteLine();
	}
}

其中我们在访问委托链的时候,加了一个异常捕捉,这样我们就可以把委托链给访问完了,最终的结果如下所示:
在这里插入图片描述
我们不仅处理了有异常的情况,而且还输出每个委托对象的返回值 . . .

委托的讲解到此为止,下面我们在讲解其它方法的知识吧 . . .
在这里插入图片描述


泛型委托

泛型委托的定义和泛型方法、泛型类、泛型接口的定义方式是差不多,如下所示,我们演示了一个泛型委托:

internal delegate void Huameng<T>(T value);

class Program
{
    static void Main(string[] args)
    {
        Huameng<Int32> huameng = null;
        huameng += new Huameng<Int32>(SayHello);
        huameng.Invoke(250);

        Huameng<String> Langzi = null;
        Langzi += new Huameng<String>(SayHello);
        Langzi.Invoke("250");
    }

    private static void SayHello<T>(T value)
    {
        if (value is Int32)
        {
            Console.WriteLine("I'm is Int32!");
        }
        else if (value is String)
        {
            Console.WriteLine("I'm is String");
        }
    }
}

由于程序比较简单,我就不讲解了,结果输出如下所示:
在这里插入图片描述
注意的几点知识:

1)委托类型是可以定义可变参数的,像如下方式即可:

internal delegate void Huameng<T>(T value, params Int32[] arr);

2).net(.NET Framework)提供了两种可以直接使用的泛型委托类型:

  • Action:不具有返回值
  • Func:具有返回值

这两种委托类型的使用方式如下所示:

static void Main(string[] args)
{
    Action<Int32, Int32, Int32> action = new Action<Int32, Int32, Int32>(SayHello);
    action.Invoke(2, 5, 0);

    Func<String, Int32, String> func = new Func<string, int, string>(SayName);
    String str = func("langzihuameng", 20);
    Console.WriteLine(str);
}
private static String SayName(String str, Int32 num)
{
    return str + " " + num.ToString();
}

private static void SayHello<T>(T value, T t1, T t2)
{
    Console.WriteLine(value.ToString());
    Console.WriteLine(t1.ToString());
    Console.WriteLine(t2.ToString());
}

结果如下所示:
在这里插入图片描述
如果我们想给委托传递带引用的参数,上面这两个类型是不可以用的,我们只能使用自己定义的委托类型,比如下面这样的:

internal delegate void DemoArgs(ref Int32 value);

3)泛型实参有返回值的委托支持逆变和协变,例如下面这种情况:
在这里插入图片描述
这两行代码是不是有点眼熟呢? 它在讲解泛型一文有讲到,有兴趣的小伙伴可以看看我其它的 C#文章,而且这是个逆变与协变在上面也讲到过 . . .

在这里插入图片描述


使用委托的几种语法糖

下面我将演示几种C#提供的针对委托的简化语法,让你感受一下高级语言的强大 . . .

1)不需要构造委托对象,直接传入回调函数
我们看看下面的代码,为了让主线程中的 Hello,World!显示,我让程序睡眠 1 秒:

static void Main(string[] args)
{
    ThreadPool.QueueUserWorkItem(SayHello);

    Thread.Sleep(1000);

    Console.WriteLine("Hello, World!");
}

private static void SayHello(Object obj)
{
    Console.WriteLine("Hello, World!");
}

QueueUserWorkItem 方法接收一个 WaitCallback 的委托对象,如下所示:
在这里插入图片描述
但我们的程序之中,可以允许直接传递回调方法,而不需要定义委托对象,CLR 会为我们自己声明对象 . . .

程序的结果如下所示:
在这里插入图片描述

.

2)lambda 表达式
lambda 表达式的出现,意味着我们将可以随定义匿名的回调方法,使用情况如下所示:
在这里插入图片描述
这样的写法是非常的方便的,但这种方式会使我们经常的滥用,我们建议 lambda表达式中的代码句不超过三行,如果超过三行,则自己定义一个方法,用于回调 . . .

C# 编译器会为 lambda表达式自动转换成为一个方法,方法名我们不得而知 . . .

结果如下所示:
在这里插入图片描述

下面我们来演示一下 lambda表达式的常见用法,代码如下所示:
在这里插入图片描述

第一个 Func类型,我们可以自动推断参数的类型,将lambda表达式的内容只有一条语句行,它会自动返回,另外要想将参数设置 ref 或者 out,我们必须使用自定义的委托类型 . . .

结果如下所示:
在这里插入图片描述

3)随意的调用局部变量
委托方法中还支持引用方法中的局部参数或者是变量,像下面这样,我们在 lambda表达式中使用方法中的局部变量:count,示例如下所示:
在这里插入图片描述
在引用局部变量时,C# 实际上是隐式的定义了一个类,我们打开 ildasm工具查看如下:
在这里插入图片描述

我们需要知识 C# 编译器 和 CLR 为我们做了什么事情,这样我们才能更好的成为 .net 大师 . . .

在这里插入图片描述


委托和反射(控制台命令演示)

如果我们在程序之中,不知道回调方法需要多少个参数,以及参数的具体类型,那么我们如何的使用哪个委托类型,这种选择就受到了限制,还好,C# 为我们提供了两个方法:

  • CreateDelegate
  • DynamicInvoke

其中,CreateDelegate 方法允许在编译时不知道委托的所有必要信息的前提下创建委托;
DynamicInvoke 方法用于调用委托对象的回调方法 . . .

下面我们在实例中来学习一下委托与反射的关系(一个完整的代码):

using System;
using System.Linq;
using System.Reflection;


// 两个不同类型的委托
internal delegate Object TwoInt32s(Int32 n1, Int32 n2);
internal delegate Object OneString(String s1);

class Program
{
    static void Main(string[] args)
    {
        // 如果命令行参数 < 2,则作出提示
        if (args.Length < 2)
        {
            String usage =
                @"{0}Usage: " +
                "{0} delType methodName [Arg1] [Arg2]" +
                "{0}    where delType is TwoInt32s, methodName must be Add of Subtract " +
                "{0}    if delType is OneString, methodName must be NumChars or Reverse " +
                "{0}" +
                "{0}Examples:" +
                "{0}    {1} TwoInt32s Add 123 321" +
                "{0}    {1} TwoInt32s Subtract 123 321" +
                "{0}    {1} OneString NumChars \"Hello there\"" +
                "{0}    {1} OneString Reverse \"Hello there\"{1}";

            Console.WriteLine(usage, Environment.NewLine, Environment.NewLine);
            return;
        }

        // 将 args[0] 转化为委托类型
        Type delType = Type.GetType(args[0]);
        if (delType == null)
        {
            Console.WriteLine("Invalid delType argument: " + args[0]);
            return;
        }


        Delegate d;
        try
        {
            // 将 args[1] 转化为方法
            MethodInfo mi = typeof(Program).GetTypeInfo().GetDeclaredMethod(args[1]);

            // 从mi这个方法创建 delType类型的委托
            d = mi.CreateDelegate(delType);
        }
        catch (ArgumentException)
        {
            Console.WriteLine("Invalid methodName argument: " + args[1]);
            return;
        }


        // 存放传给方法的参数
        Object[] callbackArgs = new Object[args.Length - 2];    


        // 如果是 TwoInt32s的委托 执行的操作
        if(d.GetType() == typeof(TwoInt32s))
        {
            try
            {
                for (Int32 i = 2; i < args.Length; i++)
                {
                    // String --> Int32
                    callbackArgs[i - 2] = Int32.Parse(args[i]);
                }
            }
            catch (FormatException)
            {
                // 必须是 Int32型的数据
                Console.WriteLine("Parameters must be integers.");
                return;
            }
        }

        // 如果是 OneString的委托 执行的操作
        if (d.GetType() == typeof(OneString))
        {
            // 复制 String
            Array.Copy(args, 2, callbackArgs, 0, callbackArgs.Length);  
        }


        // 显示结果
        try
        {
            // DynamicInvoke    执行委托对象对应的回调方法 
            Object result = d.DynamicInvoke(callbackArgs);
            Console.WriteLine("Result = " + result);
        }
        catch (TargetParameterCountException)
        {
            // 对应的方法参数不正确
            Console.WriteLine("Incorrect number of parameters specified.");
            return;
        }

    }

    // 两个参数
    private static Object Add(Int32 n1, Int32 n2)
    {
        return n1 + n2;
    }

    private static Object Subtract(Int32 n1, Int32 n2)
    {
        return n1 - n2;
    }

    // 一个参数
    private static Object NumChars(String s1)
    {
        return s1.Length;
    }

    private static Object Reverse(String s1)
    {
        return new String(s1.Reverse().ToArray());
    }
}

我们使用控制台来使用这个程序,如下所示:

1)当我们随便输出点东西时,它会教给你正确的用法:
在这里插入图片描述
2)当我们输出正确的用法之后,如下所示:
在这里插入图片描述

控制台的命令挺好玩的,大家可以自己试一试 . . .

.
.
.


猜你喜欢

转载自blog.csdn.net/weixin_42100963/article/details/107460580