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

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

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

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


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

5 ответов

Решение

На самом деле, компилятор превратит == оператор в 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 

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

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

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

ldarg.0
ldarg.1
ceq
ret

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

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

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

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

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

"Горячие клавиши" для внутренних операторов

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

Источник примитивных типов может сбивать с толку. Вы видели самую первую строку 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
}

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

Я посмотрел на CIL с помощью JustDecompile. Внутренний == переводится в CIL CEC оп код. Другими словами, это примитивное равенство 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

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

Другие вопросы по тегам