C#中的委托是什么-上(基础知识)

参考资料:
《C#7.0核心技术指南》4.1,4.2

首先明确一个概念“目标方法”:把一个或多个方法赋值给一个委托,则称这个或这些方法是该委托的“目标方法”。

什么是委托
用委托编写插件方法
多播委托
实例目标方法和静态目标方法
泛型委托类型
Func和Action委托
委托的兼容性
什么是逆变
什么是协变
泛型委托类型的参数协变
题外话

什么是委托

委托(delegate)从表面上看就是一个知道如何调用方法的对象,这个对象是调用者的代理。调用者调用委托,委托调用目标方法,解耦了调用者和目标方法。

一个简单的委托例子:

delegate int Transformer(int x);

class Program
{
    static void Main(string[] args)
    {
        Transformer t = Square; // Square方法是t委托的目标方法
        // 等价于 Transformer t = new Transformer(Square);

        int result = t(3);
        // 等价于 int result = t.Invoke(3)

        Console.WriteLine(result);
    }

    static int Square(int x) => x * x;
}

上面的Transformer兼容任何返回类型为int并有一个int类型的参数的方法,但实际应用中委托的返回类型多为void,后面会解释。委托跟回调类似。(回调函数就是一个通过函数指针调用的函数。)

我作为一个初级程序猿,接触的编程场景不多,了解了委托之后,很难想到这种东西在什么时候用比较好。下面介绍一下委托的使用场景。

用委托编写插件方法

这个例子来自《C#7.0核心技术指南》4.1.1,我进行了些许改动,让人类更好的理解。


// 声明接受一个int参数,返回int值的委托
delegate int Transformer(int x);

class Util
{
    // 该方法的第二个参数接受一个Transformer委托
    // 该方法用该委托来处理values的每一个值
    // 也就是说如果第二个参数传入一个Square方法,则Transform可以对values的每个值进行平方
    public static void Transform(int[] values, Transformer t)
    {
        for (int i = 0; i < values.Length; i++)
        {
            values[i] = t(values[i]);
        }
    }
}

class Program
{
    static void Main(string[] args)
    {
        int[] values1 = { 1, 2, 3 }; 

        Util.Transform(values1, Square);
        // 传入的第二个参数是Square方法,相当于Transformer t = Square

        foreach (int i in values1)
        {
            Console.Write(i + " "); // 1 4 9
        }

        Console.WriteLine();

        int[] values2 = { 4, 5, 6 };

        Util.Transform(values2, Cube);
        // 传入的第二个参数是Cube方法,相当于Transformer t = Cube


        foreach (int i in values2)
        {
            Console.Write(i + " "); // 64 125 216
        }
    }

    static int Square(int x) => x * x;
    static int Cube(int x) => x * x * x;
}

书中并没有给出过多介绍,对初学者不够友好,我简单介绍一下。

Util类的Transform方法的第二个参数是一个Transformer委托。Transform方法的作用就是用Transformer委托来处理values的每一个值。这样的话,我们可以根据需要,定义可以委托给Transformer的方法(在这里是需要接受int参数,返回int值的方法)。

我在代码底部定义了接受一个int参数,返回一个int值的Square方法和Cube方法,作用分别是求传入的int参数的平方和三次方并返回。

到这里,我们终于可以理解“插件方法”的意思,也就是方法作为插件插入到另一个方法中。可以像上面代码中一样,准备多个插件方法,在需要平方的场景给Transform传入Square,在需要三次方的场景给Transform传入Cube。

Transform方法是一个高阶函数(high-order function),以函数作为参数,或者返回的参数是一个委托,这种方法被称为高阶函数。

多播委托

一个委托可以引用一个目标方法,也可以引用多个目标方法。

SomeDelegate d = SomeMethod1

d += SomeMethod2 // 现在调用d不仅会调用SomeMethod1而且会调用SomeMethod2。委托会按照添加的顺序依次触发

d -= SomeMethod1 // 现在调用d只会调用SomeMethod2,因为已经把SomeMethod1从该委托中删除了

给委托赋值为null,或者把委托减到null都是允许的。

委托是不可变的,因此调用+=和-=的实质是创建一个新的委托实例,并把它赋值给已有变量。

如果一个多播委托拥有非void的返回类型,则调用者将从最后一个触发的方法接收返回值。前面的方法仍然调用,但是返回值都会被丢弃。大部分调用多播委托的情况都会返回void类型,因此这个细小的差异就没有了。

所有的委托类型都是从System.MulticastDelegate类隐式派生的。而System.MulticastDelegate继承自System.Delegate。C#将委托中的+、-、+=、-=运算符都编译成了System.Delegate的静态Combine和Remove方法。

多播委托的一个使用场景是,可以定义两个日志方法,一个把日志写到控制台,一个把日志写到文件。使用多播委托实现同时往控制台和文件输出日志的功能。

实例目标方法和静态目标方法

到目前位置,我们给委托对象的目标方法例如Square和Cube,全都是static方法。

将一个实例(非静态)方法赋值给委托对象时,该委托对象不但要维护方法的引用,还需要维护方法所属的实例的引用。

我始终不理解什么叫“维护方法的引用”和“维护方法所属的实例的引用”,这里的“维护”是什么意思?希望懂的大佬能在评论区指教我一下,不胜感激!

System.Delegate类的Target属性代表这个实例(如果委托引用的是一个静态方法,则该属性值为null)

也就是说,非静态的方法也可以委托给委托对象,需要先取得一个该方法所属类的对象。例如类ClassA的实例方法MethodA,有一个委托t,把这个方法委托给这个对象的方式就是:

ClassA objectA = new ClassA();

Transformet t = objectA.MethodA;

// 此时 t.Target == objectA,t.Method = [方法返回值类型] MethodA([参数类型])

泛型委托类型

委托的返回类型和参数类型都可以是泛型。我们可以改造一下上面的Transform方法。之前我们的Transform方法只能接受[]int类型的参数并使用委托对象来进行处理,现在我们改造成对任何类型都有效的Transform方法,接受T[]类型的参数:

public delegate T Transformer<T> (T arg);

class Util
{
    public static void Transform<T> (T[] values, Transformer<T> t)
    {
        for (int i = 0; i < values.Length; i++)
        {
            values[i] = t(values[i]);
        }
    }
}

代码如上。现在Transform方法依旧可以接受之前定义的Square和Cube方法作为第二个参数。

Func和Action委托

有泛型委托之后,在System包里有Func和Action委托,Func委托可以接受任意数目的参数,有任意的返回类型。Action委托返回类型为void。这两个是通用的委托。

关于Func委托和Action委托,我会在下一篇博客里重点分析,因为会用这两个委托才是实际开发中的重中之重。

我们尝试用接受T类型参数并返回T类型值的Func委托来代替之前的Transformer委托:

class Util
{
    public static void Transform<T> (T[] values, Func<T,T> t)
    {
        for (int i = 0; i < values.Length; i++)
        {
            values[i] = t(values[i]);
        }
    }
}

委托的兼容性

签名相似的委托也无法互相兼容,但可以用另一种写法:

delegate void D1();
delegate void D2();

D1 d1 = Method1;

D2 d2 = d1; // 错误
D2 d2 = new D2(d1); // 正确

如果两个委托实例赋予同一个方法,则可以用==来判断两个委托实例,而且结果为True。

什么是逆变

委托可以有比它的目标方法的参数类型更具体的参数类型,这称为逆变。

跟普通方法一样,普通方法的参数类型也可以比它的签名里的参数类型更加具体。当调用方法时,可以给方法的参数提供更加特定的变量类型,这是正常的多态行为。

delegate void StringAction(string s);

class Program
{
    static void Main(string[] args)
    {
        // 委托类型StringAction的参数类型是string,该委托sa的目标方法的参数类型却是object
        // string比object更具体
        StringAction sa = ActOnObject; 
        sa("hello");
    }

    static void ActOnObject(object o) => Console.WriteLine(o);
}

上面代码中,委托StringAction的参数类型是string,该委托的目标方法的参数类型却是object,string比object更具体。这就是逆变。

注意,sa无法接受string以外的参数,比如把"hello"换成int型的数字1,就会报错。

委托仅仅替其他人调用方法。在本例中,在调用StringAction时,参数类型是string。当这个参数传递给目标方法ActOnObject时,参数隐式向上转换为object。

标准事件模式的设计宗旨是通过使用公共的EventArgs基类来利用逆变特性。例如,可以用两个不同的委托调用同一个方法,一个传递MouseEventArgs而另一个则传递KeyEventArgs。

什么是协变

委托的目标方法可能返回比委托声明的返回值类型更加特定的返回值类型,这称为协变。

对比一下方法,当调用方法时,可以给方法的参数提供更加特定的变量类型,这是正常的多态行为。

delegate object ObjectRetriever();

class Program
{
    static void Main(string[] args)
    {
        // 委托类型ObjectRetriever的返回值类型是object,但该委托o的目标方法的返回值类型却是string
        // 定义一个object类型的变量,可以接受o的返回值
        ObjectRetriever o = RetrieveString;
        object result = o();
        Console.WriteLine(result);
    }

    static string RetrieveString() => "hello";
}

ObjectRetriever期望返回一个object。但若返回object子类也是可以的,这是因为委托的返回类型是协变的。

泛型委托类型的参数协变

要定义一个泛型委托类型,最好参考如下的准则:

  • 将只用于返回值类型的类型参数标记为协变(out)。
  • 将只用于参数类型的任意类型参数标记为逆变(in)。

这样可以依照类型的继承关系自然地进行类型转换。

以下委托类型拥有协变返回值类型参数,并被标记为out,可以实现下面两行的操作。即可以将返回值类型更具体的委托赋值给返回值类型更抽象的委托。

delegate TResult Func<out TResult>();

Func<string> x = ...;
Func<object> y = x;

以下委托拥有逆变类型参数T,并被标记为in,可以执行下面两行的操作,即可以将参数类型更抽象的委托赋值给参数类型更具体的委托:

delegate void Action<in T>();

Func<object> x = ...;
Func<string> y = x;

题外话

本来我是想通过官方文档来学习的,但有一部分官方文档读起来真的是味同嚼蜡,例如下面这段:

看完这句,我都想给文档译者跪下,薛定谔看了都直呼内行。众所周知,微软文档里如果有机翻内容,是会在文档顶部提示的,这篇文档上面并没有提示,说明文档都是人类翻译的......

后来我实在受不了了,就查阅了《C#7.0核心技术指南》,但实际上这个书也很拗口很难懂,还是要学好英文,看英文材料的时候尽量看原版,有余力的话再翻译成通俗易懂的汉语版本回馈社区,没有任何崇洋媚外的意思。

原文地址:https://www.cnblogs.com/Kit-L/p/13861537.html