views:

905

answers:

2

Suppose we have following type:

struct MyNullable<T> where T : struct
{
    T Value;

    public bool HasValue;

    public MyNullable(T value)
    {
        this.Value = value;
        this.HasValue = true;
    }

    public static implicit operator T(MyNullable<T> value)
    {
        return value.HasValue ? value.Value : default(T);
    }
}

And try to compile following code snippet:

MyNullable<int> i1 = new MyNullable<int>(1);
MyNullable<int> i2 = new MyNullable<int>(2);

int i = i1 + i2;

This snipped compiled well and without errors. i1 and i2 casts to integer and addition evaluated.

But if we have following type:

struct Money
{
    double Amount;
    CurrencyCodes Currency; /*enum CurrencyCode { ... } */

    public Money(double amount, CurrencyCodes currency)
    {
        Amount = amount;
        Currency = currency;
    }

    public static Money operator + (Money x, Money y)
    {
        if (x.Currency != y.Currency)
            // Suppose we implemented method ConvertTo
            y = y.ConvertTo(x.Currency); 

        return new Money(x.Amount + y.Amount, x.Currency);
    }
}

Try to compile another code snippet:

MyNullable<Money> m1 = 
   new MyNullable<Money>(new Money(10, CurrenciesCode.USD));
MyNullable<Money> m2 = 
   new MyNullable<Money>(new Money(20, CurrenciesCode.USD));

Money m3 = m1 + m2;

And now the question, why compiler generate "error CS0019: Operator '+' cannot be applied to operands of type 'MyNullable<Money>' and 'MyNullable<Money>'"?

+8  A: 

That is an interesting question... it works with Decimal, for example, but not TimeSpan, which are both proper .NET types (unlike float etc that are primatives) and both have an + operator. Curious!

Of course, you can twist the arm with:

Money m3 = (Money)m1 + (Money)m2;

And it you just use Nullable<T> it'll work for free, of course - plus you get the compiler + runtime (boxing) support. Is there a reason not to use Nullable<T> here?

I'll look at the spec; in the interim, you might think about promoting the operator to the MyNullable<T>; with regular Nullable<T>, the C# compiler provides "lifted" operators for those supported by the type, but you can't do that yourself. The best you can do is offer all the obvious ones and hope the type supports it ;-p To access operators with generics, see here, available for free download here.

Note you'd probably want to apply the appropriate "lifted" checks - i.e.

x + y => (x.HasValue && y.HasValue)
          ? new MyNullable<T>(x.Value + y.Value)
          : new MyNullable<T>();

Update

The different handling looks to relate to 14.7.4 (ECMA 334 v4) "Addition operator", where it is pre-defined for a range of types including decimal (so that was a bad test by me), since by 14.2.4 (same) "Binary operator overload resolution", the pre-defined operators do get special mention. I don't claim to understand it fully, though.

Marc Gravell
Unfortunately, Nullable<Money> doesn't work either, it looks to me as the built in primitives get special treatment somehow, although I don't know why. I looked up user-defined implicit conversions in the C# spec but I'm afraid I got lost in the technical mumble there... Hope you find it,strange..
Lasse V. Karlsen
@lassevk - Nullable<Money> works fine - simply that the result of m1 + m2 is Nullable<Money>, not Money
Marc Gravell
The MyNullable is doing things the other way round. The value of a Nullable type is not available implicitly. The MyNullable is attempting to achieve that.
AnthonyWJones
i.e. "Money? m3 = m1 + m2;" - compiles and runs, with result 30 as expected
Marc Gravell
@AnthonyWJones - and for good reason - what should the implicit cast return if it doesn't have a value? The explicit cast (.Value) blows up (reasonable). Otherwise, there is GetValueOrDefault().
Marc Gravell
@AnthonyWJones - with the combination of lifted operators (compiler) and special boxing rules (runtime), and special generic rules (both) - I don't think you're ever going to be able to make MyNullable<T> *that* close to Nullable<T>. It really is a special, unique, case.
Marc Gravell
Hm, ok, I must've tripped up the code, or didn't read the error message perhaps.
Lasse V. Karlsen
@Marc: Yes that was kind of my point, Nullable doesn't have implicit operator T hence other magic is happening. Jon has good stab at in section 4.3.3 of his book.
AnthonyWJones
Marc you're absolutely right in case that MyNullable created only for implicit casting to T. Because same expression with System.Nullable looks like:Money? m1, m2;Money? m3 = m1.GetValueOrDefault() + m2.GetValueOrDefault();Remove call of GetValueOrDefault() is my special DSL trick.
Edy Gomolyako
+6  A: 

Marc is on the right lines - it's section 7.2.4 in the C# 3.0 spec - Binary Operator Overload Resolution.

Basically the steps are:

  • We need to resolve the implementation for "X + Y" where X and Y are both MyNullable<Money>.
  • Looking at section 7.2.5 (candidate user-defined operators) we end up with an empty set, as MyNullable<T> doesn't overload +.
  • Back in 7.2.4 the set of candidate operators is the built-in set of binary operators for +, i.e. int+int, decimal+decimal etc.
  • Overload resolution rules in 7.4.3 are then applied. When we're doing MyNullable<int> + MyNullable<int> this works because of the implicit conversions of each argument to int - but when we're doing MyNullable<Money> + MyNullable<Money> it doesn't work because Money + Money isn't in the set of candidate operators.
Jon Skeet