【问题标题】:Implicit method group conversion gotcha (Part 2)隐式方法组转换陷阱(第 2 部分)
【发布时间】:2012-02-14 21:31:33
【问题描述】:

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();
    }        
}

上述代码的 CIL 中的运算符 ceq 导致“错误”(有关详细信息,请访问原始问题)。所以我的问题是:

(1) 为什么== 翻译成ceq 而不是call Delegate Equals

在这里,我不关心代表和动作之间的(取消)包装。最后,在评估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 中的所有运算符 op 声明并全部解除 此类运算符的形式,如果至少有一个运算符适用 (§7.5.3.1)关于参数列表A,那么集合 候选算子由T0中所有适用的算子组成。

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

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

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

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

【问题讨论】:

  • 这就是为什么在期望值类型语义时最好使用Equals。因为(可能)损坏的运算符重载。
  • @Groo:没错。顺便说一句,我收到了问题Possible unintended reference comparison; to get a value comparison, cast the right hand side to type 'System.Action'中代码的编译警告。
  • 这肯定与一般委托的特殊处理有关,因为使用用户定义的类层次结构尝试相同的事情会调用自定义 == 方法

标签: c# .net operator-overloading


【解决方案1】:

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

进一步:比较代表是危险的和非平凡的。每个委托实际上都是多播委托。您可以将多个函数指针添加到同一个委托。代表[L(); M(); N();] 是否等于代表[M();] ?函数指针包含类实例(例如实例方法)。 [a.M();] 是否等于 [b.M();]?这一切都取决于一个案例,比较实现需要逐步遍历调用列表。

从通用基类型 Delegate 继承的委托是隐式的,您可能会在其他情况下遇到这个问题,例如泛型约束:您不能将 Delegate 指定为泛型参数 T 的约束。这里编译器明确拒绝这一点。创建自己的类也是如此,继承自 Delegate。

这是对这两个问题的回答——“Delegate”不是纯粹的 FCL,它与编译器紧密耦合。如果您真的想要 Microsoft 的委托比较器行为 - 只需显式调用 Equals(a, b)

【讨论】:

  • 这是一个关于语言规范的问题,我认为“这是编译器魔法”不足以回答这类问题。
  • 是的,比较多播委托并非易事,但 C# 规范清楚地定义了它的工作原理。
【解决方案2】:

警告 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# 团队不想隐藏的费用。

并没有,您会收到警告,提醒您他们做出了不直观的选择。 .

【讨论】:

  • C# 中最令人惊讶的是,(Action)a == (Action)b(Delegate)a == (Delegate)b 都用于委托值比较,而 (Action)a == (Delegate)b 则用于引用相等检查(带有编译时警告)。这是不自然的,特别是因为Action 可以隐式转换(通过引用转换)为Delegate
  • 您的答案获得了一半的赏金(这很好)。但我仍然觉得 C# 编译器做了一些不容易看出与 C# 语言规范一致的事情,我想要一个关于这个的答案。另请参阅链接线程使用变体泛型委托类型对运算符 == 的重载解决方案。但我同意比较多播委托很昂贵,检查引用相等性非常便宜,并且 C# 和 VB.NET 中可能有不同的哲学来“优化”什么。
【解决方案3】:

有两种类型的运算符:用户定义的运算符和预定义的运算符。第 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)

【讨论】:

  • 那如果他变成Console.WriteLine((Delegate)a == b);,为什么输出会变成True?现在,两个操作数的编译时类型都不是“委托类型”(因为您说非具体类System.Delegate 本身不是“委托类型”),但仍然使用委托类型重载。 (我刚刚找到了这个帖子,因为我自己的问题Overload resolution on operator == with variant generic delegate types 已经链接到这个帖子了。)
【解决方案4】:

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

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

【讨论】:

  • 对于引用类型,== 运算符执行它为该特定类型定义的任何操作。许多类型不执行引用相等检查。
  • 我在我的语句中添加了“除非“==”运算符被覆盖”子句。关键是“==”op一般用来比较引用,Equals方法一般用来比较值。
  • @highphilospher:我正在撤消反对票,但 C# 不同意 Java 的 == 用于引用相等的哲学。大多数情况下不是。如果要引用相等,请使用object.ReferenceEquals 静态方法。
  • 我确实在 MSDN 中发现“==”的“预期行为”是这样的:“对于预定义的值类型,相等运算符 (==) 如果其操作数的值是"其他人也覆盖它以进行值基比较,但这是“默认”实现。 msdn.microsoft.com/en-us/library/53k8ybth(v=vs.100).aspx
  • 这是默认设置,是的。但它也是 Equals 方法的默认值。
猜你喜欢
  • 1970-01-01
  • 2012-04-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多