目录
本博文主要包括三点内容:
- 什么是委托
- 委托的声明(自定义委托)
- 委托的使用
什么是委托?
委托的意思就是有些事情自己不能亲自去做,就交给别人去做。委托其实就是函数指针的升级版,函数指针是C/C++语言中特有的,下面就是函数指针的示例。
# include<stdio.h>
//定义函数指针,返回类型为int,函数名为Calc(定义成一种数据类型),参数类型为两个int
typedef int (*Calc)(int a, int b);
int Add(int a, int b)
{
return a + b;
}
int Sub(int a, int b)
{
return a - b;
}
int main()
{
//分别取两个函数的地址
Calc func1 = &Add;
Calc func2 = ⋐
//不直接调用Add和Sub函数,而是通过func1和func2间接调用
printf("加法:%d\n",func1(10, 10));
printf("减法:%d\n",func2(10, 10));
return 0;
}
程序打印结果如下:
打印结果符合我们的预期,上述就是通过函数指针间接调用函数的过程。为了更好的理解,再介绍两个概念。(一切皆地址、直接调用与间接调用)。
一切皆地址
在程序员眼中,任何程序都是由两部分组成的,数据和算法。数据即是程序所要处理的数据,算法则是程序处理数据所用的算法。数据也可以叫做变量,算法也可以叫做函数。
- 变量(数据)是以某个地址为起点的一段内存中所存储的值,本质上是地址,这段内存的大小是由数据类型来决定的。
- 函数(算法)是以某个地址为起点的一段内存中所存储的一组机器语言指令,本质上也是地址,cpu就是通过这段内存中的指令一条一条的去执行,来完成函数中所包含的算法。
无论是数据还是算法,都是存储再内存当中的。变量就是寻找数据的地址,函数就是寻找算法的地址,因此在程序中,一切皆地址。
直接调用和间接调用
- 直接调用:通过函数名来调用一个函数,cpu通过函数名直接获得函数所在的地址开始执行,执行完之后,cpu会返回到函数调用之处。
- 间接调用,通过指向某个函数的函数指针来调用函数,函数指针作为一个指针变量,里面存储的就是函数名所对应的地址,通过函数指针,cpu会得到函数的地址,开始逐条执行机器指令,执行完后返回到调用之处。
会发现,无论是直接调用还是间接调用,cpu访问到的机器语言是一样的,因此,直接调用和间接调用的效果是一样的。
最常用委托使用演示(Action和Func)
action的使用演示
Action<T>是.NET Framework内置的泛型委托,可以使用Action<T>委托以参数形式传递方法,而不用显示声明自定义的委托。封装的方法必须与此委托定义的方法签名相对应。也就是说,封装的方法必须具有一个通过值传递给它的参数,并且不能有返回值。
using System;
namespace _19_12_25
{
class Program
{
static void Main(string[] args)
{
Calculator calculator = new Calculator();
Action action = new Action(calculator.Report);
//直接调用
calculator.Report();
//间接调用
action.Invoke();
action();
}
class Calculator
{
public void Report()
{
Console.WriteLine("I Have 3 Methods!");
}
}
}
}
程序运行结果:
和我们预想的一样,Action委托使用Invoke()调用,或者直接像函数一样调用。
//间接调用
action.Invoke();
action();
Func的使用演示
Func是一种有返回值的委托,在委托的定义时,要对函数的参数类型和返回类型进行确定。可以按照函数的类型约束条件来进行委托定义。
using System;
namespace _19_12_25
{
class Program
{
static void Main(string[] args)
{
Calculator calculator = new Calculator();
Func<int, int, int> func1 = new Func<int, int, int>(calculator.Add);
Func<int, int, int> func2 = new Func<int, int, int>(calculator.Sub);
Console.WriteLine(func1(10, 10));
Console.WriteLine(func2.Invoke(10, 10));
}
class Calculator
{
public int Add(int a, int b)
{
return a + b;
}
public int Sub(int a, int b)
{
return a - b;
}
}
}
}
我们看委托的定义:Func<int, int, int> func1 = new Func<int, int, int>(calculator.Add);
其中尖括号里面,前两个int表示委托函数的参数类型,最后一个int表示委托函数的返回类型。打印结果为20和0。
委托的声明(自定义委托)
Action和Func是内置的委托,接下来来看一下自定义的委托。在这之前,还需要了解一下几点,对后边的理解有帮助。
- 委托是一种类,类是数据类型,因此委托也是一种数据类型。
- 委托的声明方式与一般的类不同,主要是为了照顾可读性和C/C++的传统(函数指针)。
using System;
namespace _19_12_25
{
class Program
{
//声明委托
public delegate double Calc(double x, double y);
static void Main(string[] args)
{
Calculator calculator = new Calculator();
Calc calc1 = new Calc(calculator.Add);
Calc calc2 = new Calc(calculator.Sub);
Calc calc3 = new Calc(calculator.Mul);
Calc calc4 = new Calc(calculator.Div);
Console.WriteLine(calc1.Invoke(10, 10));
Console.WriteLine(calc2.Invoke(10, 10));
Console.WriteLine(calc3.Invoke(10, 10));
Console.WriteLine(calc4.Invoke(10, 10));
}
class Calculator
{
public double Add(double x, double y)
{
return x + y;
}
public double Sub(double x, double y)
{
return x - y;
}
public double Mul(double x, double y)
{
return x * y;
}
public double Div(double x, double y)
{
return x / y;
}
}
}
}
在以上例子中的Calculator类中的四个函数,它们的参数列表和返回类型都是一样的。在上述例子中的委托的声明的格式和C++函数指针的声明是一样的。委托的调用也可以不用Invoke,可以仿照函数指针的调用格式,如calc4.Invoke(10, 10)可以用calc4(10,10)来进行替代。上个例子就是自定义委托的声明和使用,不过在自定义委托中还需要注意:
- 委托作为一个类,必须声明在命名空间里,避免声明为嵌套类。
- 委托与所封装的方法必须“类型兼容”。
从上图中可以看出,我们定义的委托和所封装的函数的返回类型和参数类型是完全一致的。
委托的一般使用(把委托方法当作参数传参给另一个方法)
因为委托绑定了一个函数,所以把委托当作参数,即是将委托绑定的函数当作参数,来进行函数的动态使用。
这种把委托当作参数的使用方法又分为两种,一种是模板方法,另一种是回调方法。
模板方法
所谓模板方法,就是你写了一个方法,通过传进来的委托参数,借用指定的外部方法来产生结果。就相当于你写的代码中有一个填空题,这个填空题用传进来的委托参数来填补,通过委托参数,间接的来调用外部的方法,这个方法一般是具有返回值的,当你拿到这个返回值后继续执行。这也就解释了为什么叫模板方法,就相当于是你写的是一个模板,这个模板之中有一处是不确定的,其他的都确定好了。这个不确定的部分就靠你传进来的委托类型的参数所包含的这个方法来填补。
using System;
namespace _19_12_25
{
class Program
{
//声明委托
public delegate double Calc(double x, double y);
static void Main(string[] args)
{
ProductFactory productFactory = new ProductFactory();
WrapFactory wrapFactory = new WrapFactory();
Func<Product> func1 = new Func<Product>(productFactory.MakePizza);
Func<Product> func2 = new Func<Product>(productFactory.MakeToyCar);
//调用模板方法
Box box1 = wrapFactory.WrapProduct(func1);
Box box2 = wrapFactory.WrapProduct(func2);
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 = "Pizzza";
return product;
}
public Product MakeToyCar()
{
Product product = new Product();
product.Name = "Toy Car";
return product;
}
}
}
}
在这个程序里面,我们设置了Product、Box、WrapFactory、ProductFactory类,WrapFactory中会通过委托间接的去ProductFactory中获得产品进行包装。这个模板中,可以生产各种各样不同的产品,而且Product、Box、WrapFactory这三个类都不需要再改动,最大限度地实现了代码地重复使用。
回调方法
首先,得了解什么是回调,它指的是这样一种场景,我给了你一张我的名片,上面有我的电话,等你有一天需要找我,你打这个电话就能找到我。如果你不需要我,可以永远不给我打电话。
当以回调方法的形式来使用委托的时候,要把委托类型的参数传递到主调方法里面,委托参数里面封装了一个被回调的方法。主调方法会根据自己的逻辑来决定是否使用回调方法。回调方法一般置于代码的末端,来处理一些后续的事情。
using System;
namespace _19_12_25
{
class Program
{
//声明委托
public delegate double Calc(double x, double y);
static void Main(string[] args)
{
ProductFactory productFactory = new ProductFactory();
WrapFactory wrapFactory = new WrapFactory();
Func<Product> func1 = new Func<Product>(productFactory.MakePizza);
Func<Product> func2 = new Func<Product>(productFactory.MakeToyCar);
Logger logger = new Logger();
Action<Product> log = new Action<Product>(logger.Log);
//调用模板方法
Box box1 = wrapFactory.WrapProduct(func1, log);
Box box2 = wrapFactory.WrapProduct(func2, log);
Console.WriteLine(box1.Product.Name);
Console.WriteLine(box2.Product.Name);
}
//记录运行状态
class Logger
{
public void Log(Product product)
{
Console.WriteLine("Product '{0}' create at {1}.price is {2}", product.Name, DateTime.UtcNow, product.Price);
}
}
class Product
{
public string Name { get; set; }
public double Price { get; set; }
}
class Box
{
public Product Product { get; set; }
}
class WrapFactory
{
public Box WrapProduct(Func<Product> getProduct,Action<Product> logCallback)
{
Box box = new Box();
Product product = getProduct.Invoke();
if (product.Price >= 50)
{
logCallback.Invoke(product);
}
box.Product = product;
return box;
}
}
class ProductFactory
{
public Product MakePizza()
{
Product product = new Product();
product.Name = "Pizzza";
product.Price = 12;
return product;
}
public Product MakeToyCar()
{
Product product = new Product();
product.Name = "Toy Car";
product.Price = 100;
return product;
}
}
}
}
程序运行结果:
我们模板方法的程序示例中加入了一个类Logger,用来记录程序的的一些信息,Logger类里面有一个Log函数,它会当作一个回调函数,传递给WrapFactory中的WrapProduct函数,当得到的产品价格大于50后,会打印相关的信息。
注意事项
对于一种难精通、易使用、功能强大的东西,一旦滥用后果非常严重。
委托有几个缺点:
- 委托是一种方法级别的紧耦合,现实工作中使用要谨慎。
- 使代码的可读性下降、debug的难度增加。
- 把委托回调、异步调用和多线程纠缠在一起,会让代码变得难以阅读和维护
- 委托使用不当可能会造成内存泄漏和程序性能下降。