委托是类

委托是类,C# 提供了 delegate 关键字,使得用户能简单的声明委托。编译器及 CLR 做了大量的工作来隐藏委托的复杂性。[1]

internal delegate void FeedBack(int value);

编译器为如上的委托声明定义一个完整的类:

internal class FeedBack : System.MulticastDelegate
{// 构造器(Constructor)public FeedBack(object @object, IntPtr method);// 委托调用 [2]public virtual void Invoke(int value);// 委托异步调用public virtual IAsyncResult BeginInvoke(int value, AsyncCallback asyncCallback, object @object);public virtual void EndInvoke(IAsyncResult result);
}

可使用 ILDasm.exe 打开生成的程序集,查看这个自动生成的类。

编译器定义了 FeedBack 类,其派生自 FCL (Framework Class Library) 中的 System.MulticastDelegate 类 (所有的委托都派生自 MulticastDelegate,MulticastDelegate 派生自 Delegate)。

MulticastDelegate

所有的委托都派生自 MulticastDelegate,所以它们继承了 MulticastDelegate 的字段、属性与方法。在这些成员中,有三个非公共字段是最重要的:

字段 类型 说明
_target (Delegate 类的字段) System.Object 若委托对象包装静态方法时,此字段为 null
若委托对象包装实例方法时,此字段引用回调方法需要操作的对象 (实例方法所在的对象)
_methodPtr (Delegate 类的字段) System.IntPtr 根据平台而定的整数类型,CLR 使用它标记需要回调的方法
(因此上文 ILDasm 中的显示是 native int)
_invocationList System.Object 此字段通常为 null。构造委托链时,引用一个委托数组

委托的构造器有两个参数,一个是对象引用 (System.Object),另一个则是根据平台而定的整型 (System.IntPtr)。C# 编译器构造委托时,会分析源码以确定引用的对象及方法。对象引用被传给构造器的 object 参数,标识方法的特殊 IntPtr 值 (从 MethodDef 或 MemberRef 元数据 token 获得) 被传给构造器的 method 参数。对于静态方法,为 object 参数传递 null。构造器将这两个实参分别保存于 _target 及 _methodPtr。此外,构造器将 _invocationList 设为 null。

委托链/多播

委托链是委托对象的集合

合并

调用 Delegate.Combine(Delegate a, Delegate b) 方法对两个委托进行合并。

fbChain = (Feedback)Delegate.Combine(fbChain, fb1);

移除

调用 Delegate.Remove(Delegate source, Delegate value) 方法对两个委托进行合并。

fbChain = (Feedback)Delegate.Remove(fbChain, fb1);

原理

详情请参阅 Reference Source。

Combine

  • 若一个委托为 null

    返回非 null 委托

  • 若两个委托为 null

    返回 Combine 的第二个参数 (当然返回的也是 null)

  • 若都不为 null

    进行委托合并

    1. 判断两委托类型是否相同,不同则抛出 ArgumentException
    2. 合并两个委托对象中的委托 (包括委托链中的委托) (Object[] : resultList) (使用 for 遍历实现)
    3. 统计两个委托对象中的委托数 (int: invocationCount) (委托链不为空则统计委托链中的委托)
    4. 根据 resultList 及 invocationCount 构建新的委托对象并返回

Remove

注意,若 Remove 的目标为委托链,则该委托链需为当前操作委托对象委托链的连续子列表。[3]

  • 若指定需要去除的委托为空,直接返回当前委托
  • 若指定需要去除的委托不为空,在当前委托及其委托链中寻找目标委托或目标委托链,剔除并返回
    • 结果是委托链则构建新委托并返回
    • 结果是单一委托则直接返回该委托

不要定义过多的委托

Microsoft 在刚开始开发 .NET Framework 的时候引入了委托的概念。开发人员在 FCL 中添加类时,凡是有回调方法的地方都定义了新的委托类型。随时间的推移,他们定义的委托越来越多。仅在 MSCorLib.dll 中,就有接近 50 个委托类型,例如:

public delegate void TryCode(Object userData);
public delegate void WaitCallback(Object state);
public delegate void TimerCallback(Object state);
public delegate void ContextCallback(Object state);
public delegate void SendOrPostCallback(Object state);
public delegate void ParameterizedThreadStart(Object obj);
...

以上的示例委托,实际上都是一样的:这些委托引用的方法都是获取一个 Object 返回 void。没有必要定义这么多委托,定义一个就够了。

现在的 .NET Framework 支持泛型 (C# 2.0 版本引入),只需要几个泛型委托,就能表示多达16个参数的方法:

  • 从无参,到至多16个参数,返回值为 void 的 Action 委托:

    public delegate void Action(); // (这个不是泛型委托)
    public delegate void Action<in T>(T obj);
    public delegate void Action<in T1, in T2>(T1 arg1, T2 arg2);
    ...
    public delegate void Action<in T1, ... , in T16>(T1 arg1, ... , T16 arg16);
    
  • 从无参,到至多16个参数,返回值为 TResult 的 Func 委托:

    public delegate TResult Func<out TResult>();
    public delegate TResult Func<in T, out TResult>(T arg);
    ...
    public delegate TResult Func<in T1, ... , in T16, out TResult>(T1 arg1, ... , T16 arg16);
    

建议尽量使用以上的委托类型,而不是定义更多的委托类型。这样能减少系统中的类型数量,简化代码。

但若需使用 ref 或 out 关键字以传递引用的方式传递参数,可能不得自定义委托:

delegate void Foo(ref int bar);

event 关键字

event 关键字用于在发布类 (publisher class) 中声明事件。这是一种特殊的多播委托,仅能从声明事件的类或结构(发布类)中对其进行调用,否则产生编译器:event 的委托仅能作为 += 或 -= 的左值 (除非在其声明的类或结构中)。 如果其他类或结构订阅该事件,则在发布类引发该事件时,将调用其事件处理程序方法。 有关详细信息和代码示例,请参阅事件和委托。

public event Action action;

EventHandler

EventHandler 委托是一个预定义的委托,专门表示不生成数据的事件的事件处理程序方法。

public delegate void EventHandler(object? sender, EventArgs e);

如果事件生成数据,则必须使用泛型 EventHandler 委托类。

public delegate void EventHandler<TEventArgs>(object? sender, TEventArgs e);

委托的简化语法

Combine 与 Remove 的简化

C# 为委托重载 += 调用 Combine ,重载 -= 调用 Remove ,简化了委托链的构造。[4]

action1 = (Action) Delegate.Combine(action1, action2);
action1 += action2;
Delegate.Remove(action1, action2);
action1 -= action2;

不需要显式构造委托对象

仅仅是为了指定委托地址就构建一个对象显得有些奇怪,实际上构建委托对象是 CLR 的要求,该对象是包装器,可保证被包装的方法只能以类型安全的方式调用。C# 简化了委托的构建过程,不需要用户显示的使用 new 关键字进行委托的构造。

  • 未显式构造委托对象:

    public static void Main(string[] args)
    {Action action = PrintAction;action.Invoke();
    }private static void PrintAction()
    {Console.WriteLine("Action");
    }
    

  • 显式构造委托对象:

    // DON'T DO THIS
    public static void Main(string[] args)
    {Action action = new Action(PrintAction);action.Invoke();
    }private static void PrintAction()
    {Console.WriteLine("Action");
    }
    


对比显式构造与非显式构造的 IL code,他们都会构造一个 Action 委托实例。

不需要定义回调方法 (使用 lamdba 表达式)

不需要因构造委托而定义一个方法:

public static void Main(string[] args)
{Action action = PrintAction;action.Invoke();
}// NOT NEED
private static void PrintAction()
{Console.WriteLine("Action");
}

可以使用 lamdba 表达式简化回调:[5]

public static void Main(string[] args)
{Action action = () => Console.WriteLine("Action");action.Invoke();
}

局部变量不需要手动包装到类中即可传递给回调方法

有时可能希望回调代码引用类中定义的其他成员或方法中的局部参数:

internal class Program
{private static int bar = 21;public static void Main(string[] args){int foo = 21;Action action = () => Console.WriteLine(foo + bar); // Closure allocation: 'foo' variableaction.Invoke();}
}

实际上 lamdba 表达式主体的代码在一个单独的方法中 (CLR 的要求)。C# 通过自动辅助类实现闭包 (closure) [6]。在辅助类中,为需要传递给回调的每个值都定义一个字段。将回调方法定义为其实例方法。

构建回调方法实际上也构造了辅助类实例,使用方法中的局部变量的值初始化该实例中的字段,最后构造委托对象并绑定到该辅助对象的实例方法。

委托与反射

开发者可以在不知道回调方法的原型时使用回调。使用 MethodInfo.CreateDelegate,可在编译期不知道委托的所有必要信息的情况下创建委托:

// 构造包含静态方法的委托
public virtual Delegate CreateDelegate (Type delegateType);
// 构造包含实例方法的委托 (target 引用 this 实参)
public virtual Delegate CreateDelegate (Type delegateType, object? target);

创建完成后可用 Delegate.DynamicInvoke(Object[]) 调用它们:

// 调用委托并传递参数
public object? DynamicInvoke (params object?[]? args);

使用反射 API 获取引用了回调方法的 MethodInfo 对象,调用 CreateDelegate 构造委托 (如果是实例方法则需要传递 target 参数,指定其 this 参数)。

使用 DynamicInvoke 方法对委托对象的回调方法进行调用。DynamicInvoke 可传递一组参数,其在内部保证传递的参数与回调方法期望的参数兼容,兼容则调用回调方法,否则抛出 ArgumentException。若参数数不匹配,则抛出 TargetParameterCountException。

示例:

internal class Foo
{public static void StaticMethod() => Console.WriteLine(42);public void NonStaticMethod() => Console.WriteLine(42);public static void MethodWithPara(int num) => Console.WriteLine(num);}internal class Program
{public static void Main(string[] args){Foo delegateReflectionTest = new Foo();MethodInfo nonStaticMethodInfo = delegateReflectionTest.GetType().GetMethod("NonStaticMethod");Delegate delegate1 = nonStaticMethodInfo?.CreateDelegate(typeof(Action), delegateReflectionTest);delegate1?.DynamicInvoke();MethodInfo staticMethodInfo = delegateReflectionTest.GetType().GetMethod("StaticMethod");Delegate delegate2 = staticMethodInfo?.CreateDelegate(typeof(Action));delegate2?.DynamicInvoke();MethodInfo methodInfoWithPara = delegateReflectionTest.GetType().GetMethod("MethodWithPara");Delegate delegate3 = methodInfoWithPara?.CreateDelegate(typeof(Action<int>));delegate3?.DynamicInvoke(42);}}
}

参阅

Ildasm.exe(IL 反汇编程序)

一般,该工具位于 NETFX 4.7.2 Tools 中

C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.7.2 Tools\x64\ildasm.exe

如何合并委托(多播委托)- C# 编程指南 | Microsoft Docs

- - 和 -= 运算符 - C# 参考 | Microsoft Docs

+ 和 += 运算符 - C# 参考 | Microsoft Docs

注释

[1] 因此,可以定义类的地方,就可以定义委托。

[2] 这里把 Invoke 翻译为调用。但是要清楚 Invoke 和 Call 的区别,执行委托方法不是直接执行目标方法,而是从委托处援引 (Invoke) 目标方法执行。

[3] 实现细节请参阅 Reference Source Multicastdelegate ,算法为移除目标数组中的连续子序列

- - 和 -= 运算符 - C# 参考 | Microsoft Docs (委托删除) 中的示例:

Action a = () => Console.Write("a");
Action b = () => Console.Write("b");var abbaab = a + b + b + a + a + b;
var aba = a + b + a;var first = abbaab - aba;
first();  // output: abbaab
Console.WriteLine();
Console.WriteLine(object.ReferenceEquals(abbaab, first));  // output: TrueAction a2 = () => Console.Write("a");
var changed = aba - a;
changed();  // output: ab
Console.WriteLine();
var unchanged = aba - a2;
unchanged();  // output: aba
Console.WriteLine();
Console.WriteLine(object.ReferenceEquals(aba, unchanged));  // output: True

[4] 可通过查看 IL code 验证这点:


[5] 请参阅 => 运算符 - C# 参考 | Microsoft Docs (表达式主体定义)

[6] 捕获上下文的外部变量以在回调方法中使用。闭包有对外部变量的引用,所以可能导致外部变量所在的对象声明周期延长。

转载自:https://blog.ryuu64.top/CSharp-%E5%A7%94%E6%89%98/

C# 委托 (delegate)相关推荐

  1. C# 委托(Delegate) 事件(Event)应用详解

    委托 和 事件在 .Net Framework中的应用非常广泛,然而,较好地理解委托和事件对很多接触C#时间不长的人来说并不容易.它们就像是一道槛儿,过了这个槛的人,觉得真是太容易了,而没有过去的人每 ...

  2. C#委托(delegate、Action、Func、predicate)和事件

    C#委托(delegate.Action.Func.predicate)和事件 - 园子的蜗牛 - 博客园 C#之委托 - 摸鱼王 - 博客园 C++函数指针与C#委托之间有何联系 - 51CTO.C ...

  3. C#-----委托delegate的定义与使用

    委托是一种数据结构,它引用静态方法或引用类实例及该类的实例方法 委托的声明: delegate <函数返回类型> <委托名> (<函数参数>) //声明委托 del ...

  4. C#基础知识学习之 ✨ “精神小伙“——委托(delegate) 的含义和用法

    C#委托(Delegate) 在C#的学习过程中,自然有我们的精神小伙--委托 的一席之地 本文就介绍一下C#中的委托,这位可以包含同类方法函数的小伙子 C# 中的委托(Delegate)类似于 C ...

  5. 理解委托(delegate)及为什么要使用委托

    委托:是一种定义方法签名的类型. 当实例化委托时,您可以将其实例与任何具有兼容签名的方法相关联. 您可以通过委托实例调用方法. 上述为官方说法,理解起来比较难,举个生活中的例子: 某人有三子,让他们各 ...

  6. 异步使用委托delegate --- BeginInvoke和EndInvoke方法

    当我们定义一个委托的时候,一般语言运行时会自动帮委托定义BeginInvoke 和 EndInvoke两个方法,这两个方法的作用是可以异步调用委托. 方法BeginInvoke有两个参数: Async ...

  7. 跟小静学CLR via C#(12)-委托Delegate

    本来按照进度应该学习事件了,可总觉得应该委托在前,事件在后,才好理解. 委托是一个类,它提供了回调函数机制,而且是类型安全的.使用委托可以将方法当作另一个方法的参数来进行传递,这种将方法动态地赋给参数 ...

  8. jquery-事件委托-delegate

    代理的使用 如果希望某个成员绑定某个事件,例如li标签编定click事件 期望新进来的li也能得到这个事件 可以通过事件委托来实现 事件委托的格式 父级标签对象.delegate(str子级标签选择器 ...

  9. lambda表达式不使用委托(delegate) 用FUNC

    lLambda不使用delegate关键字,而使用  Lambda运算符 => goes to l    1.Func<int,string> getInput = (int age ...

  10. unity学习-委托(delegate),事件(event),Action,Func

    unity学习 委托 含义 定义 使用 事件 含义 使用 在进行项目的过程中,一直存在着对代码管理不当的问题,所以需要学习一些科学的代码思维,减少后期功能的修改与迭代引发的代码冗杂和内存爆炸 可以看b ...

最新文章

  1. Winform中自定义xml配置文件后对节点进行读取与写入
  2. 最短路径问题(信息学奥赛一本通-T1342)
  3. MFC PreTranslateMessage介绍
  4. Tips of keras
  5. Intergate flot with Angular js ——Angular 图形报表
  6. 科学计算机病毒代码大全,计算机病毒代码有哪些
  7. Mnist数据集介绍
  8. python毕业设计作品基于django框架 电影院购票选座系统毕设成品(5)任务书
  9. 2020年高压电工模拟考试题库及高压电工作业模拟考试
  10. Oracle9i学习之boobooke小布版001
  11. Linux命令·traceroute
  12. “天下文章一大抄”的时代已经过去
  13. 手机计算机怎么打出无限符号,Win7怎么用搜狗打无限符号|输入法打无限符号方法...
  14. ICCV2021-Soft Teacher-End-to-End Semi-Supervised Object Detection with Soft Teacher
  15. 图像处理与计算机视觉:3D射影变换
  16. Android关于虚拟控件、全面屏及悬浮球机型适配时遇到的问题
  17. 【signal】傅里叶分析和FFT蝶形算法
  18. 科研相关工具(更新中…)
  19. 大数据与公共管理变革
  20. Yeelink平台推送传感器结果——套接字编程 Windows平台

热门文章

  1. linux笔记:MOOC Linux开发环境及应用
  2. 【5min+】 秋名山的竞速。 ValueTask 和 Task
  3. 用标尺设置制表位(转)
  4. [ 网络协议篇 ] vlan 详解之 super vlan 详解
  5. win10插入外设自动播放关闭
  6. 联想y7000p安全锁孔怎么用_联想 拯救者Y7000P 触摸板怎么用?
  7. 飞机的黑匣子到底是什么?
  8. 谷歌出品的Web打包方案:Web Bundles 技术揭秘
  9. M608c M600i使用技巧
  10. 2023CS双非保研985经验分享(南大、华科、中科大科学岛、国防科大、西交、中南、深圳大学、北邮、中科院等)