C# 委托

1、什么是委托?

委托,在百度百科中的释义是“指把事情托付给别人或别的机构(办理)”,也就是说,一件事,自己不亲自做,而是将这件事交给第三方去做。比如说老板A要发表演讲,需要写一个演讲稿,A可以选择自己写,也可以选择让员工B去写,那么A让B写演讲稿这个行为就是委托。

在维基百科中,有这么一句话,是专门对c#中委托(delegate)的释义。

  • .Net Framework上的程序设计语言如C#、Visual Basic的一类数据类型用于存储多个函数指针,称为委托(delegate)。

从这句话中,我们可以知道:第一,委托和struct、class等一样,是一种数据类型,确切的说,是一种引用类型,详细内容可参见c# 值类型;第二,委托类型的变量用来存储函数指针,委托类型的变量是对委托实例的引用,也就是说,委托类型的变量存储的是实例的地址,当我们将一个委托实例化时,就可以将委托实例和一个与委托签名(signature)相同的方法关联起来了。委托可以实现方法的间接调用。
这样看来,上面举的A让B帮忙写演讲稿的例子好像就不太对了,我们将这个例子改一下。
假设A写演讲稿的能力存储在大脑的区域c中,如果A想要写演讲稿,只需要去读取c就可以了。现在,A需要一份演讲稿用于参加明天的演讲,但是,A还有更重要的事情要做,而A同一时间又只能做一件事,这个时候,A将c告诉给B,那么B就可以通过读取c来代替A完成写演讲稿这件事。在这个例子中,B是委托的实例,也就是说,A需要找一个人来帮忙写演讲稿,找啊找啊找,找到了B,那么就完成了一个类型的实例化过程,从一个抽象的委托概念到一个具体的B这个人。
简单地说,委托的存在让我们能够将方法作为参数来进行传递。比如将A的写演讲稿的功能(方法)传递给B(委托实例)。

2、C#内置委托类型

好了,解释清楚了委托是什么,下面我们来看一下c#的内置委托类型,c#为我们准备了三个内置委托类型,分别是:Action、Func、Predicate。内置委托类型的好处是不需要先定义委托类型就可以直接使用。

2.1 Action

Action类型的委托用来封装没有返回值的方法。
好的,下面,我将首次介绍委托怎么使用,首先记住两句话:

  • 将方法作为参数传递给委托。
  • 委托的签名需要和方法一致。
namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            Student stu = new Student();
            Action action = new Action(stu.DoHomework); //实例化委托类型,将方法传递给委托的构造器
            stu.DoHomework(); //通过指向Student实例的引用变量直接调用方法
            action.Invoke();  //通过委托间接调用方法
        }
    }

    class Student
    {
        public void DoHomework()
        {
            Console.WriteLine("I'm doing my homework.");
        }
    }
}

/*
Outputs:
I'm doing my homework.
I'm doing my homework.
*/

在程序中,我们将stu.DoHomework传递给Action的构造器,且签名一致,什么叫签名一致?就是说方法和委托一样,都是既没有参数也没有返回值。
通过Invoke完成对方法的间接调用,当然Invoke可以省略。
直接调用和间接调用的结果相同,这是因为,无论是直接调用还是间接调用,我们想要执行DoHomework这个方法,都是先拿到这个方法所在的内存地址,然后去执行内存中存储的指令,也就是说,直接调用和间接调用执行的是同一块内存区域中的相同的指令,自然执行结果相同。
在这里插入图片描述

Action也可以与有参数的方法相匹配。

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            Student stu = new Student();
            Action<string, string> action = new Action<string, string>(stu.DoHomework);
            stu.DoHomework("Mike","Lisa");
            action.Invoke("Mike","Lisa");
        }
    }

    class Student
    {
        public void DoHomework(string str1,string str2)
        {
            Console.WriteLine("{0} do homework with {1}.",str1,str2);
        }
    }
}

/*
Outputs:
Mike do homework with Lisa.
Mike do homework with Lisa.
*/

2.2 Func

Func类型的委托用来封装有返回值的方法。

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            Student stu = new Student();
            Func<int, int, int> func = new Func<int, int, int>(stu.CalculateAddition);
            int a = 100;
            int b = 200;
            int c = 0;
            c = func.Invoke(a, b);
            Console.WriteLine("{0}+{1}={2}",a,b,c);
        }
    }

    class Student
    {
        public int CalculateAddition(int a,int b)
        {
            int result = a + b;
            return result;
        }
    }
}

/*
Outputs:
100+200=300
*/

同样的,委托和方法的签名需要保持一致,在这个例子中,委托和方法都是有两个int类型的输入参数和一个int类型的返回值。

2.3 Predicate

Predicate类型的委托用来封装有一个输入参数并且返回值为bool类型的方法。

using System;
using System.Drawing;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            Point[] points = { new Point(100, 200),
                         new Point(150, 250), new Point(250, 375),
                         new Point(275, 395), new Point(295, 450) };

            // Define the Predicate<T> delegate.
            Predicate<Point> predicate = FindPoints;

            // Find the first Point structure for which X times Y
            // is greater than 100000.
            Point first = Array.Find(points, predicate);

            // Display the first structure found.
            Console.WriteLine("Found: X = {0}, Y = {1}", first.X, first.Y);
        }
        private static bool FindPoints(Point obj)
        {
            return obj.X * obj.Y > 100000;
        }
    }
}

/*
Outputs:
Found: X = 275, Y = 395
*/

在这个例子中,我们定义了一个数组points,里面存储了5个二维点。我们要实现的功能是:找出数组中点的坐标乘积大于100000的点,并打印出满足条件的第一个点。
这里我们使用的是Array.Find方法,该方法接受两个输入参数,第一个参数是数组类型,第二个参数是Predicate类型。

Point first = Array.Find(points, predicate);

值得一提的是,这行代码的内部逻辑是:将数组中的每个点一次传入到委托中去,也就是间接调用FindPoints方法,然后返回满足条件的第一个点。

3、自定义委托类型

除了使用c#的内置委托类型,我们还可以自定义委托类型。

namespace ConsoleApp1
{
    public delegate int Calc(int a, int b);
    class Program
    {
        static void Main(string[] args)
        {
            Calculator calculator = new Calculator();
            Calc c1 = new Calc(calculator.Add);
            Calc c2 = new Calc(calculator.Sub);

            int a = 200;
            int b = 100;
            int c = 0;
            c = c1.Invoke(a, b);
            Console.WriteLine("{0}+{1}={2}",a,b,c);

            c = c2.Invoke(a, b);
            Console.WriteLine("{0}-{1}={2}", a, b, c);
        }
    }

    class Calculator
    {
        public int Add(int a,int b)
        {
            return a + b;
        }
        
        public int Sub(int a,int b)
        {
            return a - b;
        }
    }
}

/*
Outputs:
200+100=300
200-100=100
*/
public delegate int Calc(int a, int b);

我们通过delegate关键字来定义委托类型,该委托类型的签名是:接受两个int类型的输入参数,返回值类型为int。

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            Type t = typeof(Action);
            Console.WriteLine(t.IsClass);
        }
    }
}

/*
Outputs:
True
*/

委托是一种类,我们一般将委托的定义放在命名空间下,与其他类平级,当然也可以嵌套在类中。

4、单播委托和多播委托

  • .Net Framework上的程序设计语言如C#、Visual Basic的一类数据类型用于存储多个函数指针,称为委托(delegate)。

4.1 单播委托

单播委托(single cast delegate)只存储一个函数指针,我们前面举的例子都是单播委托,也就是说,委托里面只封装了一个方法,当我们调用委托实例时,就只会去调用这一个方法。

4.2 多播委托

多播委托(multicast delegates)存储多个函数指针,按照添加的顺序依次调用方法。我们可以通过+运算符向委托实例里面添加方法,通过-运算符从委托实例里面移除方法。

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            MessageLogger logger = new MessageLogger();
            string message = "Hello,World!";
            Action<string> action1 = new Action<string>(logger.LogMessageToConsole);
            Action<string> action2 = new Action<string>(logger.LogMessageToFile);
            action1 += action2;

            action1.Invoke(message);
        }
    }

    class MessageLogger
    {
        public void LogMessageToConsole(string message)
        {
            Console.WriteLine("Console:\t{0}",message);
        }

        public void LogMessageToFile(string message)
        {
            Console.WriteLine("File:\t\t{0}",message);
        }
    }
}

/*
Outputs:
Console:        Hello,World!
File:           Hello,World!
*/

我们通过+运算符向委托中添加了两个方法,这样,当我们调用委托实例时,就可以一次性的调用两个方法了。

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            MessageLogger logger = new MessageLogger();
            string message = "Hello,World!";
            Action<string> action1 = new Action<string>(logger.LogMessageToConsole);
            Action<string> action2 = new Action<string>(logger.LogMessageToFile);
            action1 += action2;

            action1.Invoke(message);

            Console.WriteLine("---------------------");
            action1 -= action2;
            action1.Invoke(message);
        }
    }

    class MessageLogger
    {
        public void LogMessageToConsole(string message)
        {
            Console.WriteLine("Console:\t{0}",message);
        }

        public void LogMessageToFile(string message)
        {
            Console.WriteLine("File:\t\t{0}",message);
        }
    }
}

/*
Outputs:
Console:        Hello,World!
File:           Hello,World!
---------------------
Console:        Hello,World!
*/

同样的,通过-操作符从委托中移除方法。
当然,有时候,如果一个方法我们在代码中只需要使用一次,我们没有必要先声明一个类,然后再在类里面声明方法,这样显得太麻烦了,这个时候我们可以使用匿名函数或者lamda表达式,好处是不需要显式地进行方法声明了。
匿名函数:

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            Action<string> action1 = delegate(string s) { Console.WriteLine("Console:\t{0}", s); };
            Action<string> action2 = delegate (string s) { Console.WriteLine("File:\t\t{0}", s); };
            action1 += action2;
            string message = "Hello,World!";
            action1.Invoke(message);
        }
    }
}

/*
Outputs:
Console:        Hello,World!
File:           Hello,World!
*/

lamda表达式:

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            Action<string> action1 = s => Console.WriteLine("Console:\t{0}",s);
            Action<string> action2 = s => Console.WriteLine("File:\t\t{0}",s);
            action1 += action2;
            string message = "Hello,World!";
            action1.Invoke(message);
        }
    }
}

/*
Outputs:
Console:        Hello,World!
File:           Hello,World!
*/

5、委托的实际应用

5.1 示例一:菜单系统

在这里插入图片描述
现在我们要实现一个菜单系统,根据用户的选择确定金额。

namespace ConsoleApp1
{
    public delegate void SelectAction();
    class Program
    {
        public static void SelectRoumofentiao()
        {
            Console.WriteLine("您点的是肉末粉条,您需要付款:18元\n\r");
        }

        public static void SelectJianjiaorousi()
        {
            Console.WriteLine("您点的是尖椒肉丝,您需要付款:25元\n\r");
        }

        public static void SelectLvrouchaozamian()
        {
            Console.WriteLine("您点的是驴肉炒杂面,您需要付款:28元\n\r");
        }

        public static void Exit()
        {
            Console.WriteLine("退出系统\n\r");
        }
        static void Main(string[] args)
        {
            Dictionary<string, SelectAction> menu = new Dictionary<string, SelectAction>
            {
                {"1", SelectRoumofentiao},
                {"2", SelectJianjiaorousi},
                { "3",SelectLvrouchaozamian},
                {"4",Exit }
            };

            while (true)
            {
                Console.WriteLine("请输入序号点餐");
                string s = Console.ReadLine().ToString();
                if(menu.ContainsKey(s))
                {
                    menu[s].Invoke();
                    if (s == "4") break;
                }
                else
                {
                    Console.WriteLine("输入错误,请重新输入\n\r");
                }
            }
        }
    }
}

/*
Outputs:
请输入序号点餐
1
您点的是肉末粉条,您需要付款:18元

请输入序号点餐
2
您点的是尖椒肉丝,您需要付款:25元

请输入序号点餐
3
您点的是驴肉炒杂面,您需要付款:28元

请输入序号点餐
5
输入错误,请重新输入

请输入序号点餐
4
退出系统
*/

这样,我们实现了方法的动态调用。当然,菜单系统完全可以使用if-else或者switch-case直接调用方法来实现,使用委托来调用的好处是,当增加新的菜品时,我们只需要增加新的方法,而不需要对方法调用地方的代码进行修改。

5.2 示例二:通过委托将一个外部方法进行封装,传递给另一个方法内部

注:以下两个例子来源于刘铁锰老师的教学视频。

5.2.1 模板方法
namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            WrapFactory wrapFactory = new WrapFactory();
            ProductFactory productFactory = new ProductFactory();
            Box box1 = wrapFactory.WrapProduct(productFactory.MakePizza);
            Box box2 = wrapFactory.WrapProduct(productFactory.MakeToycar);

            Console.WriteLine(box1.product.Name);
            Console.WriteLine(box2.product.Name);
        }
    }

    class Product
    {
        public string Name { get; set; }
    }

    class Box
    {
        public Product product { get; set; }
    }

    class WrapFactory
    {
        public Box WrapProduct(Func<Product> getProduct)
        {
            Box box = new Box();
            Product product = getProduct.Invoke();
            box.product = product;
            return box;
        }
    }

    class ProductFactory
    {
        public Product MakePizza()
        {
            Product product = new Product();
            product.Name = "Pizza";
            return product;
        }

        public Product MakeToycar()
        {
            Product product = new Product();
            product.Name = "Toy Car";
            return product;
        }
    }
}

/*
Outputs:
Pizza
Toy Car
*/

这段代码一共定义了4个类,Product、Box、WrapFactory、ProductFactory。Product是产品类,有一个属性代表产品的名称;Box是包装箱类,有一个属性代表包装箱里面装的产品;WrapFactory是包装工厂类,用来实现对产品的包装,定义了一个方法,该方法接受输入的产品,将产品打包好返回包装箱;ProductFactory是产品工厂类,用来生产产品,定义了两个方法,分别生产Pizza和Toy Car。
我们将产品工厂的方法通过委托进行封装,然后传递到包装工厂类的方法里面,实现一个方法内部动态调用另一个方法。
想象一下,如果现在我的产品类别增加了,还需要生产蛋糕cake,那么我们只需要在ProductFactory里面增加相应方法即可,而不需要修改Product、Box、WrapFactory类,这样代码的复用性和可扩展性大大增加。

5.2.2 回调方法
namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            WrapFactory wrapFactory = new WrapFactory();
            ProductFactory productFactory = new ProductFactory();
            Logger logger = new Logger();
            Box box1 = wrapFactory.WrapProduct(productFactory.MakePizza,logger.Log);
            Box box2 = wrapFactory.WrapProduct(productFactory.MakeToycar,logger.Log);

            Console.WriteLine(box1.product.Name);
            Console.WriteLine(box2.product.Name);
        }
    }

    class Logger
    {
        public void Log(Product product)
        {
            Console.WriteLine("Product “{0}” created at {1} ,Pirce is {2}", product.Name, DateTime.UtcNow, product.Price);
        }
    }
    
    class Product
    {
        public string Name { get; set; }
        public int Price { get; set; }
    }

    class Box
    {
        public Product product { get; set; }
    }

    class WrapFactory
    {
        public Box WrapProduct(Func<Product> getProduct,Action<Product> getLogger)
        {
            Box box = new Box();
            Product product = getProduct.Invoke();
            if(product.Price>=50)
            {
                getLogger(product);
            }
            box.product = product;
            return box;
        }
    }

    class ProductFactory
    {
        public Product MakePizza()
        {
            Product product = new Product();
            product.Name = "Pizza";
            product.Price = 10;
            return product;
        }

        public Product MakeToycar()
        {
            Product product = new Product();
            product.Name = "Toy Car";
            product.Price = 100;
            return product;
        }
    }
}

/*
Outputs:
Product “Toy Car” created at 2024/12/11 6:43:02 ,Pirce is 100
Pizza
Toy Car
*/

5.3 示例三:事件

事件是基于委托的,事件和委托之间有很多相同之处,也有区别,在winform窗体应用中,事件随处可见,因此我会单独写一篇博客专门讲解委托和事件。

6、委托和函数指针

委托相当于C/C++中的函数指针,区别在于委托是完全面向对象的。
这里简单说明一下面向对象和面向过程的区别。

情景:做饭

  • 1.洗菜
  • 2.淘米
  • 3.电饭煲煮饭
  • 4.炒菜
  • 5.电饭煲停止煮饭
  • 6.吃饭

按照面向过程的思想,我们就是一步步的往下做就行,先定义一个函数实现洗菜功能,再定义一个函数完成淘米功能,再定义一个函数完成电饭煲煮饭功能…
按照面向对象的思想,我们先定义两个类,一个是电饭煲类,一个是人类。电饭煲类有2个函数:煮饭和停止煮饭,人类有4个函数:洗菜、淘米、炒菜、吃饭。定义好了之后,然后我们通过创建电饭煲实例和人类实例来调用。
面向过程性能上优于面向对象,但面向对象更容易扩展、复用、维护。

7、为什么要用委托?

  • 灵活:动态的调用方法。
  • 复用:我们可以将方法作为参数进行传递,提高代码复用。
  • 类型安全:委托提供了一种类型安全的方法来处理方法引用,所谓类型安全指的是方法的签名和委托的签名需要保持一致。
  • 事件:委托是事件的基础。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值