Почему компилятор С# не вызывает оператор неявного приведения?

Предположим, у нас есть следующий тип:

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);
    }
}

И попробуйте скомпилировать следующий фрагмент кода:

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

int i = i1 + i2;

Этот фрагмент скомпилирован хорошо и без ошибок. i1 и i2 преобразуются в целое число и оценивается сложение.

Но если у нас есть следующий тип:

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);
    }
}

Попробуйте скомпилировать другой фрагмент кода:

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;

А теперь вопрос, почему компилятор выдает "ошибка CS0019: Оператор '+' не может применяться к операндам типа 'MyNullable‹Money›' и 'MyNullable‹Money›'"?


person Ed Gomoliako    schedule 26.12.2008    source источник


Ответы (2)


Марк находится на правильном пути - это раздел 7.2.4 в спецификации С# 3.0 - Разрешение перегрузки бинарного оператора.

В основном шаги таковы:

  • Нам нужно разрешить реализацию для «X + Y», где X и Y оба равны MyNullable<Money>.
  • Глядя на раздел 7.2.5 (кандидаты на пользовательские операторы), мы получаем пустой набор, так как MyNullable<T> не перегружает +.
  • Еще в 7.2.4 набор операторов-кандидатов — это встроенный набор бинарных операторов для +, то есть int+int, decimal+decimal и т. д.
  • Затем применяются правила разрешения перегрузки в 7.4.3. Когда мы делаем MyNullable<int> + MyNullable<int>, это работает из-за неявного преобразования каждого аргумента в int, но когда мы делаем MyNullable<Money> + MyNullable<Money>, это не работает, потому что Money + Money не входит в набор операторов-кандидатов.
person Jon Skeet    schedule 27.12.2008

Это интересный вопрос... он работает, например, с Decimal, но не с TimeSpan, которые являются правильными типами .NET (в отличие от float и т. д., которые являются примитивами), и оба имеют оператор +. Любопытный!

Конечно, вы можете крутить руку с помощью:

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

И если вы просто используете Nullable<T>, это, конечно, будет работать бесплатно - плюс вы получаете поддержку компилятора + время выполнения (бокс). Есть ли причина не использовать здесь Nullable<T>?

Я посмотрю на спецификацию; тем временем вы можете подумать о повышении оператора до MyNullable<T>; с обычным Nullable<T> компилятор C# предоставляет «поднятые» операторы для тех, которые поддерживаются типом, но вы не можете сделать это самостоятельно. Лучшее, что вы можете сделать, это предложить все очевидные из них и надеяться, что тип их поддерживает ;-p Чтобы получить доступ к операторам с дженериками, см. /genericoperators.html" rel="nofollow noreferrer">здесь, доступно для бесплатного скачивания здесь.

Обратите внимание, что вы, вероятно, захотите применить соответствующие «поднятые» проверки, т.е.

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

Обновить

Другая обработка, похоже, связана с 14.7.4 (ECMA 334 v4) «Оператор сложения», где он предопределен для диапазона типов, включая десятичный (так что это был плохой тест для меня) , так как в 14.2.4 (там же) «Разрешение перегрузки бинарных операторов» предопределенные операторы получают особое упоминание. Хотя я не претендую на полное понимание.

person Marc Gravell    schedule 26.12.2008
comment
К сожалению, Nullable‹Money› тоже не работает, мне кажется, что встроенные примитивы каким-то образом обрабатываются особым образом, хотя я не знаю почему. Я искал определяемые пользователем неявные преобразования в спецификации С#, но боюсь, что потерялся там в техническом бормотании... Надеюсь, вы найдете это, странно... - person Lasse V. Karlsen; 26.12.2008
comment
@lassevk - Nullable‹Money› работает нормально - просто результат m1 + m2 равен Nullable‹Money›, а не Money - person Marc Gravell; 26.12.2008
comment
MyNullable делает все наоборот. Значение типа Nullable недоступно неявно. MyNullable пытается добиться этого. - person AnthonyWJones; 26.12.2008
comment
то есть деньги? м3 = м1 + м2; - компилируется и запускается с ожидаемым результатом 30 - person Marc Gravell; 26.12.2008
comment
@AnthonyWJones - и не зря - что должно возвращать неявное приведение, если оно не имеет значения? Явное приведение (.Value) взрывается (разумно). В противном случае есть GetValueOrDefault(). - person Marc Gravell; 26.12.2008
comment
@AnthonyWJones - с комбинацией поднятых операторов (компилятор) и специальных правил упаковки (среда выполнения) и специальных общих правил (оба) - я не думаю, что вы когда-либо сможете сделать MyNullable‹T› что близко к Nullable‹T›. Это действительно особый, уникальный случай. - person Marc Gravell; 26.12.2008
comment
Хм, хорошо, я, должно быть, споткнулся в коде или, возможно, не прочитал сообщение об ошибке. - person Lasse V. Karlsen; 26.12.2008
comment
@Marc: Да, это было моей точкой зрения, Nullable не имеет неявного оператора T, поэтому происходит другая магия. У Джона есть хороший удар в разделе 4.3.3 его книги. - person AnthonyWJones; 26.12.2008
comment
Марк, вы абсолютно правы в том случае, если MyNullable создан только для неявного приведения к T. Потому что то же выражение с System.Nullable выглядит так: Money? м1, м2; Деньги? m3 = m1.GetValueOrDefault() + m2.GetValueOrDefault(); Удалить вызов GetValueOrDefault() — мой специальный трюк с DSL. - person Ed Gomoliako; 26.12.2008