【问题标题】:Definition of "==" operator for DoubleDouble 的“==”运算符的定义
【发布时间】:2016-05-10 02:17:51
【问题描述】:

由于某种原因,我潜入了 Double 类的 .NET Framework 源代码,发现 == 的声明是:

public static bool operator ==(Double left, Double right) {
    return left == right;
}

同样的逻辑适用于每个运算符。


  • 这样定义的意义何在?
  • 它是如何工作的?
  • 为什么不创建无限递归?

【问题讨论】:

  • 我希望无限递归。
  • 我很确定它不会在任何地方与 double 进行比较,而是在 IL 中发出 ceq。这只是为了填写一些文档目的,但找不到来源。
  • 大概率这样可以通过Reflection得到这个算子。
  • 这永远不会被调用,编译器已经在(ceq opcode)中烘焙了相等逻辑,请参阅When is Double's == operator invoked?
  • @ZoharPeled 将双精度数除以零是有效的,将导致正无穷大或负无穷大。

标签: c# .net language-lawyer


【解决方案1】:

实际上编译器会将==操作符变成ceq IL代码,你提到的操作符不会被调用。

源代码中运算符的原因很可能是它可以从 C# 以外的语言调用,而不是直接(或通过反射)将其转换为 CEQ 调用。操作符中的代码编译成CEQ,所以没有无限递归。

其实如果通过反射调用操作符,可以看到操作符被调用(而不是CEQ指令),而且显然不是无限递归的(因为程序按预期终止):

double d1 = 1.1;
double d2 = 2.2;

MethodInfo mi = typeof(Double).GetMethod("op_Equality", BindingFlags.Static | BindingFlags.Public );

bool b = (bool)(mi.Invoke(null, new object[] {d1,d2}));

生成的 IL(由 LinqPad 4 编译):

IL_0000:  nop         
IL_0001:  ldc.r8      9A 99 99 99 99 99 F1 3F 
IL_000A:  stloc.0     // d1
IL_000B:  ldc.r8      9A 99 99 99 99 99 01 40 
IL_0014:  stloc.1     // d2
IL_0015:  ldtoken     System.Double
IL_001A:  call        System.Type.GetTypeFromHandle
IL_001F:  ldstr       "op_Equality"
IL_0024:  ldc.i4.s    18 
IL_0026:  call        System.Type.GetMethod
IL_002B:  stloc.2     // mi
IL_002C:  ldloc.2     // mi
IL_002D:  ldnull      
IL_002E:  ldc.i4.2    
IL_002F:  newarr      System.Object
IL_0034:  stloc.s     04 // CS$0$0000
IL_0036:  ldloc.s     04 // CS$0$0000
IL_0038:  ldc.i4.0    
IL_0039:  ldloc.0     // d1
IL_003A:  box         System.Double
IL_003F:  stelem.ref  
IL_0040:  ldloc.s     04 // CS$0$0000
IL_0042:  ldc.i4.1    
IL_0043:  ldloc.1     // d2
IL_0044:  box         System.Double
IL_0049:  stelem.ref  
IL_004A:  ldloc.s     04 // CS$0$0000
IL_004C:  callvirt    System.Reflection.MethodBase.Invoke
IL_0051:  unbox.any   System.Boolean
IL_0056:  stloc.3     // b
IL_0057:  ret 

有趣的是,整数类型不存在相同的运算符(在参考源中或通过反射),只有 SingleDoubleDecimalStringDateTime,这反驳了我的理论认为它们的存在是为了从其他语言中调用。显然你可以在没有这些运算符的情况下将其他语言中的两个整数等同起来,所以我们回到“为什么它们存在于double”这个问题上?

【讨论】:

  • 我能看到的唯一问题是 C# 语言规范规定重载运算符优先于内置运算符。所以当然,一个符合标准的 C# 编译器应该看到一个重载的运算符在这里可用并生成无限递归。唔。麻烦。
  • 这没有回答问题,恕我直言。它只解释了代码被翻译成什么,而不是为什么。根据 C# 语言规范的 7.3.4 二元运算符重载决议 部分,我也期望无限递归。我假设参考源 (referencesource.microsoft.com/#mscorlib/system/…) 在这里并不适用。
  • @DStanley - 我并不否认所生产的产品。我是说我无法将它与语言规范相协调。这就是令人不安的地方。我正在考虑仔细研究 Roslyn,看看我是否可以在这里找到任何特殊处理,但我目前还没有做好准备(错误的机器)
  • @Damien_The_Unbeliever 这就是为什么我认为它要么是规范的例外,要么是对“内置”运算符的不同解释。
  • 由于@Jon Skeet 尚未回答或对此发表评论,我怀疑这是一个错误(即违反规范)。
【解决方案2】:

这里的主要困惑是您假设所有 .NET 库(在本例中为扩展数值库,不是 BCL 的一部分)都是用标准 C# 编写的。情况并非总是如此,不同的语言有不同的规则。

在标准 C# 中,由于运算符重载解析的工作方式,您看到的这段代码会导致堆栈溢出。但是,该代码实际上并不是标准 C# 中的代码——它基本上使用了 C# 编译器的未记录功能。它没有调用操作符,而是发出以下代码:

ldarg.0
ldarg.1
ceq
ret

就是这样 :) 没有 100% 等效的 C# 代码 - 这在具有 您自己的 类型的 C# 中根本不可能。

即使这样,编译 C# 代码时也不会使用实际的运算符 - 编译器会进行一系列优化,例如在本例中,它用简单的 ceq 替换了 op_Equality 调用。同样,您不能在自己的 DoubleEx 结构中复制它 - 这是编译器的魔法。

这当然不是 .NET 中的独特情况 - 有大量无效的标准 C# 代码。原因通常是 (a) 编译器黑客和 (b) 不同的语言,以及奇怪的 (c) 运行时黑客(我在看着你,Nullable!)。

由于 Roslyn C# 编译器是 oepn 源代码,我实际上可以为您指出决定重载决议的地方:

The place where all binary operators are resolved

The "shortcuts" for intrinsic operators

当您查看快捷方式时,您会发现 double 和 double 之间的相等性导致了内在的 double 运算符,从不在类型上定义的实际 == 运算符中。 .NET 类型系统必须假装 Double 是任何其他类型,但 C# 不是 - double 是 C# 中的原语。

【讨论】:

  • 不确定我是否同意参考源中的代码只是“逆向工程”。该代码具有编译器指令 (#ifs) 和其他不会出现在已编译代码中的工件。另外,如果它是针对double 进行逆向工程的,那么为什么不对intlong 进行逆向工程呢?我确实认为源代码是有原因的,但相信在运算符内部使用 == 会被编译为防止递归的 CEQ。由于该运算符是该类型的“预定义”运算符(并且不能被覆盖),因此重载规则不适用。
  • @DStanley 我不想暗示 all 代码是逆向工程的。同样,double 不是 BCL 的一部分 - 它位于一个单独的库中,恰好包含在 C# 规范中。是的,== 被编译为 ceq,但这仍然意味着这是一个编译器黑客,您无法在自己的代码中复制,并且不是 C# 规范的一部分(就像 @987654340 Double 结构上的 @ 字段)。它不是 C# 的合同部分,因此将其视为有效的 C# 没有什么意义,即使它是使用 C# 编译器编译的。
  • @DStanely 我找不到真正的框架是如何组织的,但是在 .NET 2.0 的参考实现中,所有棘手的部分都只是编译器内在函数,用 C++ 实现。当然,仍然有很多 .NET 本地代码,但是像“比较两个双精度数”之类的东西在纯 .NET 中并不能很好地工作。这就是浮点数不包含在 BCL 中的原因之一。也就是说,代码在(非标准)C# 中实现,这可能正是您前面提到的原因 - 以确保其他 .NET 编译器可以将这些类型视为真正的 .NET 类型。
  • @DStanley 但是好吧,重点。我删除了“逆向工程”参考,并将答案改写为明确提及“标准 C#”,而不仅仅是 C#。并且不要将doubleintlong - intlong 视为所有 .NET 语言必须支持的原始类型。 floatdecimaldouble 不是。
【解决方案3】:

原始类型的来源可能会令人困惑。你见过Double 结构的第一行吗?

通常你不能像这样定义递归结构:

public struct Double : IComparable, IFormattable, IConvertible
        , IComparable<Double>, IEquatable<Double>
{
    internal double m_value; // Self-recursion with endless loop?
    // ...
}

原始类型在 CIL 中也有其原生支持。通常它们不被视为面向对象的类型。如果在 CIL 中用作 float64,则 double 只是一个 64 位值。但是,如果将其作为通常的 .NET 类型处理,则它包含一个实际值,并且它包含与任何其他类型一样的方法。

因此,您在此处看到的情况与操作员的情况相同。通常如果你直接使用 double 类型,它永远不会被调用。顺便说一句,它的源代码在 CIL 中是这样的:

.method public hidebysig specialname static bool op_Equality(float64 left, float64 right) cil managed
{
    .custom instance void System.Runtime.Versioning.NonVersionableAttribute::.ctor()
    .custom instance void __DynamicallyInvokableAttribute::.ctor()
    .maxstack 8
    L_0000: ldarg.0
    L_0001: ldarg.1
    L_0002: ceq
    L_0004: ret
}

如您所见,没有无限循环(使用ceq 工具而不是调用System.Double::op_Equality)。所以当一个双精度对象被当作一个对象来处理时,会调用操作符方法,最终将它作为 CIL 级别的float64 原始类型来处理。

【讨论】:

  • 对于本文第一部分不理解的人(可能是因为他们通常不编写自己的值类型),请尝试代码public struct MyNumber { internal MyNumber m_value; }。当然,它不能编译。错误是error CS0523: 'MyNumber' 类型的结构成员'MyNumber.m_value' 导致结构布局中出现循环
【解决方案4】:

我用 JustDecompile 查看了CIL。内部 == 被翻译成 CIL ceq 操作码。换句话说,它是原始的 CLR 相等性。

我很好奇 C# 编译器在比较两个双精度值时是否会引用 ceq== 运算符。在我想出的简单示例中(如下),它使用了ceq

这个程序:

void Main()
{
    double x = 1;
    double y = 2;

    if (x == y)
        Console.WriteLine("Something bad happened!");
    else
        Console.WriteLine("All is right with the world");
}

生成以下 CIL(注意带有标签 IL_0017 的语句):

IL_0000:  nop
IL_0001:  ldc.r8      00 00 00 00 00 00 F0 3F
IL_000A:  stloc.0     // x
IL_000B:  ldc.r8      00 00 00 00 00 00 00 40
IL_0014:  stloc.1     // y
IL_0015:  ldloc.0     // x
IL_0016:  ldloc.1     // y
IL_0017:  ceq
IL_0019:  stloc.2
IL_001A:  ldloc.2
IL_001B:  brfalse.s   IL_002A
IL_001D:  ldstr       "Something bad happened!"
IL_0022:  call        System.Console.WriteLine
IL_0027:  nop
IL_0028:  br.s        IL_0035
IL_002A:  ldstr       "All is right with the world"
IL_002F:  call        System.Console.WriteLine
IL_0034:  nop
IL_0035:  ret

【讨论】:

    【解决方案5】:

    如 System.Runtime.Versioning 命名空间的 Microsoft 文档中所述:在此命名空间中找到的类型旨在用于 .NET Framework,而不是用于用户应用程序。System.Runtime.Versioning 命名空间包含高级类型支持 .NET Framework 的并行实现中的版本控制。

    【讨论】:

    • System.Runtime.VersioningSystem.Double 有什么关系?
    猜你喜欢
    • 1970-01-01
    • 2021-01-29
    • 2016-01-26
    • 2015-12-14
    • 1970-01-01
    • 2021-07-03
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多