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

126

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

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

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


  • Какой смысл в таком определении?
  • Как это работает?
  • Почему он не создает бесконечную рекурсию?
Томас Аюб
источник
17
Я ожидал бесконечной рекурсии.
HimBromBeere 01
5
Я почти уверен, что он нигде не используется для сравнения с double, а ceqвыдается в IL. Это просто для заполнения некоторой документации, но не могу найти источник.
Хабиб
2
Скорее всего так, что этот оператор можно получить через Reflection.
Damien_The_Unbeliever 01
3
Это никогда не будет вызвано
Alex K.
1
@ZoharPeled, деление двойного на ноль допустимо и приведет к положительной или отрицательной бесконечности.
Магнус

Ответы:

62

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

Причина использования оператора в исходном коде, вероятно, заключается в том, что его можно вызвать из языков, отличных от C #, которые не переводят его в CEQвызов напрямую (или через отражение). Код внутри оператора будет скомпилирован в a 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»?

Д Стэнли
источник
12
Единственная проблема, которую я вижу в этом, заключается в том, что в спецификации языка C # сказано, что перегруженные операторы имеют приоритет над встроенными операторами. Поэтому, безусловно, соответствующий компилятор C # должен увидеть, что здесь доступен перегруженный оператор, и сгенерировать бесконечную рекурсию. Хм. Настораживает.
Damien_The_Unbeliever 01
5
Это не ответ на вопрос, имхо. Он только объясняет, на что транслируется код, но не объясняет почему. Согласно разделу 7.3.4 «Разрешение перегрузки бинарных операторов» в спецификации языка C #, я также ожидал бы бесконечной рекурсии. Я предполагаю, что справочный источник ( linksource.microsoft.com/#mscorlib/system/… ) здесь действительно не применим.
Дирк Фоллмар,
6
@DStanley - я не отрицаю то, что производится. Я говорю, что не могу согласовать это со спецификацией языка. Вот что беспокоит. Я думал о том, чтобы детально изучить Roslyn и посмотреть, смогу ли я найти здесь что-нибудь особенное, но в настоящее время я не очень хорошо настроен для этого (неправильная машина)
Damien_The_Unbeliever 01
1
@Damien_The_Unbeliever Вот почему я думаю, что это либо исключение из спецификации, либо другая интерпретация «встроенных» операторов.
D Stanley
1
Поскольку @Jon Skeet еще не ответил или не прокомментировал это, я подозреваю, что это ошибка (т.е. нарушение спецификации).
TheBlastOne
37

Основная путаница здесь заключается в том, что вы предполагаете, что все библиотеки .NET (в данном случае Библиотека расширенных числовых данных, которая не является частью 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 #.

Luaan
источник
1
Не уверен, что согласен с тем, что код в справочном источнике просто «реконструирован». В коде есть директивы компилятора #ifи другие артефакты, которых нет в скомпилированном коде. Плюс, если он был реконструирован, doubleто почему он не был реконструирован для intили long? Я действительно думаю, что для исходного кода есть причина, но считаю, что использование ==внутри оператора компилируется в, CEQчто предотвращает рекурсию. Поскольку оператор является «предопределенным» оператором для этого типа (и не может быть переопределен), правила перегрузки не применяются.
D Stanley
@DStanley Я не хотел подразумевать, что весь код реконструирован. И опять же, doubleэто не часть BCL - это отдельная библиотека, которая случайно включена в спецификацию C #. Да, ==компилируется в a ceq, но это по-прежнему означает, что это взлом компилятора, который вы не можете воспроизвести в собственном коде, и то, что не является частью спецификации C # (как и float64поле в Doubleструктуре). Это не контрактная часть C #, поэтому нет смысла рассматривать ее как действительный C #, даже если она была скомпилирована с помощью компилятора C #.
Luaan
@DStanely Я не смог найти, как организована настоящая структура, но в эталонной реализации .NET 2.0 все сложные части - это просто встроенные функции компилятора, реализованные на C ++. Конечно, есть еще много нативного кода .NET, но такие вещи, как «сравнение двух двойников», не очень хорошо работают в чистом .NET; это одна из причин, по которой числа с плавающей запятой не включены в BCL. Тем не менее, код также реализован на (нестандартном) C #, вероятно, именно по той причине, которую вы упомянули ранее - чтобы другие компиляторы .NET могли обрабатывать эти типы как настоящие типы .NET.
Luaan
@DStanley Но хорошо, точка взята. Я удалил ссылку на «реконструированный» и переформулировал ответ, чтобы явно упомянуть «стандартный C #», а не только C #. И не относитесь doubleк ним так же, как intи long- intи longявляются примитивными типами, которые должны поддерживать все языки .NET. float, decimalи doubleнет.
Luaan
12

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

То же самое и с операторами. Обычно, если вы используете тип типа 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). Поэтому, когда двойник обрабатывается как объект, будет вызван метод оператора, который в конечном итоге обработает его как float64примитивный тип на уровне CIL.

Дьёрдь Кёсег
источник
1
Для тех, кто не понимает первую часть этого поста (возможно, потому, что они обычно не пишут свои собственные типы значений), попробуйте код public struct MyNumber { internal MyNumber m_value; }. Конечно, его нельзя скомпилировать. Ошибка - это ошибка CS0523: член структуры MyNumber.m_value типа MyNumber вызывает цикл в макете структуры
Йеппе Стиг Нильсен,
8

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

Мне было любопытно узнать, будет ли компилятор C # ссылаться ceqили на ==оператор при сравнении двух значений типа double. В тривиальном примере, который я придумал (ниже), он использовал 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
Дэниел Пратт
источник
-2

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

Томас Папамихос
источник
При чем System.Runtime.Versioningтут System.Double?
Koopakiller