从this question简化并消除了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二进制运算符重载决策
x op y形式的一个操作,其中op是一个可重载的二元运算符,x是一个表达式 类型为X,y为Y类型的表达式,按如下方式处理:
•由X和Y提供的候选用户定义运算符集 确定操作运算符op(x,y)。该集包括 X和候选人提供的候选运营商联盟 Y提供的运营商,每个运营商都使用§7.3.5的规则确定。如果 X和Y是相同的类型,或者如果X和Y来自共同的 基类型,然后共享候选运算符只出现在组合中 设置一次。
•如果不是候选用户定义的运算符集 空,然后这成为了候选运算符的集合 操作。否则,预定义的二元运算符op 实现,包括他们提升的形式,成为一组 操作的候选操作员。预定义的实现 给定运算符的值在运算符的描述中指定 (§7.8至§7.12)。
•§7.5.3的重载决策规则是 应用于候选运算符集以选择最佳运算符 关于参数列表(x,y),该运算符变为 重载解析过程的结果。如果超载分辨率 如果无法选择单个最佳运算符,则会发生绑定时错误。
7.3.5候选用户定义的运算符
给定类型T和操作运算符op(A),其中op是可重载运算符,A是参数列表,由候选用户定义的运算符集提供者 运算符op(A)的T确定如下:
•确定类型 T0。如果T是可空类型,则T0是其基础类型,否则为T0 等于T.
•对于T0中的所有操作员操作声明并且全部解除 如果至少有一个运营商适用,则为此类运营商的形式 (§7.5.3.1)关于参数列表A,然后是集合 候选运营商由T0中的所有适用运营商组成。
•否则,如果T0是对象,则候选运算符集为空。
•否则,T0提供的候选运算符集就是集合 候选运营商提供的直接基类T0,或 如果T0是类型参数,则为有效基类T0。
根据规范,a和b具有相同的基类Delegate
,显然应在此处应用==
中定义的运算符规则Delegate
(运算符==调用Delegate.Equals实质上)。但现在看起来用户定义的运算符的候选列表是空的,并且最后应用了Object ==
。
(2)FCL代码是否应遵守C#语言规范?如果不是,我的第一个问题毫无意义,因为有些东西是特别对待的。然后我们可以回答所有这些问题,“哦,这是FCL的特殊处理,他们可以做我们做不到的事情。规范适用于外部程序员,不要太傻了。”
答案 0 :(得分:5)
编译器与代表的工作方式非常不同且不同寻常。有很多隐式处理。 请注意,本指南中的“常用基本类型”规则适用于“用户定义的运算符”。代表是内部和系统。例如,您可以编写Action a = M;
而不是Action a = new Action(M);
。之后您可以添加a += M;
。检查CIL中发生了什么,这是第一次有趣。
更多:比较代表是危险的,也是非常重要的。每个代表实际上都是多播委托。您可以向同一个委托添加多个函数指针。代表[L(); M(); N();]
是否等于委派[M();]
?函数指针包含类实例(例如方法)。 [a.M();]
是否等于[b.M();]
?所有这些都取决于案例,并且比较实现需要逐步调用调用列表。
从公共基类型代表继承委托是隐式的,您可以在其他方案中遇到此问题,例如泛型约束:您不能将Delegate指定为泛型参数T的约束。这里编译器明确拒绝这一点。关于创建自己的类,从Delegate继承。
这是两个问题的答案 - '委托'不是纯粹的FCL,而是与编译器紧密结合。如果您真的想要Microsoft的委托比较器行为 - 只需显式调用Equals(a, b)
答案 1 :(得分:4)
有两种类型的运算符:用户定义的运算符和预定义的运算符。第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)
。
答案 2 :(得分:3)
警告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代码,万一你不认识它。否则,微软保留两种主要托管语言的原因,即使它们具有非常相似的功能。但具有非常不同的可用性选择。每当生成代码的方式不止一种时,C#团队就一直选择性能,VB.NET团队始终为方便。
性能当然是关键所在,比较委托对象昂贵。这些规则在Ecma-335第II.14.6.1节中有详细说明。但是你可以自己推理,有很多检查要做。它需要检查委托目标对象是否兼容。并且对于每个参数,它必须检查该值是否可转换。 C#团队不想隐藏的费用。
并且没有,你得到警告提醒你他们做出了不直观的选择。
答案 3 :(得分:-1)
这里的关键是==
运算符和Equals
类型的Delegate
方法是两个不同的东西。对于引用类型,==
查看两个引用是否指向同一个对象,除非重写==
运算符(请参阅:== Operator (C#))。
由于您正在创建两个不同的Action
对象,即使它们在内部调用相同的方法,它们也是内存中不同位置的不同对象,并且不是值或string
类型,因此==
在这种情况下是ReferenceEquals
,并且不会调用Delegate.Equals
方法,该方法已被覆盖以查看这两个对象是否执行相同的操作。对于string
以外的参考类型,这是==
或Equals
的默认行为。