Определение оператора "==" для Double

По какой-то причине я прокрался в источник .NET Framework для класса Double и выяснил, что объявление == является:

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

Та же логика применяется для каждого оператора.


  • В чем смысл такого определения?
  • Как это работает?
  • Почему он не создает бесконечную рекурсию?

Ответ 1

В действительности, компилятор превратит оператор == в код IL ceq, а упомянутый оператор не будет вызываться.

Причина для оператора в исходном коде, вероятно, может быть вызвана из языков, отличных от С#, которые не переводят его непосредственно в вызов 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 

Интересно, что одни и те же операторы НЕ существуют (как в исходном источнике, так и через отражение) для интегральных типов, только Single, Double, Decimal, String и DateTime, что опровергает мои что они существуют для вызова с других языков. Очевидно, вы можете приравнивать два целых числа на других языках без этих операторов, поэтому мы снова возвращаемся к вопросу "почему они существуют для Double"?

Ответ 2

Основная путаница здесь в том, что вы предполагаете, что все библиотеки .NET(в данном случае библиотека расширенных чисел, которая не является частью BCL) написаны в стандартном С#. Это не всегда так, и разные языки имеют разные правила.

В стандартном С# фрагмент кода, который вы видите, приведет к переполнению стека из-за того, как работает разрешение перегрузки оператора. Однако код на самом деле не является стандартным С# - он в основном использует недокументированные функции компилятора С#. Вместо вызова оператора он испускает этот код:

ldarg.0
ldarg.1
ceq
ret

Что это:) Нет 100% -ного эквивалентного кода С# - это просто невозможно в С# с вашим собственным типом.

Даже тогда фактический оператор не используется при компиляции кода С# - компилятор выполняет кучу оптимизаций, как в этом случае, где он заменяет вызов op_Equality только простым ceq. Опять же, вы не можете реплицировать это в своей собственной DoubleEx структуре - магии компилятора.

Это, безусловно, не уникальная ситуация в .NET - там много кода, который недействителен, стандартный С#. Причинами обычно являются (а) взлом компилятора и (б) другой язык, с нечетными (c) хаками исполнения (я смотрю на вас, Nullable!).

Поскольку компилятор Roslyn С# является источником OEPN, я могу на самом деле указать вам место, где будет разрешено разрешение перегрузки:

Место, где разрешены все бинарные операторы

"Ярлыки" для встроенных операторов

Когда вы смотрите на ярлыки, вы увидите, что равенство между двойными и двойными результатами выполняется в внутреннем двойном операторе, никогда в фактическом операторе ==, определенном в типе. Система типа .NET должна притворяться, что Double - это тип, подобный любому другому, но С# не - Double является примитивным в С#.

Ответ 3

Источник примитивных типов может ввести в заблуждение. Вы видели первую строку структуры Double?

Обычно вы не можете определить рекурсивную структуру следующим образом:

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

Примитивные типы также имеют встроенную поддержку в CIL. Обычно они не рассматриваются как объектно-ориентированные типы. Двойной - это просто 64-битное значение, если оно используется как float64 в CIL. Однако, если он обрабатывается как обычный тип .NET, он содержит фактическое значение и содержит методы, подобные любым другим типам.

Итак, вы видите здесь ту же ситуацию для операторов. Обычно, если вы используете тип двойного типа напрямую, он никогда не будет вызван. Кстати, его источник выглядит так в 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
}

Как вы можете видеть, бесконечного цикла нет (вместо вызова System.Double::op_Equality используется инструмент ceq). Поэтому, когда double рассматривается как объект, вызывается метод operator, который в конечном итоге будет обрабатывать его как примитивный тип float64 на уровне CIL.

Ответ 4

Я посмотрел CIL с помощью JustDecompile. Внутренний == переводится в операционный код CIL ceq. Другими словами, это примитивное равенство CLR.

Мне было интересно узнать, будет ли компилятор С# ссылаться на 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

Как указано в документации Microsoft для пространства имен System.Runtime.Versioning: типы, найденные в этом пространстве имен, предназначены для использования в .NET Framework, а не для пользовательских приложений. В пространстве имен System.Runtime.Versioning содержатся расширенные типы, которые поддержка версий в бок о бок реализации .NET Framework.