本内容整理自b站up:十月的寒流 的视频,并且经过个人思考进行了完善和一些更正。
委托的作用:
1.将函数作为函数的参数进行传参并进行传递
2.基于委托去声明事件并注册
var foo = new Foo(MyFunc); //示例化一个委托,但是要立马传入一个参数
foo.Invoke() //调用一个委托
foo() //调用一个委托,和Invoke()一样
foo += MyFunc(); //注册一个委托,注册几次调用几次
foo += MyFunc2();
foo -= MyFunc();
void MyFunc()
{
"hello".Dump();
}
void MyFunc2()
{
"world".Dump();
}
delegate void Foo(); //声明一个委托
输出
hello
world
我们无法保证委托一定不是空的,委托如果是空的,在运行的时候会报错,我们可以采用
foo?.Invoke();
用来防止报错。
传参
var foo = new Foo(MyFunc);
foo(10).Dump();
string MyFunc(int number)
{
return (number + 1).ToString();
}
delegate string Foo(int a);
输出
11
委托的特性之一:只有最后一个返回值才会作为委托的返回值
var foo = new Foo(MyFunc);
foo += MyFunc2;
foo(10).Dump();
string MyFunc(int number)
{
return (number + 1).ToString();
}
string MyFunc2(int number)
{
return (number + 10).ToString();
}
delegate string Foo(int a);
输出
20
回调函数
MyHeavyJob(MyCallback);
void MyHeavyJob(Callback callback)
{
Thread.Sleep(1500);
callback();
}
void MyCallback()
{
"Job done".Dump();
}
delegate void Callback();
输出
//在1500ms后
Job done
有人会问:为什么不直接调用函数,而要建立一个委托?
用委托而不是直接调用函数通常是为了提供更大的灵活性和可扩展性。委托可以让我们在代码中定义一个接口,以便在特定的任务完成后执行不同的操作。这样,可以在运行时指定回调函数,而不是在编写代码时硬编码特定的函数调用。
PickOne(10, 20, MySpecialRule).Dump();
int PickOne(int a, int b, MyRule rule)
{
if(rule(a, b) return a;
else return b;
}
bool MySpecialRule(int x, int y)
{
return true;
}
bool MySpecialRule2(int x, int y)
{
return false;
}
delegate bool MyRule(int x, int y);
输出
10
使用泛型
var rule = new MyRule<int>(MySpecialRule);
rule(10, 20).Dump();
bool MySpecialRule(int x, int y)
{
return true;
}
delegate bool MyRule(int x, int y);
.Net封装好的强类型委托:
Action 没有返回值
Func 有返回值
PickOne(10, 20, (a, b) => true).Dump(); //匿名委托
int PickOne(int a, int b, Func<int, int, bool> rule) //.Net封装
{
if(rule(a, b) return a;
else return b;
}
对比接口,委托的好处有:
1.委托可以更直接地表示一个单一操作或方法,使代码更加简洁。委托声明通常很简洁,适合于单个方法的调用,不需要像接口一样声明一个完整的类结构。
2.支持匿名方法和 lambda 表达式。
3.委托支持多播,允许一个委托实例包含多个方法
委托支持逆变与协变
1.协变(out):允许委托的返回类型从基类转换为派生类类型。适用于返回类型的委托,例如 Func。
var myDelegate = new MyDelegate(() => "hello");
delegate object MyDelegate();
2.逆变(in):允许委托的输入参数类型从派生类转换为基类类型。适用于参数类型的委托,例如 Action。
var myDelegate2 = new MyDelegate2(Foo);
void Foo(object obj) => Console.WriteLine("Foo");
delegate void MyDelegate2(string param);
委托的弊端
1.调用委托的时候,如果其中一个委托报错,则后面的不会被调用
2.只有最后一个的返回值才会作为委托的返回值
3.因为是数组,所以remove(-=)的复杂度是O(n),remove的时候,从后往前遍历。
4.线程不安全
委托和c/c++函数指针的区别
1.委托可以"指向"多个函数
2.委托可以指向同一个函数多次
3.函数是包含在类中的,所以函数引用也包含了所在对象的信息;而c/c++的函数指针只是函数的入口地址
事件(event)
事件是c#提供的语法糖,效果是:
1.将委托以私有变量的形式封装在类内,不让外面访问
2.对于委托进行了封装,从而定义add与remove的方法
3.在add与remove中通过互锁的方式提供线程安全性
我们可以自定义事件的add和remove操作,但是必须要注意线程安全性。
例子:使用 Interlocked.CompareExchange 实现线程安全的 add 和 remove
using System;
using System.Threading;
public class Demo
{
private Action _myEvent; // 内部委托,用于存储事件处理程序
public event Action MyEvent
{
add
{
Action original, updated;
do
{
original = _myEvent; // 获取当前事件委托
updated = (Action)Delegate.Combine(original, value); // 尝试添加新处理程序
}
while (Interlocked.CompareExchange(ref _myEvent, updated, original) != original);
}
remove
{
Action original, updated;
do
{
original = _myEvent; // 获取当前事件委托
updated = (Action)Delegate.Remove(original, value); // 尝试移除处理程序
}
while (Interlocked.CompareExchange(ref _myEvent, updated, original) != original);
}
}
public void InvokeEvent()
{
_myEvent?.Invoke();
}
}
工作原理
首先将 _myEvent 的当前值赋给 original。
使用 Delegate.Combine(original, value) 将新的事件处理程序添加到委托中,并将结果赋给 updated。
调用 Interlocked.CompareExchange(ref _myEvent, updated, original),尝试将 _myEvent 更新为 updated,但前提是 _myEvent 的当前值没有被其他线程修改(即 _myEvent 仍等于 original)。
如果 CompareExchange 成功(返回值等于 original),表示订阅成功;如果失败(返回值不等于 original),则说明其他线程修改了 _myEvent,需要重新尝试。
remove 访问器:
与 add 类似,首先获取 _myEvent 的当前值到 original。
使用 Delegate.Remove(original, value) 从委托中移除指定的处理程序,并将结果赋给 updated。
再次调用 Interlocked.CompareExchange,确保 _myEvent 仅在它仍等于 original 的情况下被更新。
通过循环确保成功地移除了事件处理程序。
事件只能在类的内部进行调用
var demo = new Demo();
demo.MyEvent += () => "hello".Dump();
demo.InvokeEvent();
class Demo
{
public event Action MyEvent; //实例化event, Action是委托类型
public void InvokeEvent()
{
MyEvent?.Invoke();
}
}
输出
hello