Let's start with a very simple piece of code:
decimal d = 2;
Console.WriteLine("d == 2 = {0}", d == 2);
Console.WriteLine("d == (decimal)2 = {0}", d == (decimal)2);
Console.WriteLine("d.Equals(2) = {0}", d.Equals(2));
Console.WriteLine("d.Equals((decimal)2) = {0}", d.Equals((decimal)2));
The result is 4xtrue. Now, let's change a type of a variable d to decimal?:
decimal? d = 2;
This time the result will be True, True, False, True. The explanation of this situation is quite easy. Equals method is implemented as follows for Nullable<T> type:
public override bool Equals(object other)
{
if (!this.HasValue)
{
return (other == null);
}
if (other == null)
{
return false;
}
return this.value.Equals(other);
}
If this has a value and other parameter is not null then Decimal.Equals(object value) will be called. Decimal.Equals(object value) method works in this way, that if value parameter is not decimal then the result will be always false.
It seems to me that the current implementation is not intuitive and I wonder why Nullable<T> doesn't provide developers with generic version of Equals method e.g.:
public bool Equals(T other)
{
if (!this.HasValue)
return false;
return this.value.Equals(other);
}
Was it done on purpose or is it an omission?
Comment 1:
A brief comment to be clear. I suggested that Nullable<T> should have two Equals methods i.e.: public override bool Equals(object other) and public bool Equals(T other)
As you know, a value type cannot be assigned a null value. For example, int i = null will give you a compile time error. C# 2.0 introduced nullable types that allow you to assign null to value type variables. You can declare nullable types using Nullable<t> where T is a type.
Warning: Possible NULL Reference Return This error is because the reference type was nullable by default, but because enabling nullable reference types, the type is non-nullable now. In order to fix this warning, the return type should be marked as nullable.
For example, in nullable of integer type you can store values from -2147483648 to 2147483647, or null value.
Instead of writing (decimal)2
you can write 2m
(will use that in the following).
When you use the ==
operator, no boxing occurs. The C# compiler will choose the pre-defined overload (i.e. the overload defined in the C# Language Specification; this is not necessarily a real .NET method) of operator ==
which matches best.
There are overloads:
operator ==(int x, int y);
operator ==(decimal x, decimal y);
There are no "mixed" overloads like operator ==(decimal x, int y);
. Because an implicit conversion exists from int
to decimal
, your literal 2
is converted to 2m
implicitly when you use ==
.
With Equals
, in some situations boxing occurs. You don't need nullables for that. To give examples, all of these calls:
object.Equals(2, 2m);
object.Equals(2m, 2);
((object)2).Equals(2m);
((object)2m).Equals(2);
(2).Equals((object)2m);
(2m).Equals((object)2);
(2).Equals(2m);
return false
! "Two" of type Int32
is not equal to "two" of type Decimal
.
Only when method overloading leads to a conversion between int
and decimal
will the result be true
. For example:
(2m).Equals(2); // true
So while an extra overload of Equals
could be added to Nullable<>
, the behavior you describe is not really related to Nullable<>
.
Although I like these questions, they can only really be answered by those on the design team responsible for the type. An obvious workaround is to access the Value
that is T
and use the Equals
of that.
My best guess is that it would probably force all T
to be IEquatable<T>
, in order to generically access Equals<T>
on a given type. This would work for the core value types, but other .NET structs wouldn't necessarily implement that interface and enum
types don't.
I suppose it could be done via type checking / casting, but this then comes a lot of legwork versus the caller simply doing: myNullable.GetValueOrDefault().Equals()
.
You can make an extension method to do this task, to get it to call the method you need to specify the generic argument explicitly (otherwise the compiler calls Equals(object)
:
class Program
{
static void Main(string[] args)
{
double? d = null;
Console.WriteLine(d.Equals<double>(0.0));
d = 0.0;
Console.WriteLine(d.Equals<double>(0.0));
Console.Read();
}
}
public static class NullableExtensions
{
public static bool Equals<T>(this T? left, T right) where T : struct, IEquatable<T>
{
if (!left.HasValue)
return false;
return right.Equals(left.Value);
}
}
I've asked a question about why this call is needed.
Turns out the reason for having to force it down into the extension method is due to the compiler implementation only using extension methods if no suitable method exists, in this case Equals(object)
is considered more suitable than Equals<T>(T)
as the latter is an extension method. It's in the spec.
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