I've created a custom struct to represent an amount. It is basically a wrapper around decimal
. It has an implicit conversion operator to cast it back to decimal
.
In my unit test, I assert that the Amount equals the original decimal value, but the test fails.
[TestMethod]
public void AmountAndDecimal_AreEqual()
{
Amount amount = 1.5M;
Assert.AreEqual(1.5M, amount);
}
When I use an int though (for which I did not create a conversion operator), the test does succeed.
[TestMethod]
public void AmountAndInt_AreEqual()
{
Amount amount = 1;
Assert.AreEqual(1, amount);
}
When I hover the AreEqual
, it shows that the first one resolves to
public static void AreEqual(object expected, object actual);
and the second one leads to
public static void AreEqual<T>(T expected, T actual);
It looks like the int
value 1
is implicitly cast to an Amount
, while the decimal
value 1.5M
is not.
I don't understand why this happens. I would've expected just the opposite. The first unit test should be able to cast the decimal
to an Amount
.
When I add an implicit cast to int
(which wouldn't make sense), the second unit test also fails. So adding an implicit cast operator breaks the unit test.
I have two questions:
Amount
struct so both tests will succeed?(I know I could change the test to do an explicit conversion, but if I don't absolutely have to, I won't)
My Amount struct (just a minimal implementation to show the problem)
public struct Amount
{
private readonly decimal _value;
private Amount(decimal value)
{
_value = value;
}
public static implicit operator Amount(decimal value)
{
return new Amount(value);
}
public static implicit operator decimal(Amount amount)
{
return amount._value;
}
}
Bad things happen when you can convert implicit
ly in both directions, and this is one example.
Because of the implicit conversions the compiler is able to pick Assert.AreEqual<decimal>(1.5M, amount);
and Assert.AreEqual<Amount>(1.5M, amount);
with equal value.*
Since they're equal, neither overload will be picked by inference.
Since there's no overload to pick by inference, neither make it into the list for picking the best-match and only the (object, object)
form is available. So it's the one picked.
With Assert.AreEqual(1, amount)
then since there is an implicit conversion from int
to Amount
(via implicit int->decimal) but no implicit conversion from Amount
to int
the compiler thinks "obviously they mean the Assert.AreEqual<Amount>()
here"†, and so it is picked.
You can explicitly pick an overload with Assert.AreEqual<Amount>()
or Assert.AreEqual<decimal>()
but you're probably better off making one of your conversions the "narrowing" from that has to be explicit
if at all possible because this feature of your struct will hurt you again. (Hurrah for unit tests finding flaws).
*Another valid overload choice is to pick Assert.AreEqual<object>
, but it's never picked by inference because:
object
anyway.As such it can only ever be called by including the <object>
in the code.
†The compiler treats everything said to it as either obvious in meaning or completely incomprehensible. There are people like that too.
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