委托
.NET团队之初想要实现一种用于任何后期绑定算法的引用类型,也就是想要一种可用于对方法引用的一种结构,同时又希望它能支持单播和多播,于是委托诞生了。
多播指的将多个方法调用链接在一起调用,就像一个列表一样
单播指的是单一方法的调用,其实可以认为单播是多播的一种特例
委托是.NET 1.0版本的一部分(在泛型出现之前),从功能上说,委托就是一种对方法的引用,类似于函数指针,或者说,委托是对方法的一种分类引用,相同参数类型和返回类型的方法视为一类,可被同一种类型的委托引用。
delegate
声明委托使用delegate关键字,例如:
//无返回值委托类型
public delegate void EventHandler(object? sender, EventArgs e);
//有返回值委托类型
public delegate Assembly? ResolveEventHandler(object? sender, ResolveEventArgs args);
委托还可与泛型一起使用,例如使用很多的返回值的Action委托和有返回值的Func委托:
//无返回值的Action委托
public delegate void Action();
public delegate void Action<in T>(T arg);
public delegate void Action<in T1, in T2>(T1 arg1, T2 arg2);
//有返回值的Func委托
public delegate TResult Func<out TResult>();
public delegate TResult Func<in T1, out TResult>(T1 arg);
public delegate TResult Func<in T1, in T2, out TResult>(T1 arg1, T2 arg2);
上面Action委托和Func 委托的变体可包含多达 16 个输入参数,所以一般时候我们都不用自定义委托,当然如果需要,我们可以自行使用delegate关键字去声明委托来满足我们特殊的需求。
委托是一种类型,就像class关键字声明的是类类型,而delegate关键字声明的是委托类型,同样我们可以实例化委托类型:
class Program
{
public int Multiply(int a, int b)
{
return a * b;
}
public static int Plus(int a, int b)
{
return a + b;
}
static void Main(string[] args)
{
//静态方法转换
Func<int, int, int> func = Plus;
//实例方法转换
Func<int, int, int> func = new Program().Multiply;
//使用new关键字
Func<int, int, int> func = new Func<int, int, int>(Plus);
//使用Lambda表达式
Func<int, int, int> func = (a, b) => a - b;
int result = func(1, 2);
}
}
上面使用4中方式来获取一个委托类型的实例,比如将方法作为参数赋值给一个委托类型,在编译时会告知编译器将方法引用转换。还有Lambda表达式,它是一种匿名委托,在Linq中经常永达。
总之,无论是静态方法还是实例方法,能否转换为一个委托实例取决于方法的参数类型、参数顺序、返回值类型是否与委托类型一致。
Delegate类和MulticastDelegate类
委托不仅能类一样实例化,还能像类一样调用方法,也可以创建委托类型的属性,或者将委托类型变量当做参数一样传递,总之,可以将委托类型当做一种特殊的类来使用,而这一点得益于Delegate和MulticastDelegate两个类。
MulticastDelegate类是Delegate类的子类,但是我们无法直接创建Delegate和MulticastDelegate的子类,因为它是由编译器自动派生的。当使用delegate关键字声明的委托,在编译时会生成一个特定的委托类型,而这个委托类型就是派生MulticastDelegate类,因此我们无需像类一样显示用继承才是使用父类的方法(我们用的数组也是类似原理,派生于Array类)。
虽然Delegate和MulticastDelegate两个类都是委托的父类,但是不要认为Delegate就是单播类型,MulticastDelegate就是多播类型,事实上,C#中的委托都是多播类型,比如:
class Program
{
public static void Multiply(int a, int b)
{
Console.WriteLine("a*b=" + (a * b));
}
public static void Plus(int a, int b)
{
Console.WriteLine("a+b=" + (a + b));
}
static void Main(string[] args)
{
//第一个方法
Action<int, int> action = Plus;
//链接第二个方法(多播)
action += Multiply;
action(2, 3);
//输出
//a+b=5
//a*b=6
}
}
需要注意的是,如果委托有返回值,那么返回的是最后一个链接方法的值,其他方法的返回值将会被丢弃:
class Program
{
public static int Multiply(int a, int b)
{
Console.WriteLine("a*b=" + (a * b));
return a * b;
}
public static int Plus(int a, int b)
{
Console.WriteLine("a+b=" + (a + b));
return a + b;
}
static void Main(string[] args)
{
//第一个方法
Func<int, int, int> func = Plus;
//链接第二个方法(多播)
func += Multiply;
int result = func(2, 3);
Console.WriteLine("result=" + result);
//输出
//a+b=5
//a*b=6
//result=6
}
}
注意,这里链接支持+=、-=、+、- 四个运算:
+:链接两个委托,并返回新的委托链,类似于数值运算中的+运算
-:从委托链中剔除一个指定委托,并返回提出后的委托链,如果委托链无任何委托,则返回null,类似于数值运算中的-运算
+=:先执行+运算,再新的委托链赋值给左边的委托变量,类似于数值运算中的+=运算
-=:先执行-运算,再新的委托链赋值给左边的委托变量,类似于数值运算中的-=运算
常用方法与属性
Invoke、BeginInvoke、EndInvoke
Invoke是调用委托方法,而BeginInvoke、EndInvoke是它的异步模式,例如:
Func<int, int, int> func = Plus;
int result = func(1, 2);
//等价于
int result = func.Invoke(1, 2);
因为委托类型也是应用类型,那么那也就可能为null,如果直接使用可能导致空指针异常,因此我们常将Invoke方法与null条件运算符(?.)一起使用,而非直接调用:
Func<int, int, int> func = null;
int result = func(1, 2);//将导致空指针异常
int? result = func?.Invoke(1, 2);//但此时返回值类型可能需要使用可空类型接收
DynamicInvoke
DynamicInvoke来自Delegate类,和Invoke方法一样是调用委托用的,只不过DynamicInvoke调用的参数是一个object数组,所以它常和反射结合起来使用,用于参数情况未知的情况:
Func<int, int, int> func = Plus;
int result = (int)func.DynamicInvoke(new object[] { 1, 2 });
Method、Target
Method:多播委托中最后一个委托方法的引用,它是一个System.Reflection.MethodInfo对象
Target:如果多播委托中最后一个委托方法是实例方法,那么Target就是调用的实例对象,如果是静态方法,那么Target就是null
事件
事件和委托类似,只不过事件时基于一种广播订阅模式,实际上,事件是建立在对委托的基础之上的,有了委托,我们才能建立起事件模型。
event
定义一个事件使用event关键字,事件类型即委托类型,如:
//点击事件
public event EventHandler Click;
定义了一个事件,同样可以像委托一样进行赋值、四则运算(+=、-=、+、-)等操作,同样可以想委托一样使用Delegate类和MulticastDelegate类中的方法,比如像方法一样触发事件,或者调用Invoke方法触发事件。
但是事件和委托还是有区别的,事件作为委托的一种特殊用法,和委托的主要却别在于:
1、事件必须定义在类或者结构体等内部,换句话说,委托与类、结构体等是一个级别的,而事件与方法、属性等是一个级别的
2、虽然事件可以使用Delegate类和MulticastDelegate类中的方法,但是它仅限在定义这个时间的类或者结构体等内部才能,比如事件的触发(像方法调用直接触发、调用Invoke方法触发)只能在类或者结构体等内部触发,列如:
class Program
{
static void Main(string[] args)
{
var btn = new Button();
btn.Click += (sender,e) => Console.WriteLine("Click");
//btn.Click(btn, EventArgs.Empty);//报错
btn.OnClick();//不报错
}
}
public class Button
{
//点击事件
public event EventHandler Click;
public void OnClick()
{
Click(this, EventArgs.Empty);
}
}
3、事件采用 += 运算表示订阅,使用 -= 表示取消订阅,虽然事件也支持 +、- 运算,但是+、- 运算也只是被限制在定义这个时间的类或者结构体等内部才能使用。
4、定义的事件时一个引用类型,默认是null,所以在触发前要判断,避免空指针异常,建议采用Invoke方法与null条件运算符(?.)一起使用来触发事件,事件可以使用 = 进行赋值,但是同样被限制在定义这个时间的类或者结构体等内部才能使用。
5、标准的事件模型应该没有返回值,确实需要返回值时,应该改用参数的引用传递特性来返回数据。
一个完整简单的事件例子:
class Program
{
static void Main(string[] args)
{
MessageProducer messageProducer = new MessageProducer();
Subscriber zhangsan = new Subscriber("张三");
Subscriber lisi = new Subscriber("李四");
Subscriber wangwu = new Subscriber("王五");
messageProducer.Subscribe(zhangsan);
messageProducer.Subscribe(lisi);
messageProducer.Subscribe(wangwu);
messageProducer.Produce("hello");
messageProducer.Unsubscribe(lisi);
messageProducer.Produce("hello again");
}
}
public delegate void MessageProducerHandler(string message);
public class MessageProducer
{
public event MessageProducerHandler MessageHandle;
/// <summary>
/// 订阅
/// </summary>
/// <param name="subscriber"></param>
public void Subscribe(Subscriber subscriber)
{
Console.WriteLine($"【{subscriber.Name}】订阅了消息");
MessageHandle += subscriber.OnMessage;
}
/// <summary>
/// 取消订阅
/// </summary>
/// <param name="subscriber"></param>
public void Unsubscribe(Subscriber subscriber)
{
Console.WriteLine($"【{subscriber.Name}】取消了消息订阅");
MessageHandle -= subscriber.OnMessage;
}
/// <summary>
/// 发布一条消息
/// </summary>
/// <param name="message"></param>
public void Produce(string message)
{
Console.WriteLine($"发布消息【{message}】");
MessageHandle?.Invoke(message);
}
}
public class Subscriber
{
public Subscriber(string name)
{
Name = name;
}
/// <summary>
/// 订阅者名称
/// </summary>
public string Name { get; }
/// <summary>
/// 消息处理方法
/// </summary>
/// <param name="message"></param>
public void OnMessage(string message)
{
Console.WriteLine($"【{Name}】收到消息【{message}】");
}
}
打印结果:

总结
我们可以认为事件时委托的一种特殊用法,什么时候用委托,什么时候用事件,总结如下:
1、如果需要像一个变量使用,在某段代码后执行回调,应该使用委托,比如 linq 的语法
2、如果需要有返回值,应该采用委托
3、如果需求表现出一次订阅,长时间内接收多个信息的特性,应该使用事假模型。
参考文档:https://docs.microsoft.com/en-us/dotnet/csharp/delegates-overview
原创文章,作者:ItWorker,如若转载,请注明出处:https://blog.ytso.com/tech/pnotes/282448.html