Learning Hard C# 学习笔记: 8.C#中的特性 - 委托

发布时间 2023-10-05 22:16:39作者: ErgoCogito

前几章, 讲的都是面向对象语言共同的内容, 本章开始是C#的独有特性 - 委托.

委托是C#最重要的特性之一,C#后面的所有特性基本都是建立在委托的基础上的.

8.1 C#委托是什么

例如, 法庭上律师为当事人辩护, 他真正执行的是当事人的陈词, 律师就相当于一个委托对象, 而当事人则委托律师对象为自己辩护.

C#中的委托可以比作律师对象, 它是一个类, 我们可以将其理解为函数的一个包装, 它使得C#中的函数可以作为参数来传递.

委托的定义方法如下:

public delegate void MyDelegate(int para1, string para2);

委托能包装的方法是有一定限制的, 例如被上面MyDelegate包装的方法需要满足以下条件:

  • 方法的返回类型必须为void;
  • 方法必须有两个参数, 并且第一个参数应为int类型, 第二个参数为string类型.

归纳起来, 可以被委托包装的方法必须满足以下规则:

  • 方法签名必须与委托一致, 方法签名包括参数的个数, 类型和顺序;
  • 方法的返回类型要和委托一致, 注意, 方法的返回类型不属于方法签名的一部分.

8.2 委托的使用

下面例子演示了委托的使用:

// 委托使用的演示
class Program
{
    // 1. 使用delegate关键字来定义一个委托类型
    delegate void MyDelegate(int para1, int para2);

    static void Main(string[] args)
    {
        // 2. 声明委托变量d
        MyDelegate d;
        // 3. 实例化委托类型,传递的方法也可以为静态方法,这里传递的是实例方法
        d = new MyDelegate(new Program().Add);

        // 4. 委托类型作为参数传递给另一个方法
        MyMethod(d);
        Console.Read();
    }

    // 该方法的定义必须与委托定义相同,即返回类型为void, 两个int类型的参数
    void Add(int para1, int para2)
    {
        int sum = para1 + para2;
        Console.WriteLine("两个数的和为:"+sum);
    }

    // 方法的参数是委托类型
    private static void MyMethod(MyDelegate mydelegate)
    {
        // 5.在方法中调用委托
        mydelegate(1,2);
    }
}

从以上代码可以看出, 使用委托步骤为: 定义委托类型→声明委托变量→实例化委托→作为参数传递给方法→调用委托.

  1. 定义委托类型:delegate void MyDelegate(type para1, type para2); 。其定义方式类似于方法的定义,只是多了一个delegate 关键字。
  2. 声明委托变量:MyDelegate d; 。既然委托是一种类型,那么可以使用委托来声明一个委托变量,相当于int a 。
  3. 实例化委托:d = new MyDelegate(obj.InstanceMethod); 。第二步只是声明了委托变量,但并没有将它实例化。类的实例化使用new 关键字来实现,而委托也属于类类型,所以委托的实例化也使用new 关键字来进行的。这里需要注意的是,委托的实例化是用一个方法名(不能带左右括号)作为参数,并且该方法的定义必须符合委托的定义,即该方法的返回类型、参数个数和类型必须与委托定义中的一样。这样,前面3步就好比构造了一个律师对象,而方法InstanceMethod 好比是当事人的方法。
  4. 作为参数传递给方法:MyMethod(d); 。委托使得在C#中,可以把一个方法作为另一个方法的参数,而委托可以看作是一个包装方法的对象。
  5. 在方法中调用委托。MyMethod 方法好比是法官,MyMethod 方法先调用委托,委托再调用方法InstanceMethod 。这个过程就如同法官向律师问话,律师真正陈诉的是当事人的情况。

最后的运行结果也是两个数的和

在使用中, 注意以下几个问题.

  • 在第3步中,被传递的方法的定义必须与委托定义相同,即方法的返回类型和参数个数、参数类型都必须与委托相同。并且,传递的是方法名,方法名后不能带有左右括号。
  • 在第5步中,委托的调用与方法调用类似,传递的实参类型和个数必须与委托定义一致。
  • 由于委托是方法的包装类型,所以对委托的调用也就是对其所包装的方法的调用,上面第5步实际上是调用了Add 方法来对传入的实参进行计算。

8.3 为什么要引入委托

前面代码中为什么不直接在MyMethod 方法里直接调用Add 方法,反而要实例化一个委托对象来完成调用呢?

委托使得一个方法可以作为另一个方法的参数进行传递,这就是委托最大的作用。下面再通过一个例子来诠释C#中引入委托的原因。

//不使用委托实现打招呼方法
public void Greeting(string name,string language)
{
    switch (language)
    {
        case "zh-cn":
            ChineseGreeting(name);
            break;
        case "en-us":
            EnglishGreeting(name);
            break;
        default:
            EnglishGreeting(name);
            break;
    }
}

// 英国人打招呼方法
public void EnglishGreeting(string name)
{
    Console.WriteLine("Hello,  "+name);
}

// 中国人打招呼方法
public void ChineseGreeting(string name)
{
    Console.WriteLine("你好, "+name);
}        

上实现方式的可扩展性很差,如果之后我们需要添加德国、日本等打招呼方法,就必须修改Greeting 方法内的case 语句,来适应新的需求,这样特别不方便。有了委托,我们就可以把函数作为参数,并像下面的代码这样去实现Greeting 方法了:

class Program
{
    static void Main(string[] args)
    {
        // 引入委托之后
        Program p = new Program();
        p.Greeting("李志", p.ChineseGreeting);
        p.Greeting("Tommy Li", p.EnglishGreeting);
        Console.Read();
    }

    public delegate void GreetingDelegate(string name); // 定义委托类型
    // 有了委托之后,可以像下面这样实现打招呼方法
    public void Greeting(string name, GreetingDelegate callback)
    {
        // 调用委托
        callback(name);
    }

    // 英国人打招呼方法
    public void EnglishGreeting(string name)
    {
        Console.WriteLine("Hello,  " + name);
    }

    // 中国人打招呼方法
    public void ChineseGreeting(string name)
    {
        Console.WriteLine("你好, " + name);
    }

}        

引入委托之后,就可以把函数作为参数传递给另一个方法了。委托可以提高方法扩展性,当你需要添加其他语言版本时,只需要定义一个额外的方法,并把该方法传递到Greeting 方法就可以了。


8.4 委托的本质

此处略过, 本质就是类.


8.5 委托链

C#中的委托可以封装多个方法, C#中把封装多个方法的委托称作委托链或多路广播委托。

class Program
{
    // 声明一个委托类型
    public delegate void DelegateTest();
    static void Main(string[] args)
    {
        // 用静态方法来实例化委托
        DelegateTest dtstatic = new DelegateTest(Program.method1);

        DelegateTest dtinstance= new DelegateTest(new Program().method2);

        // 定义一个委托对象,一开始初始化为null,即不代表任何方法
        DelegateTest delegatechain = null;
        // 使用“+”符号链接委托,链接多个委托后就成为了委托链
        delegatechain += dtstatic;
        delegatechain += dtinstance;

        // 调用委托链
        delegatechain();
        Console.Read();
    }

    // 静态方法
    private static void method1()
    {
        Console.WriteLine( "这是静态方法");
    }

    // 实例方法
    private void method2()
    {
        Console.WriteLine( "这是实例方法");
    }
}

从以上代码可知,通过使用“+”运算符,我们能将多个委托对象链接到一个委托对象实例上,使其成为多路广播委托实例。在调用委托链时,被绑定到委托链中的每个委托都会被执行。

上面代码执行结果:

这是静态方法
这是实例方法

8.5.2 从委托链中移除委托

既然能用“+”运算符把委托链接到一个委托对象实例上,自然也可以使用“–”运算符将某个委托从委托链对象上移除。

示例如下:

class Program
{
    public delegate void DelegateTest();
    static void Main(string[] args)
    {
        // 用静态方法来实例化委托
        DelegateTest dtstatic = new DelegateTest(Program.method1);

        DelegateTest dtinstance= new DelegateTest(new Program().method2);

        // 定义一个委托对象,一开始初始化为null,即不代表任何方法
        DelegateTest delegatechain = null;

        // 使用“+”符号链接委托,链接多个委托后就成为了委托链
        delegatechain += dtstatic;
        delegatechain += dtinstance;

        // 使用“-”运算符把dtstatic委托从委托链中移除
        delegatechain -= dtstatic;

        // 调用委托链
        delegatechain();
        Console.Read();
    }

    // 静态方法
    private static void method1()
    {
        Console.WriteLine( "这是静态方法");
    }

    // 实例方法
    private void method2()
    {
        Console.WriteLine( "这是实例方法");
    }
}    

以上代码通过使用“-”运算符将dtstatic 委托从委托链实例中移除,当我们再次调用委托链对象时,结果如下:

这是实例方法

8.6 归纳总结

本章首先介绍了委托的调用和它引入的原因,之后从IL的角度揭秘了委托的本质。最后介绍了委托链的概念:我们可以使用“+”运算符把一个委托添加到委托链实例中,也可以使用“-”运算符把委托实例从委托链中移除。