Визначення оператора "==" для Double


126

Чомусь я пробрався до джерела .NET Framework для класу Doubleі з'ясував, що декларація ==:

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

Така ж логіка стосується кожного оператора.


  • У чому сенс такого визначення?
  • Як це працює?
  • Чому це не створює нескінченну рекурсію?

17
Я очікую нескінченних рекурсій.
HimBromBeere

5
Я впевнений, що він не використовується для порівняння ніде з подвійним, натомість ceqвидається в IL. Це просто там, щоб заповнити деяку мету документації, але не можу знайти джерело.
Хабіб

2
Швидше за все, щоб цього оператора можна було отримати через Reflection.
Damien_The_Unbeliever

3
Це ніколи не буде викликано, компілятор має зафіксовану логіку рівності (ceq opcode), див. Коли викликається оператор Double = ==?
Алекс К.

1
@ZoharPeled ділення подвійного з нулем є дійсним і призведе до позитивної чи негативної нескінченності.
Магнус

Відповіді:


62

Насправді компілятор перетворить ==оператор в ceqIL-код, і оператор, якого ви згадуєте, не буде викликаний.

Причина оператора у вихідному коді, ймовірно, тому його можна викликати з інших мов, крім 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"?


12
Єдина проблема, яку я можу побачити з цим, полягає в тому, що специфікація мови C # говорить про те, що перевантажені оператори мають перевагу над вбудованими операторами. Тож, безумовно, відповідний компілятор C # повинен бачити, що тут перевантажений оператор доступний і генерує нескінченну рекурсію. Хм. Смутні.
Damien_The_Unbeliever

5
Це не відповідає на питання, imho. Це лише пояснює, до чого перекладається код, але не чому. Згідно з розділом 7.3.4. Розв’язання бінарного оператора щодо перевантаження в специфікації мови C # я також очікував би нескінченної рекурсії. Я припускаю, що посилання на джерело ( referenceource.microsoft.com/#mscorlib/system/… ) насправді тут не застосовується.
Дірк Волмар

6
@DStanley - Я не заперечую те, що виробляється. Я кажу, що не можу погодитись із мовою. Ось що хвилює. Я думав про переїзд через Рослін і бачив, чи зможу тут знайти якусь спеціальну обробку, але зараз я не готовий це робити (неправильна машина)
Damien_The_Unbeliever

1
@Damien_The_Unbeliever Ось чому я думаю, що це або виняток із специфікації, або інша інтерпретація "вбудованих" операторів.
D Стенлі

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 # є початковим джерелом, я фактично можу вказати вам на місце вирішення роздільної здатності перевантаження:

Місце, де всі бінарні оператори вирішені

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

Якщо ви подивитесь на ярлики, ви побачите, що рівність між подвійними та подвійними результатами в внутрішньому подвійному операторі, ніколи в реальному ==операторі, визначеному для типу. Система типу .NET повинна робити вигляд, що Doubleтакий тип, як і будь-який інший, але C # ні - doubleце примітив у C #.


1
Не впевнений, я згоден, що код у довідковому джерелі просто "інженерно". Код має директиви (компілятори #if) та інші артефакти, які не були б у складеному коді. Плюс, якщо він був спроектований реверсом, doubleто чому він не був спроектований intабо long? Я думаю, що є причина для вихідного коду, але я вважаю, що використання ==оператора всередині збирається таким чином, CEQщо запобігає рекурсії. Оскільки оператор є "заздалегідь визначеним" оператором для цього типу (і його неможливо переотримати), правила перевантаження не застосовуються.
D Стенлі

@DStanley Я не хотів натякувати на те, що весь код сконструйований у зворотному напрямку. І знову ж таки, doubleне є частиною BCL - вона знаходиться в окремій бібліотеці, яка просто трапляється включеною в специфікацію C #. Так, ==збирання збирається в a ceq, але це все ще означає, що це хакер компілятора, який ви не можете реплікувати у власному коді, і те, що не входить у специфікацію C # (як і float64поле в Doubleструктурі). Це не договірна частина C #, тому мало сенсу трактувати її як дійсну C #, навіть якщо вона складена разом із компілятором C #.
Луань

@DStanely Я не міг знайти, як організований реальний фреймворк, але в еталонній реалізації .NET 2.0 всі складні частини - це лише внутрішні компоненти компілятора, реалізовані в C ++. Звичайно, існує багато рідного коду .NET, але такі речі, як "порівняння двох пар", не дуже добре працюватимуть у чистому .NET; це одна з причин, що числа з плаваючою комою не включаються до BCL. Однак, код також реалізований у (нестандартному) C #, ймовірно, саме з тієї причини, про яку ви згадали раніше, - щоб переконатися, що інші компілятори .NET можуть розглядати ці типи як реальні типи .NET.
Луань

@DStanley Але добре, точка. Я видалив посилання "інженерно-технічного спрямування" і переформулював відповідь, щоб явно згадати "стандартний C #", а не просто C #. І не поводьтесь doubleтак само, як intі long-, intа longце примітивні типи, які повинні підтримувати всі мови .NET. float, decimalі doubleні.
Луань

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, він містить фактичне значення і містить методи, як і будь-які інші типи.

Тож те, що ви бачите тут, така ж ситуація для операторів. Зазвичай, якщо ви використовуєте тип подвійного типу безпосередньо, він ніколи не буде викликаний. До речі, його джерело виглядає так у 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" викликає цикл у структурі структури
Jeppe Stig Nielsen

8

Я подивився CIL з JustDecompile. Внутрішній ==перекладається на код CQ ceq op. Іншими словами, це примітивна рівність 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

-2

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


Що System.Runtime.Versioningстосується System.Double?
Koopakiller
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.