Arhn - архитектура программирования

c# Оператор сложения, заимствованный из неявного преобразования в одни типы и обратно, но не в другие?

Давайте создадим класс и сделаем его неявно конвертируемым в 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

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

// 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
02.11.2019
Новые материалы

Коллекции публикаций по глубокому обучению
Последние пару месяцев я создавал коллекции последних академических публикаций по различным подполям глубокого обучения в моем блоге https://amundtveit.com - эта публикация дает обзор 25..

Представляем: Pepita
Фреймворк JavaScript с открытым исходным кодом Я знаю, что недостатка в фреймворках JavaScript нет. Но я просто не мог остановиться. Я хотел написать что-то сам, со своими собственными..

Советы по коду Laravel #2
1-) Найти // You can specify the columns you need // in when you use the find method on a model User::find(‘id’, [‘email’,’name’]); // You can increment or decrement // a field in..

Работа с временными рядами спутниковых изображений, часть 3 (аналитика данных)
Анализ временных рядов спутниковых изображений для данных наблюдений за большой Землей (arXiv) Автор: Рольф Симоэс , Жильберто Камара , Жильберто Кейрос , Фелипе Соуза , Педро Р. Андраде ,..

3 способа решить квадратное уравнение (3-й мой любимый) -
1. Методом факторизации — 2. Используя квадратичную формулу — 3. Заполнив квадрат — Давайте поймем это, решив это простое уравнение: Мы пытаемся сделать LHS,..

Создание VR-миров с A-Frame
Виртуальная реальность (и дополненная реальность) стали главными модными терминами в образовательных технологиях. С недорогими VR-гарнитурами, такими как Google Cardboard , и использованием..

Демистификация рекурсии
КОДЕКС Демистификация рекурсии Упрощенная концепция ошеломляющей О чем весь этот шум? Рекурсия, кажется, единственная тема, от которой у каждого начинающего студента-информатика..