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