EnriqueMark
设计模式与架构

委托

2026-04 简体中文

C#的委托,事实上在DI里面就已经在用了。假设我定义了一个func类型,叫A,然后在下面A(),这里虽然我没显示写delegete关键字,这就已经是委托了。因为C#官方定义了三个封装好的委托类型:Func<T>Action<T>Predicate<T>,这三分别是有输出结果和没输出结果以及输出bool结果,最后一个不是很常用。

delegate关键字最常用的方法是下面这种场景:假设我有一个很复杂的方法,比如要接受五个值传入,那写func里会很麻烦,那不如直接delegete定义好,比如叫A,然后我就可以直接A a,而不用func 写的老长。

// 不自定义的话
Func<string, int, DateTime, bool, MyConfig, MyResult> handler = ...;

// 自定义 delegate 后
delegate MyResult DataHandler(string name, int count, DateTime time, bool flag, MyConfig config);

DataHandler handler = ...;  // 干净多了

另一个好处则是语义化,也就是字面意思上光看这个类型就能让人知道这是干嘛的,否则Func后面写一长串,就得加注释才能看得明白了。而另外则是 event 功能,需要借助delegate类型才能将其重新声明为事件。

/// 先声明一个dalegate
delegate void OnPlayerDead(Player player, string reason);
/// 然后再声明这是一个事件
event OnPlayerDead PlayerDied;

事件的场景是这样的:可以在需要的时候,一次性、动态的运行多个方法。比如一个队列,里面有十个方法调用,但某事件发生后,+=或-=,然后最终调用这个委托变量,哪怕我一开始写的是10次,它也许最终只会运行是5次或15次。这在需要灵活执行的地方非好用,也是经典的发布-订阅机制。发布方不关心订阅者如何操作,而只关注控制发布结果在何时执行。

事件往往是多播委托的替代。多轮委托指的就是一个delegate很容易在任何地方被重新用=重载,比如直接=null,那这个从字面上看就非常有风险了。就跟继承一样,难以预料核实会被重载,一旦出现一次,下游依赖全完。所以才会有事件,用来控制委托的操作范围:事件只能被增加或减少,而无法重载。也就是受控的多播委托。

// 普通多播委托 —— 外部可以为所欲为
// 大部分情况下直接声明为官方定义好的delegate类型就行了
public Action OnDead;

player.OnDead = null;      // ✅ 直接清空,其他模块挂的全没了
player.OnDead();           // ✅ 外部直接触发,不该这样
player.OnDead += MyMethod; // ✅

// event —— 外部只能 +/-
public event Action OnDead;

player.OnDead = null;      // ❌ 报错
player.OnDead();           // ❌ 报错
player.OnDead += MyMethod; // ✅

简单的说,委托就是一种方法的委派,我用一个变量代替了原先的方法,将其作为原先方法的代表来使用。从原方法的角度来看,就是我把自己委托给了被委派的对象。作为元素的时候尤其明显正因为这种情况下我必须得找一个变量代表原方法,否则我就无法操作。除此之外则是需要动态和灵活操作方法执行的时候,希望动态的执行不定数个方法时尤其明显。我能想到的一个场景就是用一个run方法,然后接受一个外部传入的枚举来判断什么情况下执行哪个——这种场合我就可以直接用委托,让事件发生时+=,不需要时-=,然后在最终执行这个被委派的变量即可——只需要一行,什么方法都不需要了。

我设想的场景不是写在一起,而是散在各处——比如我专门暴露了一个方法就是给外部使用的被委托变量,定义好(委托是不是+=的时候类型必须是和我定义好的一致)然后集中执行。我作为上级根本不关心你给了几个,具体几个你自己看情况加(动态的事件)。这种形式下似乎有点IoC的味道了,具体的执行反转出去了,方法内部专注于自身的逻辑,而不关心具体数量。这就是上面所说的发布-订阅,本质仍然是一种去耦合。