目录
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、为什么要用委托?
- 灵活:动态的调用方法。
- 复用:我们可以将方法作为参数进行传递,提高代码复用。
- 类型安全:委托提供了一种类型安全的方法来处理方法引用,所谓类型安全指的是方法的签名和委托的签名需要保持一致。
- 事件:委托是事件的基础。