隐式方法组转换问题(第2部分)

从这个问题简化并摆脱了LinqPad的可能影响(没有密集),这样一个简单的控制台应用程序:

public class Program { static void M() { } static void Main(string[] args) { Action a = new Action(M); Delegate b = new Action(M); Console.WriteLine(a == b); //got False here Console.Read(); } } 

“false”结果来自上面代码的CIL中的运算符ceq (有关详细信息,请访问原始问题)。 所以我的问题是:

(1)为什么==正在转换为ceq而不是call Delegate Equals

在这里,我不关心Delegate和Action之间的(un)包装。 最后,在评估a == b ,a是Action类型,而b是Delegate 。 从规格:

7.3.4二元运算符重载决策

forms为x op y的操作,其中op是可重载的二元运算符,x是类型X的表达式,y是类型Y的表达式,按如下方式处理:

•确定由操作运算符op(x,y)的X和Y提供的候选用户定义运算符集。 该集合由X提供的候选运算符与Y提供的候选运算符的并集组成,每个运算符使用§7.3.5的规则确定。 如果X和Y是相同的类型,或者如果X和Y是从公共基类型派生的,则共享候选运算符仅出现在组合集中一次。

•如果候选用户定义的运算符集不为空,则此变为该操作的候选运算符集。 否则,预定义的二元运算符op实现(包括它们的提升forms)成为该操作的候选运算符集。 给定运算符的预定义实现在运算符的描述中指定(第7.7节到第7.12节)。

•§7.5.3的重载决策规则应用于候选运算符集,以根据参数列表(x,y)选择最佳运算符,此运算符将成为重载决策过程的结果。 如果重载决策无法选择单个最佳运算符,则会发生绑定时错误。

7.3.5候选用户定义的运算符

给定类型T和操作运算符op(A),其中op是可重载运算符,A是参数列表,由T为运算符op(A)提供的候选用户定义运算符集合如下确定:

•确定类型T0。 如果T是可空类型,则T0是其基础类型,否则T0等于T.

•对于T0中的所有运算符op声明和此类运算符的所有提升forms,如果对于参数列表A至少有一个运算符适用(第7.5.3.1节),则候选运算符集由所有此类适用运算符组成。 T0。

•否则,如果T0是对象,则候选运算符集为空。

•否则,T0提供的候选运算符集合是由T0的直接基类提供的候选运算符集合,或者如果T0是类型参数则是T0的有效基类。

从规范中,a和b具有相同的基类Delegate ,显然在Delegate定义的运算符规则==应该在这里应用(operator ==从本质上调用Delegate.Equals)。 但现在看起来用户定义的运算符的候选列表是空的,最后应用了Object ==

(2)FCL代码是否应遵守C#语言规范? 如果不是,我的第一个问题毫无意义,因为有些东西是特别对待的。 然后我们可以回答所有这些问题,“哦,这是FCL的特殊处理,他们可以做我们做不到的事情。规范适用于外部程序员,不要傻”。

有两种类型的运算符:用户定义的运算符和预定义的运算符。 第7.3.5节“候选用户定义的运算符”不适用于预定义的运算符。 例如, decimal运算符看起来像反编译器中的用户定义运算符,但C#将它们视为预定义运算符并对它们应用数字提升(数字提升不应用于用户定义的运算符)。

第7.10.8节“委托相等运算符”将operator ==(Delegate, Delegate)定义为预定义运算符,因此我认为有关用户定义运算符的所有规则都不适用于此运算符(尽管这不是在这种情况下,规范中100%清除,只要用户定义的运算符(),预定义运算符就不会应用。

 Every delegate type implicitly provides the following predefined comparison operators: bool operator ==(System.Delegate x, System.Delegate y); bool operator !=(System.Delegate x, System.Delegate y); 

System.Delegate本身不被视为委托类型,因此重载解析的唯一候选operator ==(object, object)operator ==(object, object)

编译器与代表的工作方式非常不同和不同寻常。 有很多隐式处理。 请注意,本指南中的“公共基本类型”规则适用于“用户定义的运算符”。 代表是内部和系统。 例如,您可以编写Action a = M; 代替Action a = new Action(M); 。 你可以加a += M; 之后。 检查CIL中发生了什么,这是第一次有趣。

更多:比较代表是危险的,不重要的。 每个代表实际上都是多播委托。 您可以向同一个委托添加多个函数指针。 代表是否[L(); M(); N();] [L(); M(); N();] [L(); M(); N();]等于委托[M();] ? 函数指针包含类实例(例如方法)。 [aM();]是否等于[bM();] ? 所有这些都取决于案例,并且比较实现需要逐步调用调用列表。

委托从公共基类型委托inheritance是隐式的,你可以在另一个场景中遇到这个问题,例如generics约束:你不能将Delegate指定为generics参数T的约束。这里编译器明确拒绝这一点。 关于创建自己的类,从Delegateinheritance的相同。

这是两个问题的答案 – ‘委托’不是纯粹的FCL,而是与编译器紧密结合。 如果你真的想要微软的委托比较器行为 – 只需要明确调用Equals(a, b)

警告CS0253:可能的意外参考比较; 要获得值比较,请在右侧输入“System.Action”

这是对C#代码的警告。 不要忽视该警告,C#团队非常清楚他们为此比较生成的代码是意外的。 他们不必生成代码,他们可以很容易地完成您的预期。 像这样的代码做:

 Module Module1 Sub M() End Sub Sub Main() Dim a = New Action(AddressOf M) Dim b = DirectCast(New Action(AddressOf M), [Delegate]) Console.WriteLine(a = b) ''got True here Console.Read() End Sub End Module 

哪个生成几乎相同的MSIL,除了你得到的不是ceq

  IL_001d: call bool [mscorlib]System.Delegate::op_Equality(class [mscorlib]System.Delegate, class [mscorlib]System.Delegate) 

你希望C#代码能做什么。 这是VB.NET代码,万一你不认识它。 否则,微软保留两种主要托管语言的原因,即使它们具有非常相似的function。 但是具有非常不同的可用性选择。 每当生成代码的方式不止一种时,C#团队就会一直选择性能 ,VB.NET团队始终如一。

并且性能当然是关键,比较委托对象是昂贵的 。 这些规则在Ecma-335第II.14.6.1节中有详细说明。 但是你可以自己推理,有很多检查要做。 它需要检查委托目标对象是否兼容。 并且对于每个参数,它必须检查该值是否可转换。 C#团队不想隐藏的费用。

并且没有,你得到警告提醒你他们做出了不直观的选择。 。

这里的关键是==运算符和Delegate类型的Equals方法是两个不同的东西。 对于引用类型, ==查看两个引用是否指向同一个对象,除非重写==运算符(请参阅: ==运算符(C#) )。

由于您正在创建两个不同的Action对象,即使它们在内部调用相同的方法,它们也是内存中不同位置的不同对象,并且不是值或string类型,所以==在这种情况下是ReferenceEquals ,并且不调用Delegate.Equals方法,该方法已被重写以查看两个对象是否执行相同的操作。 对于除string以外的引用类型,这是==Equals的默认行为。