Определение оператора "==" для 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.