>> >> >> Reference << << << <<<<<<Ref>>>>>>
Delegate
Modified: 2025-06-01 | Author:ljf12825

委托是类型安全的函数指针——它把方法当作值来传递、存储、组合和调用
可以把“要做的动作”抽象成一个委托类型,然后把不同的方法绑定到它上面

声明与使用

using System;

public delegate void DelegateTest(int x);

public class FuncLib
{
    public static void Foo(int x) => Console.WriteLine("foo" + x);
    public static void Bar(int x) => Console.WriteLine("bar" + x);

    public void Func1(int x) => Console.WriteLine("func1" + x);
}

class Program
{

    public static void Func_inner(int x) => Console.WriteLine("funcinner" + x);

    static void Main()
    {
        FuncLib funclib = new();
        DelegateTest d = FuncLib.Foo;
        d += FuncLib.Bar;
        d += funclib.Func1;

        d(4);

        DelegateTest dd = x => Console.WriteLine("inner" + x);
        dd(5);
    }
}

委托绑定的函数具有以下约束

public class MyClass
{
    private void PrivateMethod(int x) => Console.WriteLine("private");
    protected void ProtectedMethod(int x) => Console.WriteLine("protected");
    internal void InternalMethod(int x) => Console.WriteLine("internal");
    public void PublicMethod(int x) => Console.WriteLine("public");
}

class Program
{
    static void Main()
    {
        MyClass obj = new MyClass();
        DelegateTest d;

        d = obj.PublicMethod; // 可以访问
        d = obj.InternalMethod; // 同程序集可以访问

        // d = obj.PrivateMethod; // 不可访问
        // d = obj.ProtectedMethod; // 不可访问
    }
}
class Program
{
    private void InstanceMethod(int x) => Console.WriteLine("instance");
    private static void StaticMethod(int x) => Console.WriteLine("static");

    DelegateTest instanceDelegate = InstanceMethod; // 实例字段绑定实例方法
}
static void Main()
{
    // 局部方法
    void LocalMethod(int x) => Console.WriteLine($"local: {x}");

    DelegateTest d = LocalMethod; // 可以绑定局部方法
    d(5);

    // 注意:委托会延长局部方法的生命周期
    // 即使退出Main方法,LocalMethod也不会被立即回收
}

常见内置委托

不用显式声明类型时常用

// Action
Action action1 = () => Console.WriteLine("Hello, World!");
action1();

// Action<T>
Action<string> action2 = (name) => Console.WriteLine($"Hello, {name}!");
action2("Alice");

// Action<T1, T2>
Action<string, int> action3 = (name, age) => Console.WriteLine($"{name} is {age} years old");
action3("Bob", 25);

// 多播
Action multiAction = () => Console.WriteLine("First action");
multiAction += () => Console.WriteLine("Second action");
multiAction += () => Console.WriteLine("Third action");
multiAction();
// Func<TReturn>
Func<int> func1 = () => 42;
Console.WriteLine($"Result: {func1()}");

// Func<T, TReturn>
Func<int, string> func2 = (x) => $"Number: {x}";
Console.WriteLine(func2(100));

// Func<T1, T2, TReturn>
Func<int, int, int> func3 = (a, b) => a + b;
Console.WriteLine($"Sum: {func3(5, 3)}");

// 复杂示例:字符串处理
Func<string, string, string> formatName = (firstName, lastName) => $"{lastName}, {firstName}";
Console.WriteLine(formatName("John", "Doe"));

// 多播(只返回最后一个方法的返回值)
Func<int> multiFunc = () => { Console.WriteLine("First"); return 1; };
multiFunc += () => { Console.WriteLine("Second"); return 2; };
int result = multiFunc(); // 输出:First, Second, 返回2
// Predicate<T>
Predicate<int> isEven = (x) => x % 2 == 0;
Console.WriteLine($"Is 10 even? {isEven(10)}");
Console.WriteLine($"Is 7 even? {isEven(7)}");

// 字符串判断
Predicate<string> isLongEnough = (s) => s.Length >= 5;
Console.WriteLine($"Is 'hello' long enough? {isLongEnough("hello")}");

// 在List中使用Predicate
List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

// 使用Predicate查找所有偶数
List<int> evenNumber = numbers.FindAll(isEven);
Console.WriteLine("Even numbers: " + string.Join(", ", evenNumber));

// 使用匿名方法
Predicate<int> isGreaterThan5 = (x) => x > 5;
List<int> largeNumbers = numbers.FindAll(isGreaterThan5);
Console.WriteLine("Numbers > 5: " + string.Join (", ", largeNumbers));
// Comparison
List<string> names = new List<string>
{
    "John", "Alice", "Bob", "Zoe", "Mike"
};

// Comparison<T>
Comparison<string> byLength = (x, y) => x.Length.CompareTo(y.Length);
Comparison<string> alphabetically = (x, y) => string.Compare(x, y);
Comparison<string> byLengthDesc = (x, y) => y.Length.CompareTo(x.Length);

// 按长度排序
names.Sort(byLength);
Console.WriteLine("By length: " + string.Join(", ", names));

// 按字母顺序排序
names.Sort(alphabetically);
Console.WriteLine("Alphabetically: " + string.Join(", ", names));

// 按长度降序排序
names.Sort(byLengthDesc);
Console.WriteLine("By length desc: " + string.Join(", ", names));
// Converter
List<int> numbers_ = new List<int> { 1, 2, 3, 4, 5 };

// Converter<TInput, TOutput>
Converter<int, string> intToString = (x) => $"Number: {x}";
Converter<int, double> intToDouble = (x) => x * 1.5;

// 转换整个列表
List<string> stringNumbers = numbers_.ConvertAll(intToString);
List<double> doubleNumbers = numbers_.ConvertAll(intToDouble);

// 复杂转换示例
Converter<string, int> parseToInt = (s) =>
{
    if (int.TryParse(s, out int result))
        return result;
    return 0;
};

List<string> stringValues = new List<string> { "10", "20", "abc", "30" };
List<int> parsedNumbers = stringValues.ConvertAll(parseToInt);
Console.WriteLine("Parsed numbers: " + string.Join(", ", parsedNumbers));

绑定与多播

绑定(Binding)

绑定就是将具体的方法与委托实例关联起来的过程

  1. 直接绑定单个方法
void Show(string msg) => Console.WriteLine(msg);

MyDelegate del = Show; // 绑定
del("Hello"); // 调用
  1. 绑定静态方法
static void Print(string msg) => Console.WriteLine(msg);

MyDelegate del = Print;
del("Static method");

静态方法没有目标对象,委托内部的Target字段为null

  1. 绑定实例方法
var obj = new Program();
MyDelegate del = obj.Show;

实例方法绑定时,委托内部保存着

调用时就像obj.Show(msg)一样执行

多播(Multicast)

一个委托实例可以绑定多个方法,调用时会按顺序执行所有方法

  1. 使用++=
void A(string msg) => Console.WriteLine("A: " + msg);
void B(string msg) => Console.WriteLine("B: " + msg);

MyDelegate del = A;
del += B; // 多播绑定
del("Hi");

输出

A: Hi
B: Hi
  1. 使用-=取消绑定
del -= A;
del("Hi again");

输出

B: Hi again

多播委托内部维护一个调用列表(Invocation List)
可以通过

Delegate[] list = del.GetInvocationList();

来查看有哪些函数被绑定
当执行del()时,运行时会按列表顺序依次调用每个目标函数

对于多播委托

delegate int Calc();

Calc c = () => 1;
c += () => 2;
int result = c(); // result == 2
delegate int Func();

Func f = () => Console.WriteLine(1);
f += () => Console.WriteLine(2);
f = () Console.WriteLine(3);
f(); // 3

委托的底层

委托在C#中本质上就是一个类
它并不是“语法糖”或“函数指针”,而是一个真正的类实例————一个封装了“函数入口 + 调用目标”的对象
但这东西的地位很特殊:

委托的编译后结构

源代码 vs 编译后代码

// 源代码
public delegate void MyDelegate(string message);

// 编译后实际上会生成一个完整的类
public class MyDelegate : System.MulticastDelegate
{
    // 构造函数
    public MyDelegate(object target, IntPtr methodPtr); // 构造函数的参数决定其可以接受任意类型,任意类型函数

    // 调用方法
    public virtual void Invoke(string message);

    // 异步调用方法
    public virtual IAsyncResult BeginInvoke(string message, AsyncCallback callback, object state);

    public virtual void EndInvoke(IAsyncResult result);
}

这说明

虽然语法上看是class,但它不是普通的用户类,而是CLR特殊支持类型
DelegateMulticastDelegate是由运行时(Runtime)内部硬编码逻辑管理的
也就是说:编译器和CLR一起为委托类型生成了“隐藏的行为”

看起来很普通,但关键在于

构造过程的底层逻辑

当写

MyDelegate d = new MyDelegate(SomeMethod);

编译器其实转成

d = (MyDelegate)Delegate.CreateDelegate(typeof(MyDelegate), null, methodInfo);

Delegate.CreateDelegate的底层逻辑是

  1. 检查方法签名是否和MyDelegate匹配
  2. 拿到该方法的入口地址(IntPtr methodPtr
  3. 如果是实例方法,记录目标对象(target
  4. 创建一个委托对象
  5. _methodPtr_target写进去
  6. 返回一个完整可调用的委托实例

所以每一个委托实例,本质上就是

{
    _target = 对象指针(或null)
    _methodPtr = 方法JIT后入口地址
}

只要这个方法签名匹配,CLR就能让它调用起来

这其实跟C++的函数指针没本质区别
可以这样类比

typedef void (*MyDelegate)(int);

这个函数指针可以指向任意void f(int)的函数,因为签名匹配

C#委托就是它的“安全封装版”

C#委托就是类型安全的函数指针对象

委托的内存布局

委托对象的内部结构

public abstract class Delegate
{
    // 三个核心字段
    private object _target; // 方法所属的对象实例(静态方法时为null)
    private IntPtr _methodPtr; // 方法指针
    private IntPtr _invocationList; // 多播委托的调用列表
}

C#的MulticastDelegate正是基于_invocationList实现多播

实际内存示例

class Program
{
    public static void StaticMethod(string msg) { }
    public void InstanceMethod(string msg) { }

    static void Main()
    {
        // 单播委托
        MyDelegate del1 = StaticMethod;
        MyDelegate del2 = new Program().InstanceMethod;

        // 内存结构
        // del1: _target = null, _methodPtr = StaticMethod地址
        // del2: _target = Program实例,_methodPtr = InstanceMethod地址
    }
}

多播委托的底层实现

调用链的存储方式

static void Main()
{
    MyDelegate multicast = Method1;
    multicast += Method2;
    multicast += Method3;

    // 底层实现
    // multicast._invocationList 指向一个数组
    // [Delegate(Method1), Delegate(Method2), Delegate(Method3)]
}

多播委托的调用过程

// 伪代码展示多播委托的Invoke实现
public void Invoke(string message)
{
    if (_invocationList == null)
    {
        // 单播委托:直接调用
        _methodPtr.Invoke(_target, message);
    }
    else
    {
        // 多播委托:遍历调用列表
        var delegates = (Delegate[])_invocationList;
        foreach (var del in delegates)
        {
            del.Invoke(message);
        }
    }
}

通过ILDASM查看实际IL代码

源代码

public delegate void MyDelegate(string msg);

class Program
{
    public static void Method1(string msg) { }
    public static void Method2(string msg) { }

    static void Main()
    {
        MyDelegate del = Method1;
        del += Method2;
        del("test");
    }
}

对应的IL代码(简化)

// 委托实例化
IL_0000: ldnull
IL_0001: ldftn void Program::Method1(string)
IL_0007: newobj instance void MyDelegate::.ctor(object, native int)

// 多播委托组合
IL_000c: ldnull  
IL_000d: ldftn void Program::Method2(string)
IL_0013: newobj instance void MyDelegate::.ctor(object, native int)
IL_0018: call class [mscorlib]System.Delegate 
        [mscorlib]System.Delegate::Combine(class [mscorlib]System.Delegate, 
                                          class [mscorlib]System.Delegate)

// 委托调用
IL_001d: ldstr "test"
IL_0022: callvirt instance void MyDelegate::Invoke(string)

性能与开销

委托的本质(性能角度)

委托本质是一个类对象,继承自System.MulticastDelegate
它不是轻量的“函数指针”,而是一个包含以下信息的结构体/对象

class MulticastDelegate 
{
    object Target; // 实例引用(如果静态方法则为null)
    IntPtr MethodPtr; // 方法指针
    MulticastDelegate Prev; // 用于形成调用链(多播)
}

每次写

Action a = Foo;

编译器背后做的是

  1. 创建一个新的委托对象
  2. 在其中记录目标实例和方法指针

这意味着:

创建成本(绑定阶段)

场景开销
绑定静态方法较低(一次分配)
绑定实例方法稍高(需要保存目标引用)
多播(+=较高(新建委托链对象)
Lambda捕获最高(生成闭包类 + 实例化)
Action a = Foo; // 创建一个委托对象
a += Bar; // 实际上创建了一个新的MulticastDelegate

每次+=都会产生一个新对象,因为MulticastDelegate是不可变的

因此频繁地+=/-=会产生GC压力;尤其在事件系统中,建议提前缓存委托对象,不要每帧都新建

调用成本(执行阶段)

调用委托的成本主要来自

  1. 一次间接跳转(类似虚函数调用)
  2. 安全检查与封装开销
  3. 多播时的循环遍历

大致性能对比(以Release模式为基准)

调用方式相对耗时说明
直接方法调用1x基准
虚函数调用~1.2x有一次虚表跳转
单播委托调用~1.5x额外的委托封装层
多播委托调用(3个目标)~3.5x逐个调用InvocationList

在现代JIT(RyuJIT)下,单播委托的调用几乎能被内联优化到接近直接调用。但多播或捕获闭包的情况仍然较慢

闭包带来的额外成本

int x = 0;
Action a = () => x++;

编译器生成

class DisplayClass
{
    public int x;
    public void Lambda() { x++; }
}

这会带来

所以如果在Update、Timer或循环中频繁创建lambda,GC压力会显著上升

优化策略

多播调用链的开销

当有多播委托时

Action a = Foo;
a += Bar;
a += Baz;
a();

执行过程:

  1. 获取InvocationList
  2. 遍历每个目标
  3. 逐个调用

这意味着时间复杂度是O(n)
当n较大时(例如事件订阅几十个监听器),性能会线性下降

JIT优化与委托内联

现代.NET JIT(RyuJIT、CoreCLR)在以下场景可内联委托

Action a = Foo;
a(); // JIT可直接内联为 Foo();

常见陷阱

引用捕获陷阱(Lambda闭包)

  1. 捕获外部变量的生命周期延长
List<Action> actions = new List<Action>();
for (int i = 0; i < 3; i++)
{
    actions.Add(() => Console.WriteLine(i));
}
foreach (var a in actions) a();

输出

3
3
3

因为Lambda捕获了变量i的引用,而不是当时的值。i在循环结束后等于3,所以所有委托都输出3
正确做法:

for (int i = 0; i < 3; i++)
{
    int copy = i;
    actions.Add(() => Console.WriteLine(copy));
}
  1. 捕获导致GC无法回收 如果一个委托捕获了外部对象引用(比如this或局部变量),即使那个对象已经“理论上”不需要了,委托持有的闭包对象依然会让它存活
    结果:内存泄露

事件解绑陷阱

button.Click += (s, e) => DoSomething();
button.Click -= (s, e) => DoSomething(); // 无效解绑

这两个Lambda是不同的实例,所以第二行不会解除绑定
正确做法

EventHandler handler = (s, e) => DoSomething();
button.Click += handler;
button.Click -= handler; // 有效解绑

陷阱实质

性能陷阱

  1. 多播委托开销 多播委托(+=多个方法)内部是也给调用链表,每次Invoke()都会遍历调用
foreach (var d in invocationList)
    d.DynamicInvoke(args);

问题

在高频调用场景(如游戏Update循环)中,多播委托不适合直接使用。可以用事件分发器(如UnityEvent、自定义ActionList)优化

异步与委托陷阱

  1. 异步捕获上下文
button.Click += async(s, e) => await.LongTask();

如果LongTask()抛异常,事件调用者无法感知异常(因为返回的是void异步委托)
推荐

button.Click += async (s, e) => {
    try { await LongTask(); }
    catch (Exception ex) { Log(ex); }
}

静态与实例绑定陷阱

委托可绑定

在某些情况下,忘记解除绑定的实例方法会持有整个对象的引用,导致

eventHandler += obj.SomeMethod; // obj无法被回收

这会引起对象长期驻留内存,尤其是订阅了全局事件(例如静态事件)
解决方案:

Delegate.Combine/Remove的逻辑陷阱

Action a = A;
a += B;
a += A;
a -= A;
a();

输出

B
A

因为-=只移除最后一次出现的匹配项,要清除所有A,必须循环调用-=或重新构造链

泛型委托的类型匹配陷阱(协变/逆变)

Func<object> f1 = () => "hello";
Func<string> f2 = f1; // 编译错误(逆变不允许)

协变和逆变在delegate类型参数上有限制

理解错会导致委托赋值异常

Delegate.DynamicInvoke性能坑

使用DynamicInvoke()调用委托非常慢(涉及反射调用和装箱拆箱),除非必要,不要再热路径使用
替代方案:直接调用委托

delegateInstance(param); // 比DynamicInvoke快几十倍