c # Оператор сложения заимствован из неявного преобразования в и из одних типов, но не других? - PullRequest
0 голосов
/ 02 ноября 2019

Давайте создадим класс и сделаем его неявно конвертируемым в int:

class Foo
{
    int val;
    public Foo() { }
    public Foo(int val) => this.val = val;
    public static implicit operator int(Foo f) => f.val;
    public static implicit operator Foo(int val) => new Foo(val);
}

Следующий код прекрасно компилируется:

Foo foo1 = new Foo();
Foo foo2 = new Foo();
Foo sum = foo1 + foo2;

Он просто преобразует каждое значение в int, выполняет сложение и преобразует обратно в Foo.

Теперь давайте попробуем использовать decimal вместо int:

class Foo
{
    decimal val;
    public Foo() { }
    public Foo(decimal val) => this.val = val;
    public static implicit operator decimal(Foo f) => f.val;
    public static implicit operator Foo(decimal val) => new Foo(val);
}

Снова код:

Foo sum = foo1 + foo2;

отлично работает.

Теперь давайте попробуем другую структуру с определенным оператором сложения, TimeSpan

class Foo
{
    TimeSpan val;
    public Foo() { }
    public Foo(TimeSpan val) => this.val = val;
    public static implicit operator TimeSpan(Foo f) => f.val;
    public static implicit operator Foo(TimeSpan val) => new Foo(val);
}

Теперь, когда мы попробуем

Foo sum = foo1 + foo2;

Мы получаем ошибку компилятора:

Ошибка CS0019 Оператор '+' нельзя применить к операндам типа 'Foo' и 'Foo'

Это кажется странным. И decimal, и TimeSpan имеют перегруженные + определенные операторы:

(из метаданных)

public static TimeSpan operator +(TimeSpan t1, TimeSpan t2);


public static Decimal operator +(Decimal d1, Decimal d2);

Почему это работает для некоторых типов, но не для других?

1 Ответ

1 голос
/ 02 ноября 2019

Используя это:

// int
Foo sum = foo1 + foo2;

А:

// decimal
Foo sum = foo1 + foo2;

А:

// TimeSpan
Foo sum = (TimeSpan)foo1 + (TimeSpan)foo2;

А:

// TimeSpan
var sum = (TimeSpan)foo1 + foo2;

Воткод IL для первого:

// ...
// Foo foo = (int)f + (int)f2;
IL_000d: ldloc.0
IL_000e: call int32 ConsoleApp.Foo::op_Implicit(class ConsoleApp.Foo)
IL_0013: ldloc.1
IL_0014: call int32 ConsoleApp.Foo::op_Implicit(class ConsoleApp.Foo)
IL_0019: add
IL_001a: call class ConsoleApp.Foo ConsoleApp.Foo::op_Implicit(int32)
IL_001f: stloc.2

для второго:

//...
// decimal num = (decimal)f + (decimal)f2;
IL_000d: ldloc.0
IL_000e: call valuetype [mscorlib]System.Decimal ConsoleApp.Foo::op_Implicit(class ConsoleApp.Foo)
IL_0013: ldloc.1
IL_0014: call valuetype [mscorlib]System.Decimal ConsoleApp.Foo::op_Implicit(class ConsoleApp.Foo)
IL_0019: call valuetype [mscorlib]System.Decimal [mscorlib]System.Decimal::op_Addition(valuetype [mscorlib]System.Decimal, valuetype [mscorlib]System.Decimal)
IL_001e: stloc.2
//...

и для третьего:

// ...
// TimeSpan timeSpan = (TimeSpan)f + (TimeSpan)f2;
IL_000d: ldloc.0
IL_000e: call valuetype [mscorlib]System.TimeSpan ConsoleApp.Foo::op_Implicit(class ConsoleApp.Foo)
IL_0013: ldloc.1
IL_0014: call valuetype [mscorlib]System.TimeSpan ConsoleApp.Foo::op_Implicit(class ConsoleApp.Foo)
IL_0019: call valuetype [mscorlib]System.TimeSpan [mscorlib]System.TimeSpan::op_Addition(valuetype [mscorlib]System.TimeSpan, valuetype [mscorlib]System.TimeSpan)
IL_001e: stloc.2
// ...

и для четвертого:

//...
// TimeSpan timeSpan = (TimeSpan)f + (TimeSpan)f2;
IL_000d: ldloc.0
IL_000e: call valuetype [mscorlib]System.TimeSpan ConsoleApp.Foo::op_Implicit(class ConsoleApp.Foo)
IL_0013: ldloc.1
IL_0014: call valuetype [mscorlib]System.TimeSpan ConsoleApp.Foo::op_Implicit(class ConsoleApp.Foo)
IL_0019: call valuetype [mscorlib]System.TimeSpan [mscorlib]System.TimeSpan::op_Addition(valuetype [mscorlib]System.TimeSpan, valuetype [mscorlib]System.TimeSpan)
IL_001e: stloc.2
//...

В первом случае код вызывает int32 op_Implicit, затем добавляются два целых числа со стандартной инструкцией add на сборке, которая добавляет последние два значения, помещенные в стек, и помещает в него результат.

В других случаях вызывается метод ::op_Addition, и он использует последние два значения, помещенные в стек, в качестве параметров для помещения результата в стек.

В третьем случае, если мы приведем первый операндКроме того, компилятор выводит тип для второго операнда, который приведен к типу ожидаемого результата.

Нет реальной разницы между использованием decimal или TimeSpan.

Таким образом, кажется, что без приведения одного из двух Foo к ожидаемому типу при использовании типа, отличного от чисел, компиляторне может вывести тип, используемый для добавления, поэтому он пытается добавить два Foo, не выводя ожидаемый.

Теперь, если мы посмотрим на интерфейсы классов Decimal и TimeSpan предоставленной таблицы методов. по метаданным мы видим, что Decimal реализует IConvertible, но не TimeSpan.

IConvertible имеет метод ToInt32 и ToDecimal.

Если мы попробуем Foo / DateTime, у нас возникнет та же проблема, что и с TimeSpan, но DateTime помечен как реализующий Iconvertible ... но определение класса не имеет методов для преобразования ... и они не реализованы, как мы можем прочитать в Документах Microsoft: "Это преобразование не поддерживается".

Возможно, это может объяснить, почему компилятор может определить тип для int и для decimal, чтобы выполнить сложение, но не для TimeSpan, а также DateTime. Возможно, для оператора по умолчанию + компилятор ищет только неявные операторы для типов, которые могут быть преобразованы. Вот только идея, которая может быть ложной или истинной, я не знаю.

Если мы добавим этот перегруженный оператор в Foo / TimeSpan:

public static Foo operator +(Foo value1, TimeSpan value2)
{
  return (TimeSpan)value1 + value2;
}

Это компилируетсяи он вызывает operator TimeSpan на foo2 рядом с operator +:

TimeSpan sum = foo1 + foo2;

Поскольку здесь компилятор видит, что существует перегруженный оператор +, и генерирует этот код IL:

// TimeSpan timeSpan = v + f;
IL_001d: ldloc.0
IL_001e: ldloc.1
IL_001f: call valuetype [mscorlib]System.TimeSpan ConsoleApp.Foo::op_Implicit(class ConsoleApp.Foo)
IL_0024: call class ConsoleApp.Foo ConsoleApp.Foo::op_Addition(class ConsoleApp.Foo, valuetype [mscorlib]System.TimeSpan)
IL_0029: call valuetype [mscorlib]System.TimeSpan ConsoleApp.Foo::op_Implicit(class ConsoleApp.Foo)
IL_002e: stloc.2
...