何时为Double';s==调用了运算符

本文关键字:调用 运算符 Double 何时 | 更新日期: 2023-09-27 18:00:17

这一切都始于有人向我提出的一个技巧问题

Double a = Double.NaN;
Console.WriteLine(a == a); // => false
Console.WriteLine(a.Equals(a)); // => true

以上内容似乎不对。a应该总是==到它自己(引用相等)&两者应该是一致的。

看起来Double重载了==运算符。反射器确认如下:

[__DynamicallyInvokable]
public static bool operator ==(double left, double right)
{
    return (left == right);
}

奇怪的是,这看起来是递归的,并且没有提到NaN特定的行为。那么,为什么它返回false呢?

所以我添加了更多的代码来区分

var x = "abc";
var y = "xyz";
Console.WriteLine(x == y); // => false

现在我看到

    L_0001: ldc.r8 NaN
    L_000a: stloc.0 
    L_000b: ldloc.0 
    L_000c: ldloc.0 
    L_000d: ceq 
    L_000f: call void [mscorlib]System.Console::WriteLine(bool)
    L_0014: nop 
    L_0015: ldloca.s a
    L_0017: ldloc.0 
    L_0018: call instance bool [mscorlib]System.Double::Equals(float64)
    L_001d: call void [mscorlib]System.Console::WriteLine(bool)
    L_0022: nop 
    L_0023: ldstr "abc"
    L_0028: stloc.1 
    L_0029: ldstr "xyz"
    L_002e: stloc.2 
    L_002f: ldloc.1 
    L_0030: ldloc.2 
    L_0031: call bool [mscorlib]System.String::op_Equality(string, string)
    L_0036: call void [mscorlib]System.Console::WriteLine(bool)
  • 对于双打,==运算符调用转换为ceq IL操作码
  • 其中,对于字符串,它转换为System.String::op_Equality(String,String)

果不其然,ceq的文档指定它是针对浮点数字和NaN的特殊大小写。这就解释了观察结果。

问题:

  • 为什么在Double上定义op_Equality?(该实现不考虑NaN的特定行为)
  • 何时调用

何时为Double';s==调用了运算符

反射器的错误解释

你从Reflector看到的反编译实际上是Reflector中的一个bug。反射器需要能够反编译一个函数,其中两个替身正在进行比较;在这些函数中,您会发现ceq直接发射到代码中。因此,Reflector将ceq指令解释为==介于两个doubles之间,以帮助反编译正在比较两个double的函数。

默认情况下,值类型不附带==实现。(用户定义的结构是否继承重载==运算符?)但是,所有内置标量类型都有一个显式重载运算符,编译器会将其翻译成适当的CIL。重载还包含一个简单的基于ceq的比较,因此==运算符重载的动态/后期绑定/基于反射的调用不会失败。


更多详细信息

对于预定义的值类型,如果其操作数的值相等,否则为false。供参考字符串以外的类型,如果其两个操作数引用相同的对象。对于字符串类型,==比较字符串。

--http://msdn.microsoft.com/en-us/library/53k8ybth.aspx

您所说的意味着==使用引用类型语义来比较double。但是,由于double是一种值类型,因此它使用值语义。这就是为什么3 == 3是真的,即使它们是不同的堆栈对象。

您几乎可以将这种编译器转换视为LINQ的Queryable对象如何包含带有代码的扩展方法,但编译器将这些调用转换为表达式树,并将其传递给LINQ提供程序。在这两种情况下,底层函数都不会被真正调用。


Double的比较语义

Double的文档确实暗示了ceq CIL指令的工作原理:

如果通过调用Equals方法测试两个Double.NaN值是否相等,则该方法将返回true。但是,如果使用相等运算符测试两个NaN值的相等性,则该运算符将返回false。当您想确定Double的值是否不是数字(NaN)时,另一种选择是调用IsNaN方法。

--http://msdn.microsoft.com/en-us/library/ya2zha7s.aspx


原始编译器源

如果你查看反编译的C#编译器源代码,你会发现以下代码可以处理将双重比较直接转换为ceq:

private void EmitBinaryCondOperator(BoundBinaryOperator binOp, bool sense)
{
    int num;
    ConstantValue constantValue;
    bool flag = sense;
    BinaryOperatorKind kind = binOp.OperatorKind.OperatorWithLogical();
    if (kind <= BinaryOperatorKind.GreaterThanOrEqual)
    {
        switch (kind)
        {
            ...
            case BinaryOperatorKind.Equal:
                goto Label_0127;
            ...
        }
    }
...
Label_0127:
    constantValue = binOp.Left.ConstantValue;
    if (((constantValue != null) && constantValue.IsPrimitiveZeroOrNull) && !constantValue.IsFloating)
    {
        ...
        return;
    }
    constantValue = binOp.Right.ConstantValue;
    if (((constantValue != null) && constantValue.IsPrimitiveZeroOrNull) && !constantValue.IsFloating)
    {
        ...
        return;
    }
    this.EmitBinaryCondOperatorHelper(ILOpCode.Ceq, binOp.Left, binOp.Right, sense);
    return;
}

上面的代码来自Roslyn.Compilers.CSharp.CodeGen.CodeGenerator.EmitBinaryCondOperator(...),我添加了"…",以便使代码更可读。

在msdn中指出;

如果通过调用Equals测试两个Double.NaN值是否相等方法,则该方法返回true。但是,如果测试两个NaN值对于使用相等运算符的相等,运算符返回false。当您想确定Double的值是否一个数字(NaN),另一种选择是调用IsNaN方法。

这是为了符合IEC 60559:1989,因为它规定两个NaN值不相等,因为它们不被视为数字,所以op_Equal的定义符合本标准;

根据IEC 60559:1989,两个值为NaN永远不平等。但是,根据System.Object::Equals方法,最好重写此方法以提供值相等语义。由于System.ValueType提供该功能通过使用反射Equals特别指出,值类型应该考虑重写默认ValueType实现以获得性能增长事实上,从源头上看System.ValueType::Equals(clr''src''BCL''System''ValueType.cs的第36行在SSCLI中),CLR Perf团队甚至对System.ValueType::Equals不快的影响。

参考:http://blogs.msdn.com/b/shawnfa/archive/2004/07/19/187792.aspx

来自msdn:http://msdn.microsoft.com/en-us/library/ya2zha7s.aspx

如果通过调用Equals测试两个Double.NaN值是否相等方法,则该方法返回true。但是,如果测试两个NaN值对于使用相等运算符的相等,运算符返回false。当您想确定Double的值是否一个数字(NaN),另一种选择是调用IsNaN方法。