13

I have this code I run in linqpad:

    long x = long.MaxValue;
    decimal y = x;

    x.Dump();
    y.Dump();

    (x == y).Dump();
    (y == x).Dump();

    Object.Equals(x, y).Dump();
    Object.Equals(y, x).Dump();
    x.Equals(y).Dump();
    y.Equals(x).Dump();

It produces this output:

    9223372036854775807
    9223372036854775807
    True
    True
    False
    False
    False
    True

Note the last two lines: x.Equals(y) is false but y.Equals(x) is true. So the decimal considers itself equal to a long with the same value but the long doesn't consider itself equal to the decimal that has the same value.

What's the explanation for this behavior?

Update:

I accepted Lee's answer.

I was very curious about this and wrote this little program:

using System;
namespace TestConversion
{
  class Program
  {
    static void Main(string[] args)
    {
      long x = long.MaxValue;
      decimal y = x;

      Console.WriteLine(x);
      Console.WriteLine(y);

      Console.WriteLine(x == y);
      Console.WriteLine(y == x);

      Console.WriteLine(Object.Equals(x, y));
      Console.WriteLine(Object.Equals(y, x));
      Console.WriteLine(x.Equals(y));
      Console.WriteLine(y.Equals(x));
      Console.ReadKey();
    }
  }
}

Which I then disassembled in IL:

.method private hidebysig static void Main(string[] args) cil managed
{
  .entrypoint
  .maxstack 2
  .locals init (
    [0] int64 x,
    [1] valuetype [mscorlib]System.Decimal y)
  L_0000: nop 
  L_0001: ldc.i8 9223372036854775807
  L_000a: stloc.0 
  L_000b: ldloc.0 
  L_000c: call valuetype [mscorlib]System.Decimal [mscorlib]System.Decimal::op_Implicit(int64)
  L_0011: stloc.1 
  L_0012: ldloc.0 
  L_0013: call void [mscorlib]System.Console::WriteLine(int64)
  L_0018: nop 
  L_0019: ldloc.1 
  L_001a: call void [mscorlib]System.Console::WriteLine(valuetype [mscorlib]System.Decimal)
  L_001f: nop 
  L_0020: ldloc.0 
  L_0021: call valuetype [mscorlib]System.Decimal [mscorlib]System.Decimal::op_Implicit(int64)
  L_0026: ldloc.1 
  L_0027: call bool [mscorlib]System.Decimal::op_Equality(valuetype [mscorlib]System.Decimal, valuetype [mscorlib]System.Decimal)
  L_002c: call void [mscorlib]System.Console::WriteLine(bool)
  L_0031: nop 
  L_0032: ldloc.1 
  L_0033: ldloc.0 
  L_0034: call valuetype [mscorlib]System.Decimal [mscorlib]System.Decimal::op_Implicit(int64)
  L_0039: call bool [mscorlib]System.Decimal::op_Equality(valuetype [mscorlib]System.Decimal, valuetype [mscorlib]System.Decimal)
  L_003e: call void [mscorlib]System.Console::WriteLine(bool)
  L_0043: nop 
  L_0044: ldloc.0 
  L_0045: box int64
  L_004a: ldloc.1 
  L_004b: box [mscorlib]System.Decimal
  L_0050: call bool [mscorlib]System.Object::Equals(object, object)
  L_0055: call void [mscorlib]System.Console::WriteLine(bool)
  L_005a: nop 
  L_005b: ldloc.1 
  L_005c: box [mscorlib]System.Decimal
  L_0061: ldloc.0 
  L_0062: box int64
  L_0067: call bool [mscorlib]System.Object::Equals(object, object)
  L_006c: call void [mscorlib]System.Console::WriteLine(bool)
  L_0071: nop 
  L_0072: ldloca.s x
  L_0074: ldloc.1 
  L_0075: box [mscorlib]System.Decimal
  L_007a: call instance bool [mscorlib]System.Int64::Equals(object)
  L_007f: call void [mscorlib]System.Console::WriteLine(bool)
  L_0084: nop 
  L_0085: ldloca.s y
  L_0087: ldloc.0 
  L_0088: call valuetype [mscorlib]System.Decimal [mscorlib]System.Decimal::op_Implicit(int64)
  L_008d: call instance bool [mscorlib]System.Decimal::Equals(valuetype [mscorlib]System.Decimal)
  L_0092: call void [mscorlib]System.Console::WriteLine(bool)
  L_0097: nop 
  L_0098: call valuetype [mscorlib]System.ConsoleKeyInfo [mscorlib]System.Console::ReadKey()
  L_009d: pop 
  L_009e: ret 
}

You can see indeed that the long value is converted to decimal.

Thank you guys!

jpmc26
  • 28,463
  • 14
  • 94
  • 146
boggy
  • 3,674
  • 3
  • 33
  • 56
  • fyi, casting the the decimal back to a long works: `x.Equals((long)y).Dump();` – DLeh Jan 23 '15 at 20:57
  • While this doesn't exactly explain this behavior, you might want to take a look at this: http://stackoverflow.com/questions/485175/is-it-safe-to-check-floating-point-values-for-equality-to-0-in-c-net/485210#485210 – seN Jan 23 '15 at 20:58
  • @leppie: In the real program we don't compare them. We discovered this by doing some debugging and I found it strange - hence my post. – boggy Jan 23 '15 at 21:00
  • @costa consider updating title to explicitly talk about long/decimal comparison... – Alexei Levenkov Jan 23 '15 at 21:09
  • @costa are you still asking if Equals should be communicative? If so, yes Equals should be both communicative and transitive. But this is only a guideline. I had reasons to break this guideline myself previously. But in this case regarding long and decimal comparison, it sure sounds like MS messed up. Equals is supposed to check for value equality. Also see https://msdn.microsoft.com/en-US/library/ms173147(v=vs.80).aspx where it clearly states implementation of Equals should guarantee x.Equals(y) and y.Equals(x) return the same value. – user2880486 Jan 23 '15 at 21:51
  • @user2880486: Thanks for the link. I as well think MS messed up on this one. – boggy Jan 23 '15 at 22:12
  • @AlexeiLevenkov: Not sure, I should change it. My question was more about the fact that MS violates this principle where if x.Equals(y) is true then y.Equals(x) should be true as well (see the link posted by @user2880486). – boggy Jan 23 '15 at 22:14
  • @costa note that the link explicitly talks about `virtual bool Equals(object)` and not other variants... While confusing a bit I don't see decimal's `Equal(object)` to be non-symmetrical - it always fails to compare to different type. – Alexei Levenkov Jan 23 '15 at 23:01
  • @AlexeiLevenkov: You are right. y.Equals((object) x) returns false which is consistent with the principle. I guess the whole thing is confusing because the compiler resolvs the call to Equals(decimal) as opposed to Equals(object). And Equals(decimal) doesn't exactly work as the other form. – boggy Jan 23 '15 at 23:06

3 Answers3

18

This happens because in

y.Equals(x);

the decimal.Equals(decimal) overload is being called since there is an implicit conversion between long and decimal. As a result the comparison returns true.

However, since there is no implicit conversion from decimal to long

x.Equals(y)

calls long.Equals(object) which causes y to be boxed and the comparison returns false since it cannot be unboxed to a long.

Lee
  • 142,018
  • 20
  • 234
  • 287
  • This answer plus link from other answer - http://stackoverflow.com/a/28118709/477420 gives complete explanation. – Alexei Levenkov Jan 23 '15 at 21:05
  • This answer is weird, but still somehow feels correct... Are you saying while decimal knows about long, but long does not know about decimal (in terms of conversion)? – leppie Jan 23 '15 at 21:09
  • No, he says `decimal` cannot be converted into `long` without risking precision loss, and therefore there is no implicit conversion. You can convert `long` to `decimal` without data loss and that's why there is conversion in that direction. – MarcinJuraszek Jan 23 '15 at 21:11
  • @MarcinJuraszek: Please explain why it fails to convert. For example, given your reasoning, even if the value is `1`, it would fail? – leppie Jan 23 '15 at 21:12
  • No, but because there is a case that it would fail that conversion has to be made explicit - compiler will not do implicit conversions there. It can fail, so you have to take responsibility for that, and forcing you to use explicit cast is just that - you have to agree to do the conversion and agree that in some cases it may fail and/or result in data loss. – MarcinJuraszek Jan 23 '15 at 21:14
  • @MarcinJuraszek: So you are saying `long.MaxValue` cannot be represented with a `decimal` type? – leppie Jan 23 '15 at 21:16
  • 2
    You're looking at wrong conversion. `1.1` cannot be represented in `long`. So conversion from `decimal` to `long` has to be explicit, because it will result in precision loss. Conversion from `long` to `decimal` is implicit. – MarcinJuraszek Jan 23 '15 at 21:17
  • @MarcinJuraszek: No, this is just silly... explain why `9223372036854775807.Equals((ulong)9223372036854775807)` returns `false` – leppie Jan 23 '15 at 21:26
  • 1
    The same reason. Because there is no implicit conversion from `ulong` to `long`. And why is that? Because there are values that can be represented in `ulong` but can't in `long`. – MarcinJuraszek Jan 23 '15 at 21:29
  • @MarcinJuraszek: That is saying the bit pattern is different, which it is not – leppie Jan 23 '15 at 21:31
  • 2
    @leppie - The bit patterns aren't being compared. The `Equals(object)` overload is chosen since there's no implicit conversion from `ulong` to `long`. The argument is boxed and the first thing the `long.Equals(object)` method must do is see if the argument is a boxed long to do the comparison. This fails since the argument is a boxed `ulong`. – Lee Jan 23 '15 at 21:34
  • @MarcinJuraszek: I see... `1.Equals((uint)1)` also returns `false`. At least it is consistent, but totally dubious IMO... Is an overflow check really that hard? – leppie Jan 23 '15 at 21:38
  • @leppie at what point you want to do overflow check? Compiler clearly can't do that as it does not know the value. So it has to match more generic `Equals(object)`. At that point `long` (or whatever type of the left argument) has to at run-time decide if value of `object` *feels* like good one to compare to - while possible it will be much slower than "same type" check and may mean a lot of types to check against... – Alexei Levenkov Jan 23 '15 at 22:52
  • @leppie also note that `Equals` is just yet another method to compiler - while it comes from root type it could be special cased, but I'd expect one would need very good reason and very well thought out behavior for special casing... – Alexei Levenkov Jan 23 '15 at 22:56
6

Implicit vs Explicit conversions.

From MSDN:

Implicit conversions: No special syntax is required because the conversion is type safe and no data will be lost. Examples include conversions from smaller to larger integral types, and conversions from derived classes to base classes.

Explicit conversions (casts): Explicit conversions require a cast operator. Casting is required when information might be lost in the conversion, or when the conversion might not succeed for other reasons. Typical examples include numeric conversion to a type that has less precision or a smaller range, and conversion of a base-class instance to a derived class.

A long will easily convert to a decimal, but the reverse is not true, so the evaluation fails.

crthompson
  • 15,653
  • 6
  • 58
  • 80
-2

You are comparing Object references and values. The references of course are not the same - except for the reference you explicitly set on line 2. The values however are.

C# automatically takes care of pointer management (e.g. "Referencing memory") for you. You are accessing that layer which isn't initially obvious from the syntax. This is the nature of C#.

Nick M
  • 429
  • 4
  • 10