For some reason I was sneaking into the .NET Framework source for the class Double
and found out that the declaration of ==
is:
public static bool operator ==(Double left, Double right) {
return left == right;
}
The same logic applies for every operator.
In reality, the compiler will turn the ==
operator into a ceq
IL code, and the operator you mention will not be called.
The reason for the operator in the source code is likely so it can be called from languages other than C# that do not translate it into a CEQ
call directly (or through reflection). The code within the operator will be compiled to a CEQ
, so there is no infinite recursion.
In fact, if you call the operator via reflection, you can see that the operator is called (rather than a CEQ
instruction), and obviously is not infinitely recursive (since the program terminates as expected):
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}));
Resulting IL (compiled by 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
Interestingly - the same operators do NOT exist (either in the reference source or via reflection) for integral types, only Single
, Double
, Decimal
, String
, and DateTime
, which disproves my theory that they exist to be called from other languages. Obviously you can equate two integers in other languages without these operators, so we're back to the question "why do they exist for double
"?
The main confusion here is that you're assuming that all .NET libraries (in this case, the Extended Numerics Library, which is not a part of the BCL) are written in standard C#. This isn't always the case, and different languages have different rules.
In standard C#, the piece of code you're seeing would result in a stack overflow, due to the way operator overload resolution works. However, the code isn't actually in standard C# - it basically uses undocumented features of the C# compiler. Instead of calling the operator, it emits this code:
ldarg.0
ldarg.1
ceq
ret
That's it :) There is no 100% equivalent C# code - this simply isn't possible in C# with your own type.
Even then, the actual operator isn't used when compiling C# code - the compiler does a bunch of optimizations, like in this case, where it replaces the op_Equality
call with just the simple ceq
. Again, you can't replicate this in your own DoubleEx
struct - it's compiler magic.
This certainly isn't a unique situation in .NET - there's plenty of code that isn't valid, standard C#. The reasons are usually (a) compiler hacks and (b) a different language, with the odd (c) runtime hacks (I'm looking at you, Nullable
!).
Since the Roslyn C# compiler is oepn source, I can actually point you at the place where overload resolution is decided:
The place where all binary operators are resolved
The "shortcuts" for intrinsic operators
When you look at the shortcuts, you'll see that equality between double and double results in the intrinsic double operator, never in the actual ==
operator defined on the type. The .NET type system has to pretend that Double
is a type like any other, but C# doesn't - double
is a primitive in C#.
The source of the primitive types can be confusing. Have you seen the very first line of the Double
struct?
Normally you cannot define a recursive struct like this:
public struct Double : IComparable, IFormattable, IConvertible
, IComparable<Double>, IEquatable<Double>
{
internal double m_value; // Self-recursion with endless loop?
// ...
}
Primitive types have their native support in CIL as well. Normally they are not treated like object-oriented types. A double is just a 64-bit value if it is used as float64
in CIL. However, if it is handled as a usual .NET type, it contains an actual value and it contains methods like any other types.
So what you see here is the same situation for operators. Normally if you use the double type type directly, it will never be called. BTW, its source looks like this in 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
}
As you can see, there is no endless loop (the ceq
instrument is used instead of calling the System.Double::op_Equality
). So when a double is treated like an object, the operator method will be called, which will eventually handle it as the float64
primitive type on the CIL level.
I took a look at the CIL with JustDecompile. The inner ==
gets translated to the CIL ceq op code. In other words, it's primitive CLR equality.
I was curious to see if the C# compiler would reference ceq
or the ==
operator when comparing two double values. In the trivial example I came up with (below), it used ceq
.
This program:
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");
}
generates the following CIL (note the statement with label 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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With