C#(二)——委托

575 阅读6分钟

一、什么是委托


委托(Delegate) 是存有对某个方法的引用的一种引用类型变量。引用可在运行时被改变,类似于 C 或 C++ 中函数的指针。委托(Delegate)特别用于实现事件和回调方法。所有的委托(Delegate)都派生自 System.Delegate 类。


  这句话的大意是,以前我们一般都是把数据作为参数传进方法,现在有了委托,小方法也能作为参数传进大方法。这里的“小方法”就是我们要请求“大方法”做的委托。

二、为什么要用委托

举个栗子,我们要写一个问候的方法

public void GreetPeople(string name)
{
    EnglishGreeting(name);
}
 
public void EnglishGreeting(string name)
{
    Console.WriteLine("Good Morning, " + name);
}

这个方法可以对不同名字的人问候,但是如果对方是个中国人,就要再加一个中国人的问候方法

public void ChineseGreeting(string name)
{
    Console.WriteLine("早上好, " + name);
}

这样我们见到别人,就不只是要看一下他叫什么了,还要选一下我们要用中国人的问候方法还是美国人的问候方法

public enum Language
{
    English, Chinese
}
public void GreetPeople(string name, Language lang)
{
    switch (lang)
    {
        case Language.English:
            EnglishGreeting(name);
            break;
        case Language.Chinese:
            ChineseGreeting(name);
            break;
    }
}

OK,尽管这样解决了问题,但如果我们需要再添加韩文版、日文版,就不得不反复修改枚举和 GreetPeople() 方法,以适应新的需求。


在考虑新的解决方案之前,我们先看看 GreetPeople 的方法签名:

public void GreetPeople(string name, Language lang);

如果 GreetPeople 可以接受一个参数变量,这个变量可以代表一个另一个方法:
当我们给这个变量赋值 EnglishGreeting 的时候,它代表着 EnglsihGreeting() 这个方法;
当我们给它赋值 ChineseGreeting 的时候,它又代表着 ChineseGreeting() 方法。

我们将这个参数变量命名为 MakeGreeting ,然后,在方法体里,我们也可以像使用别的参数一样使用 MakeGreeting 。但是,由于 MakeGreeting 代表着一个方法,它的使用方式应该和它被赋的方法(比如 ChineseGreeting)是一样的,比如:

MakeGreeting(name);

所以,它应该是这个样子了:

public void GreetPeople(string name, *** MakeGreeting)
{
    MakeGreeting(name);
}

那么问题来了,*** 这个方法参数的类型应该是什么呢?

说明: 这里已不再需要枚举了,因为在给 MakeGreeting 赋值的时候动态地决定使用哪个方法,是 ChineseGreeting 还是 EnglishGreeting,而在这个两个方法内部,已经对使用 “Good Morning” 还是 “早上好 ” 作了区分。

聪明的你应该已经想到了,现在是委托该出场的时候了!但讲述委托之前,我们再看看 MakeGreeting 参数所能代表的 ChineseGreeting() 和 EnglishGreeting() 方法的签名:

public void EnglishGreeting(string name)

public void ChineseGreeting(string name)

需要注意的是,MakeGreeting 的参数类型定义应该能够确定 MakeGreeting 可以代表的方法种类,再进一步讲,就是 MakeGreeting 要能代表的方法的参数类型和返回类型。

于是,委托出现了:它定义了 MakeGreeting 参数所能代表的方法的种类,也就是 MakeGreeting 参数的类型。


本例中委托的定义:

public delegate void GreetingDelegate(string name);

与上面 EnglishGreeting() 方法的签名对比一下,除了加入了delegate 关键字以外,其余的是不是完全一样?现在,让我们再次改动 GreetPeople() 方法,如下所示:

//声明委托
public delegate void GreetingDelegate(string name);

public void GreetPeople(string name, GreetingDelegate MakeGreeting)
{
    MakeGreeting(name);
}

所以,完整的代码如下

//声明委托
public delegate void GreetingDelegate(string name);
 
class Program
{
    private static void EnglishGreeting(string name)
    {
        Console.WriteLine("Good Morning, " + name);
    }
 
    private static void ChineseGreeting(string name)
    {
        Console.WriteLine("早上好, " + name);
    }
 
    private static void GreetPeople(string name, GreetingDelegate MakeGreeting)
    {
        MakeGreeting(name);
    }
 
    static void Main(string[] args)
    {
        GreetPeople("Liker", EnglishGreeting);
        GreetPeople("李志中", ChineseGreeting);
        Console.ReadLine();
    }
}

注意:委托的位置跟类一样。

现在这个代码长得还并不像我们经常见到的委托的格式,进一步的演变请看下一章中 3.2 多播委托


总结:委托是一个类,它定义了方法的类型,使得可以将方法当作另一个方法的参数来进行传递,这种将方法动态地赋给参数的做法,可以避免在程序中大量使用If … Else…(Switch)语句对相同的其他参数进行处理,同时使得程序具有更好的可扩展性。

三、委托的一般用法

3.1 命名方法委托

  这种委托就形如上面的栗子,分成三步走:定义声明委托、实例化委托、调用委托

下面是一个使用委托完成将图书信息按照价格升序排序的应用。

class Book : IComparable<Book>
{
    //定义构造方法为图书名称和价格赋值
    public Book(string name, double price)
    {
        Name = name;
        Price = price;
    }
    
    //定义图书名称属性
    public string Name { get; set; }
    //定义图价格属性
    public double Price { get; set; }
    
    //实现比较器中比较的方法
    public int CompareTo(Book other)
    {
        return (int)(this.Price - other.Price);
    }
    
    //重写ToString方法,返回图书名称和价格
    public override string ToString()
    {
        return Name + ":" + Price;
    }
    
    //图书信息排序
    public static void BookSort(Book[] books)
    {
        Array.Sort(books);
    }
}
class Program
    {
        //定义对图书信息排序的委托
        public delegate void BookDelegate(Book[] books);
        
        static void Main(string[] args)
        {
            //实例化委托
            BookDelegate bookDelegate = new BookDelegate(Book.BookSort);
            
            Book[] book = new Book[3];
            book[0] = new Book("计算机应用", 50);
            book[1] = new Book("C# 教程", 59);
            book[2] = new Book("VS2017应用", 49);
            
            //调用委托
            bookDelegate(book);
            foreach (Book bk in book)
            {
                Console.WriteLine(bk);
            }

            Console.ReadLine();
        }
    }

注:关于比较器 CompareTo 可以参考 IComparable、IComparer接口:比较两个对象的值

因此,使用委托可以用同一个方法一次性处理一组数据

3.2 多播委托

  多播委托 就是一句话做好多事。

比如上面第二章中 中国人和美国人问候 的栗子,如果我们想对同一个人说不同语言的问候,可以进一步修改为:

static void Main(string[] args)
{
    GreetingDelegate delegate1;
    delegate1 = EnglishGreeting; 
    delegate1 += ChineseGreeting;
    GreetPeople("Liker", delegate1);
    Console.ReadLine();
}

实际上,因为 GreetPeople 没有做其他 EnglishGreeting 和 ChineseGreeting 公共的事情,所以我们可以也可以绕过 GreetPeople 方法,通过委托来直接调用 EnglishGreeting 和 ChineseGreeting :

static void Main(string[] args)
{
    GreetingDelegate delegate1;
    delegate1 = EnglishGreeting;
    delegate1 += ChineseGreeting; 
    delegate1("Liker");
    Console.ReadLine();
}

因此,使用委托可以 将多个方法绑定到同一个委托变量 ,当调用此变量时(这里用“调用”这个词,是因为此变量代表一个方法),可以依次调用所有绑定的方法。当然,也可以解绑方法。

3.3 匿名委托

  对于一些 仅需要使用一次 的委托,可以采用匿名委托。

class Program
{
    //定义委托
    public delegate void AreaDelegate(double length, double width);
    
    static void Main(string[] args)
    {
        Console.WriteLine("请输入长方形的长:");
        double length = double.Parse(Console.ReadLine());
        Console.WriteLine("请输入长方形的宽:");
        double width = double.Parse(Console.ReadLine());
        
        //定义匿名委托
        AreaDelegate areaDelegate = delegate
        {
            Console.WriteLine("长方形的面积为:" + length * width);
        };    //注意:分号!!!
        
        //调用匿名委托列表
        areaDelegate(length, width);
    }
}

通过上面 3 个步骤即可完成匿名委托的定义和调用,需要注意的是,在定义匿名委托时,代码块结束后要在 {} 后加上分号

3.4 事件

  事件是一种特殊的委托,具体的使用见我的下一篇小文# C#(三)——事件


参考文献

  hushzhang的博客_博客园

  手册:C语言中文网