Поделиться через


Проверенные пользовательские операторы

Заметка

Эта статья является спецификацией компонентов. Спецификация служит проектным документом для функции. Она включает предлагаемые изменения спецификации, а также информацию, необходимую во время проектирования и разработки функции. Эти статьи публикуются до тех пор, пока предложенные изменения спецификации не будут завершены и включены в текущую спецификацию ECMA.

Может возникнуть некоторое несоответствие между спецификацией компонентов и завершенной реализацией. Различия зафиксированы в соответствующих заметках собраний по проектированию языка (LDM).

Дополнительные сведения о процессе внедрения спецификаций функций в стандарт языка C# см. в статье о спецификациях .

Сводка

C# должен поддерживать определение checked вариантов следующих определяемых пользователем операторов, чтобы пользователи могли как включать, так и отключать поведение переполнения в зависимости от необходимости.

  • Унарные операторы ++ и --§12.8.16 и §12.9.6.
  • Унарный оператор -§12.9.3.
  • Двоичные операторы +, -, *и /§12.10.
  • Явные операторы преобразования.

Мотивация

Пользователь не может объявить тип и поддерживать как проверенные, так и непроверенные версии оператора. Это затрудняет перенос различных алгоритмов для использования предлагаемых интерфейсов generic math, предоставляемых командой библиотек. Аналогичным образом это делает невозможным предоставление такого типа, как Int128 или UInt128 без одновременного предоставления языком собственной поддержки, чтобы избежать разрушающих изменений.

Подробный дизайн

Синтаксис

Грамматика операторов (§15.10) будет изменена, чтобы разрешить использование ключевого слова checked после ключевого слова operator непосредственно перед токеном оператора.

overloadable_unary_operator
    : '+' | 'checked'? '-' | '!' | '~' | 'checked'? '++' | 'checked'? '--' | 'true' | 'false'
    ;

overloadable_binary_operator
    : 'checked'? '+'   | 'checked'? '-'   | 'checked'? '*'   | 'checked'? '/'   | '%'   | '&'   | '|'   | '^'   | '<<'
    | right_shift | '=='  | '!='  | '>'   | '<'   | '>='  | '<='
    ;
    
conversion_operator_declarator
    : 'implicit' 'operator' type '(' type identifier ')'
    | 'explicit' 'operator' 'checked'? type '(' type identifier ')'
    ;    

Например:

public static T operator checked ++(T x) {...}
public static T operator checked --(T x) {...}
public static T operator checked -(T x) {...}
public static T operator checked +(T lhs, T rhs) {...}
public static T operator checked -(T lhs, T rhs) {...}
public static T operator checked *(T lhs, T rhs) {...}
public static T operator checked /(T lhs, T rhs) {...}
public static explicit operator checked U(T x) {...}
public static T I1.operator checked ++(T x) {...}
public static T I1.operator checked --(T x) {...}
public static T I1.operator checked -(T x) {...}
public static T I1.operator checked +(T lhs, T rhs) {...}
public static T I1.operator checked -(T lhs, T rhs) {...}
public static T I1.operator checked *(T lhs, T rhs) {...}
public static T I1.operator checked /(T lhs, T rhs) {...}
public static explicit I1.operator checked U(T x) {...}

Для краткости ниже оператор с ключевым словом checked называется checked operator и оператором без него называется regular operator. Эти термины не применимы к операторам, у которых нет формы checked.

Семантика

Как ожидается, определяемая пользователем checked operator создает исключение, если результат операции слишком велик, чтобы его можно было представить в типе назначения. Что значит быть слишком большим в действительности зависит от характера целевого типа и не предписывается языком. Как правило, исключение вызывается System.OverflowException, но язык не имеет конкретных требований в отношении этого.

Ожидается, что определяемая пользователем regular operator не создает исключение, если результат операции слишком велик, чтобы представить в целевом типе. Вместо этого ожидается, что будет возвращен экземпляр, представляющий усеченный результат. Что значит быть слишком большим и подвергнуться усечению, на самом деле зависит от природы целевого типа и не предписывается языком.

Все существующие пользовательские операторы, имеющие форму checked и поддерживаемые в категории regular operators. Понятно, что многие из них, скорее всего, не следуют семантике, указанной выше, но для целей семантического анализа компилятор предполагает, что они являются.

Проверенный или непроверенный контекст в checked operator

Контекст, отмеченный как установленный или снятый в составе checked operator, не зависит от наличия ключевого слова checked. Другими словами, контекст такой же, как и сразу в начале объявления оператора. Разработчику потребуется явно переключить контекст, если часть алгоритма не может полагаться на контекст по умолчанию.

Имена в метаданных

Раздел I.10.3.1 "Унарные операторы" ECMA-335 будет изменен, чтобы включить op_CheckedIncrement, op_CheckedDecrement, op_CheckedUnaryNegation в качестве имен методов, реализующих проверенные ++, -- и - унарные операторы.

Раздел I.10.3.2 "Двоичные операторы" стандарта ECMA-335 будет изменен для включения op_CheckedAddition, op_CheckedSubtraction, op_CheckedMultiply, op_CheckedDivision в качестве имен методов, реализующих проверку двоичных операторов +, -, *и /.

Раздел "Операторы преобразования I.10.3.3" ECMA-335 будет изменен, чтобы включить op_CheckedExplicit в качестве имени метода, реализующего проверенный оператор явного преобразования.

Унарные операторы

Унарный checked operators следует правилам §15.10.2.

Кроме того, для объявления checked operator требуется парное объявление regular operator (возвращаемый тип также должен совпадать). Ошибка во время компиляции возникает в противном случае.

public struct Int128
{
    // This is fine, both a checked and regular operator are defined
    public static Int128 operator checked -(Int128 lhs);
    public static Int128 operator -(Int128 lhs);

    // This is fine, only a regular operator is defined
    public static Int128 operator --(Int128 lhs);

    // This should error, a regular operator must also be defined
    public static Int128 operator checked ++(Int128 lhs);
}

Двоичные операторы

Двоичным checked operators следует соблюдать правила из §15.10.3.

Кроме того, для объявления checked operator требуется парное объявление regular operator (возвращаемый тип также должен совпадать). Ошибка во время компиляции возникает в противном случае.

public struct Int128
{
    // This is fine, both a checked and regular operator are defined
    public static Int128 operator checked +(Int128 lhs, Int128 rhs);
    public static Int128 operator +(Int128 lhs, Int128 rhs);

    // This is fine, only a regular operator is defined
    public static Int128 operator -(Int128 lhs, Int128 rhs);

    // This should error, a regular operator must also be defined
    public static Int128 operator checked *(Int128 lhs, Int128 rhs);
}

Возможные операторы, определяемые пользователем

Операторы пользователей-кандидатов (§12.4.6) будут изменены следующим образом (дополнения и изменения выделены полужирным шрифтом).

Учитывая тип T и данную операцию operator op(A), где op является перегруженным оператором и A является списком аргументов, набор пользовательских операторов, предоставляемых T для operator op(A), определяется следующим образом.

  • Определите тип T0. Если T является nullable типом, T0 — его базовый тип, иначе T0 равно T.
  • Найдите набор операторов, определённых пользователем U. Этот набор состоит из следующих элементов:
    • В контексте оценки unchecked учитываются все обычные объявления operator op в T0.
    • В контексте оценки checked все проверенные и обычные объявления operator op в T0, за исключением регулярных объявлений, которые имеют парное сопоставление с checked operator объявлениями.
  • Для всех объявлений operator op в U и всех обобщённых форм таких операторов, если хотя бы один оператор применим (§12.4.6 - Применимый член функции) относительно списка аргументов A, то набор операторов-кандидатов состоит из всех таких применимых операторов в T0.
  • В противном случае, если T0 равен object, набор кандидатов на операторов пуст.
  • В противном случае набор операторов кандидатов, предоставляемых T0, является набором операторов кандидатов, предоставляемых прямым базовым классом T0, или эффективным базовым классом T0, если T0 является параметром типа.

Аналогичные правила будут применяться при определении набора операторов-кандидатов в интерфейсах https://github.com/dotnet/csharplang/blob/main/meetings/2017/LDM-2017-06-27.md#shadowing-within-interfaces.

Раздел §12.8.20 будет изменен, чтобы отразить влияние установленного или снятого контекста на разрешение перегрузки унарных и бинарных операторов.

Пример #1:

public class MyClass
{
    public static void Add(Int128 lhs, Int128 rhs)
    {
        // Resolves to `op_CheckedAddition`
        Int128 r1 = checked(lhs + rhs);

        // Resolves to `op_Addition`
        Int128 r2 = unchecked(lhs + rhs);

        // Resolve to `op_Subtraction`
        Int128 r3 = checked(lhs - rhs);

        // Resolve to `op_Subtraction`
        Int128 r4 = unchecked(lhs - rhs);

        // Resolves to `op_CheckedMultiply`
        Int128 r5 = checked(lhs * rhs);

        // Error: Operator '*' cannot be applied to operands of type 'Int128' and 'Int128'
        Int128 r6 = unchecked(lhs * rhs);
    }

    public static void Divide(Int128 lhs, byte rhs)
    {
        // Resolves to `op_Division` - it is a better match than `op_CheckedDivision`
        Int128 r4 = checked(lhs / rhs);
    }
}

public struct Int128
{
    public static Int128 operator checked +(Int128 lhs, Int128 rhs);
    public static Int128 operator +(Int128 lhs, Int128 rhs);

    public static Int128 operator -(Int128 lhs, Int128 rhs);

    // Cannot be declared in C# - missing unchecked operator, but could be declared by some other language
    public static Int128 operator checked *(Int128 lhs, Int128 rhs);

    public static Int128 operator checked /(Int128 lhs, int rhs);

    public static Int128 operator /(Int128 lhs, byte rhs);
}

Пример #2:

class C
{
    static void Add(C2 x, C3 y)
    {
        object o;
        
        // error CS0034: Operator '+' is ambiguous on operands of type 'C2' and 'C3'
        o = checked(x + y);
        
        // C2.op_Addition
        o = unchecked(x + y);
    }
}

class C1
{
    // Cannot be declared in C# - missing unchecked operator, but could be declared by some other language
    public static C1 operator checked + (C1 x, C3 y) => new C3();
}

class C2 : C1
{
    public static C2 operator + (C2 x, C1 y) => new C2();
}

class C3 : C1
{
}

Пример #3:

class C
{
    static void Add(C2 x, C3 y)
    {
        object o;
        
        // error CS0034: Operator '+' is ambiguous on operands of type 'C2' and 'C3'
        o = checked(x + y);
        
        // C1.op_Addition
        o = unchecked(x + y);
    }
}

class C1
{
    public static C1 operator + (C1 x, C3 y) => new C3();
}

class C2 : C1
{
    // Cannot be declared in C# - missing unchecked operator, but could be declared by some other language
    public static C2 operator checked + (C2 x, C1 y) => new C2();
}

class C3 : C1
{
}

Операторы преобразования

Преобразование checked operators следуйте правилам §15.10.4.

Однако для объявления checked operator требуется парное объявление regular operator. Ошибка во время компиляции возникает в противном случае.

Следующий абзац

Подпись оператора преобразования состоит из исходного типа и целевого типа. (Это единственная форма элемента, для которого возвращаемый тип участвует в сигнатуре.) Неявная или явная классификация оператора преобразования не является частью подписи оператора. Таким образом, класс или структура не может объявлять как неявный, так и явный оператор преобразования с одинаковыми исходными и целевыми типами.

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

Обработка специфически заданных пользователем явных преобразований

Третий маркер в §10.5.5:

  • Найдите набор применимых пользовательских и поднятых операторов преобразования, U. Этот набор включает определяемые пользователем и обобщенные (или перегруженные) неявные или явные операторы преобразования, объявленные классами или структурами в D, которые выполняют преобразование из типа, включающего или включенного в S, в тип, включающий или включенный в T. Если U пуст, преобразование не определено и возникает ошибка во время компиляции.

будут заменены следующими маркерами:

  • Найдите набор операторов преобразования, U0. Этот набор состоит из следующих элементов:
    • В контексте оценки unchecked определяемые пользователем неявные или обычные явные операторы преобразования, объявленные классами или структурами в D.
    • В контексте оценки checked определяемые пользователем неявные или регулярные или проверенные явные операторы преобразования, объявленные классами или структурами в D, кроме обычных явных операторов преобразования, которые имеют парно-мудрое сопоставление checked operator объявление в том же декларативном типе.
  • Найдите набор применимых пользовательских и поднятых операторов преобразования, U. Этот набор состоит из определяемых пользователем и снятых неявных или явных операторов преобразования в U0, которые преобразуются из типа, охватывающего или охватываемого S в тип, охватывающий или охватываемый T. Если U пуст, преобразование не определено и возникает ошибка во время компиляции.

Проверенные и снятые операторы разделу "11.8.20" будут изменены, чтобы отразить влияние, которое отображается в контексте проверки или отмены проверки при обработке определяемых пользователем явных преобразований.

Внедрение операторов

checked operator не реализует regular operator и наоборот.

Деревья выражений Linq

Checked operators будет поддерживаться в деревах выражений Linq. Узел UnaryExpression/BinaryExpression будет создан с соответствующим MethodInfo. Будут использоваться следующие методы фабрики:

public static UnaryExpression NegateChecked (Expression expression, MethodInfo? method);

public static BinaryExpression AddChecked (Expression left, Expression right, MethodInfo? method);
public static BinaryExpression SubtractChecked (Expression left, Expression right, MethodInfo? method);
public static BinaryExpression MultiplyChecked (Expression left, Expression right, MethodInfo? method);

public static UnaryExpression ConvertChecked (Expression expression, Type type, MethodInfo? method);

Обратите внимание, что C# не поддерживает присваивания в деревьях выражений, поэтому проверки инкремента и декремента также не поддерживаются.

Не существует метода фабрики для проверенного деления. Существует открытый вопрос об этом - проверенное деление в деревах выражений Linq.

Динамический

Мы изучим затраты на добавление поддержки проверяемых операторов в динамическом вызове в CoreCLR и внедрим её, если затраты не будут слишком высокими. Это цитата из https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-02-09.md.

Недостатки

Это увеличивает сложность языка и позволяет пользователям вносить больше видов разрушительных изменений в их типы.

Альтернативы

Универсальные математические интерфейсы, которые библиотеки планируют предоставлять, могут предоставлять именованные методы (например, AddChecked). Основной недостаток заключается в том, что это менее читаемо и удобно для поддержки и не использует правила приоритета операторов в языке.

В этом разделе перечислены альтернативные варианты, но не реализованные

Размещение ключевого слова checked

Кроме того, ключевое слово checked можно переместить в место прямо перед ключевым словом operator:

public static T checked operator ++(T x) {...}
public static T checked operator --(T x) {...}
public static T checked operator -(T x) {...}
public static T checked operator +(T lhs, T rhs) {...}
public static T checked operator -(T lhs, T rhs) {...}
public static T checked operator *(T lhs, T rhs) {...}
public static T checked operator /(T lhs, T rhs) {...}
public static explicit checked operator U(T x) {...}
public static T checked I1.operator ++(T x) {...}
public static T checked I1.operator --(T x) {...}
public static T checked I1.operator -(T x) {...}
public static T checked I1.operator +(T lhs, T rhs) {...}
public static T checked I1.operator -(T lhs, T rhs) {...}
public static T checked I1.operator *(T lhs, T rhs) {...}
public static T checked I1.operator /(T lhs, T rhs) {...}
public static explicit checked I1.operator U(T x) {...}

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

operator_modifier
    : 'public'
    | 'static'
    | 'extern'
    | 'checked'
    | operator_modifier_unsafe
    ;
public static checked T operator ++(T x) {...}
public static checked T operator --(T x) {...}
public static checked T operator -(T x) {...}
public static checked T operator +(T lhs, T rhs) {...}
public static checked T operator -(T lhs, T rhs) {...}
public static checked T operator *(T lhs, T rhs) {...}
public static checked T operator /(T lhs, T rhs) {...}
public static checked explicit operator U(T x) {...}
public static checked T I1.operator ++(T x) {...}
public static checked T I1.operator --(T x) {...}
public static checked T I1.operator -(T x) {...}
public static checked T I1.operator +(T lhs, T rhs) {...}
public static checked T I1.operator -(T lhs, T rhs) {...}
public static checked T I1.operator *(T lhs, T rhs) {...}
public static checked T I1.operator /(T lhs, T rhs) {...}
public static checked explicit I1.operator U(T x) {...}

ключевое слово unchecked

Были предложения по поддержке ключевого слова unchecked в той же позиции, что и ключевое слово checked со следующими возможными значениями:

  • Просто для явного отражения регулярного характера оператора или
  • Возможно, для обозначения особенной специфики оператора, который должен использоваться в контексте unchecked. Язык может поддерживать op_Addition, op_CheckedAdditionи op_UncheckedAddition, чтобы ограничить количество критических изменений. Это добавляет еще один уровень сложности, который, скорее всего, не требуется в большинстве кода.

Имена операторов в ECMA-335

Кроме того, имена операторов могут быть op_UnaryNegationChecked, op_AdditionChecked, op_SubtractionChecked, op_MultiplyChecked, op_DivisionChecked, с Checked в конце. Похоже, что уже установлен шаблон, когда имена заканчиваются словом "оператора". Например, существует оператор op_UnsignedRightShift, а не оператор op_RightShiftUnsigned.

Checked operators неприменимо в контексте unchecked

Компилятор при выполнении поиска членов для нахождения пользовательских операторов-кандидатов в контексте unchecked может игнорировать checked operators. Если обнаружены метаданные, определяющие только checked operator, возникнет ошибка компиляции.

public class MyClass
{
    public static void Add(Int128 lhs, Int128 rhs)
    {
        // Resolves to `op_CheckedMultiply`
        Int128 r5 = checked(lhs * rhs);

        // Error: Operator '*' cannot be applied to operands of type 'Int128' and 'Int128'
        Int128 r5 = unchecked(lhs * rhs);
    }
}

public struct Int128
{
    public static Int128 operator checked *(Int128 lhs, Int128 rhs);
}

Более сложные правила поиска операторов и разрешения перегрузки в контексте checked

Компилятор, выполняя поиск элемента для нахождения кандидатов на пользовательские операторы в контексте checked, также будет учитывать применимые операторы, заканчивающиеся на Checked. То есть, если компилятор пытается найти применимые члены функции для оператора двоичного добавления, он будет искать как op_Addition, так и op_AdditionChecked. Если единственным применимым элементом функции является checked operator, он будет использоваться. Если существуют regular operator и checked operator и применяются одинаково, то checked operator будет предпочтительнее. Если и regular operator, и checked operator существуют, но regular operator точно совпадает, а checked operator нет, компилятор предпочитает regular operator.

public class MyClass
{
    public static void Add(Int128 lhs, Int128 rhs)
    {
        // Resolves to `op_CheckedAddition`
        Int128 r1 = checked(lhs + rhs);

        // Resolves to `op_Addition`
        Int128 r2 = unchecked(lhs + rhs);

        // Resolve to `op_Subtraction`
        Int128 r3 = checked(lhs - rhs);

        // Resolve to `op_Subtraction`
        Int128 r4 = unchecked(lhs - rhs);
    }

    public static void Multiply(Int128 lhs, byte rhs)
    {
        // Resolves to `op_Multiply` even though `op_CheckedMultiply` is also applicable
        Int128 r4 = checked(lhs * rhs);
    }
}

public struct Int128
{
    public static Int128 operator checked +(Int128 lhs, Int128 rhs);
    public static Int128 operator +(Int128 lhs, Int128 rhs);

    public static Int128 operator -(Int128 lhs, Int128 rhs);

    public static Int128 operator checked *(Int128 lhs, int rhs);
    public static Int128 operator *(Int128 lhs, byte rhs);
}

Еще один способ создания набора пользовательских операторов

Разрешение перегрузки унарного оператора

Предположим, что regular operator совпадают в контексте оценки unchecked, checked operator соответствует контексту оценки checked и оператор, который не имеет формы checked (например, +), соответствует одному из контекстов, то первый пункт в §12.4.4 — разрешение перегрузки унарного оператора:

будет заменено следующими двумя точками маркера:

  • Набор определяемых пользователем операторов, предоставляемых для операции , определяется в соответствующем проверяемом или непроверяемом контексте с использованием правил определения кандидатов операторов, заданных пользователем.
  • Если набор пользовательских операторов не является пустым, это становится набором операторов-кандидатов для операции. В противном случае набор кандидатов пользовательских операторов, предоставляемого X для операции operator op(x), совпадающего с противоположным контекстом проверки или без проверки, определяется с помощью правил §12.4.6 — кандидаты пользовательских операторов.

Разрешение перегрузки двоичных операторов

Предположим, что regular operator соответствует контексту оценки unchecked, checked operator соответствует контексту оценки checked и оператор, который не имеет формы checked (например, %), соответствует любому из контекстов, то первый пункт в списке в §12.4.5 — разрешение перегрузки двоичных операторов:

  • Определяется набор потенциальных пользовательских операторов, предоставляемый X и Y для операции operator op(x,y). Набор состоит из объединения кандидатных операторов, предоставляемых X, и кандидатных операторов, предоставляемых Y, при этом каждый определяется согласно правилам §12.4.6 — кандидатные операторы, определяемые пользователем. Если X и Y одного типа, или X и Y являются производными от общего базового типа, то операторы, общие для кандидатов, встречаются в объединённом наборе только один раз.

будет заменено следующими двумя точками маркера:

  • Определяется набор пользовательских определённых операторов, предоставляемых X и Y для операции operator op(x,y), соответствующего текущему проверенному/непроверенному контексту. Набор состоит из объединения операторов-кандидатов, предоставляемых X, и операторов-кандидатов, предоставляемых Y, при этом каждый определяется с помощью правил §12.4.6 — кандидаты на пользовательские операторы. Если X и Y одного типа, или если X и Y производные от общего базового типа, то общие операторы-кандидаты встречаются в объединенном наборе только один раз.
  • Если набор операторов, определяемых пользователем, не является пустым, то он становится набором операторов-кандидатов для операции. В противном случае определяется набор пользовательских операторов, предоставляемых X и Y для операции operator op(x,y), соответствующей противоположному контексту проверённости/непроверённости. Набор состоит из объединения кандидатных операторов, предоставляемых X, и кандидатных операторов, предоставляемых Y, каждый из которых определяется согласно правилам §12.4.6 — Пользовательские определенные операторные кандидаты. Если X и Y одного типа, или если X и Y производные от общего базового типа, то общие кандидаты-операторы встречаются только один раз в объединённом наборе.
Пример #1:
public class MyClass
{
    public static void Add(Int128 lhs, Int128 rhs)
    {
        // Resolves to `op_CheckedAddition`
        Int128 r1 = checked(lhs + rhs);

        // Resolves to `op_Addition`
        Int128 r2 = unchecked(lhs + rhs);

        // Resolve to `op_Subtraction`
        Int128 r3 = checked(lhs - rhs);

        // Resolve to `op_Subtraction`
        Int128 r4 = unchecked(lhs - rhs);

        // Resolves to `op_CheckedMultiply`
        Int128 r5 = checked(lhs * rhs);

        // Resolves to `op_CheckedMultiply`
        Int128 r5 = unchecked(lhs * rhs);
    }

    public static void Divide(Int128 lhs, byte rhs)
    {
        // Resolves to `op_CheckedDivision`
        Int128 r4 = checked(lhs / rhs);
    }
}

public struct Int128
{
    public static Int128 operator checked +(Int128 lhs, Int128 rhs);
    public static Int128 operator +(Int128 lhs, Int128 rhs);

    public static Int128 operator -(Int128 lhs, Int128 rhs);

    public static Int128 operator checked *(Int128 lhs, Int128 rhs);

    public static Int128 operator checked /(Int128 lhs, int rhs);
    public static Int128 operator /(Int128 lhs, byte rhs);
}
Пример #2:
class C
{
    static void Add(C2 x, C3 y)
    {
        object o;
        
        // C1.op_CheckedAddition
        o = checked(x + y);
        
        // C2.op_Addition
        o = unchecked(x + y);
    }
}

class C1
{
    public static C1 operator checked + (C1 x, C3 y) => new C3();
}

class C2 : C1
{
    public static C2 operator + (C2 x, C1 y) => new C2();
}

class C3 : C1
{
}
Пример #3:
class C
{
    static void Add(C2 x, C3 y)
    {
        object o;
        
        // C2.op_CheckedAddition
        o = checked(x + y);
        
        // C1.op_Addition
        o = unchecked(x + y);
    }
}

class C1
{
    public static C1 operator + (C1 x, C3 y) => new C3();
}

class C2 : C1
{
    public static C2 operator checked + (C2 x, C1 y) => new C2();
}

class C3 : C1
{
}
Пример #4:
class C
{
    static void Add(C2 x, byte y)
    {
        object o;
        
        // C1.op_CheckedAddition
        o = checked(x + y);
        
        // C2.op_Addition
        o = unchecked(x + y);
    }

    static void Add2(C2 x, int y)
    {
        object o;
        
        // C2.op_Addition
        o = checked(x + y);
        
        // C2.op_Addition
        o = unchecked(x + y);
    }
}

class C1
{
    public static C1 operator checked + (C1 x, byte y) => new C1();
}

class C2 : C1
{
    public static C2 operator + (C2 x, int y) => new C2();
}
Пример #5:
class C
{
    static void Add(C2 x, byte y)
    {
        object o;
        
        // C2.op_CheckedAddition
        o = checked(x + y);
        
        // C1.op_Addition
        o = unchecked(x + y);
    }

    static void Add2(C2 x, int y)
    {
        object o;
        
        // C1.op_Addition
        o = checked(x + y);
        
        // C1.op_Addition
        o = unchecked(x + y);
    }
}

class C1
{
    public static C1 operator + (C1 x, int y) => new C1();
}

class C2 : C1
{
    public static C2 operator checked + (C2 x, byte y) => new C2();
}

Обработка явных преобразований, заданных пользователем

Предположим, что regular operator соответствует контексту оценки unchecked и checked operator соответствует контексту оценки checked, третий пункт в §10.5.3 Оценка определяемых пользователем преобразований:

  • Найдите набор применимых пользовательских и поднятых операторов преобразования, U. Этот набор состоит из определяемых пользователем и поднимаемых неявных или явных операторов преобразования, которые объявлены классами или структурами в D и преобразовывают тип, включающий или охваченный S, в тип, включающий или охватываемый T. Если U пуст, преобразование не определено и возникает ошибка во время компиляции.

будет заменено следующими пунктами списка:

  • Найдите набор применимых пользовательских и продвинутых явных операторов преобразования , соответствующих текущему проверенному/непроверенному контексту, U0. Этот набор состоит из операторов явного преобразования, определяемых пользователем и предоставляемых (или поднятых) классами или структурами в D, которые соответствуют текущему проверенному/непроверенному контексту и преобразуют из типа, включающего или включаемого S в тип, включающий или включаемый T.
  • Найдите набор применимых пользовательских и поднятых явных операторов преобразования , соответствующих противоположному контексту проверки/непроверкии U1. Если U0 не пуст, U1 пуст. В противном случае этот набор состоит из определенных пользователем и снятых явных операторов преобразования, объявленных классами или структурами в D, которые соответствуют противоположному контексту "проверен/непроверен" и преобразуются из типа, охватывающего или охватываемого S, в тип, охватывающий или охватываемый T.
  • Найдите набор применимых пользовательских и поднятых операторов преобразования, U. Этот набор состоит из операторов из U0, U1, а также из определяемых пользователем и поднятых неявных операторов преобразования, объявленных классами или структурами в D, которые преобразуют из типа, охватывающего или охватываемого S, в тип, охватывающий или охватываемый T. Если U пуст, преобразование не определено и возникает ошибка во время компиляции.

Еще один способ создания набора пользовательских операторов, определенных кандидатом

Разрешение перегрузки унарного оператора

Первый пункт в разделе §12.4.4 будет изменен следующим образом (дополнения выделены полужирным шрифтом).

  • Набор определяемых пользователем операторов, предоставляемых X для операции operator op(x), определяется с помощью правил "Потенциальные определяемые пользователем операторы" ниже. Если набор содержит по крайней мере один оператор в проверенной форме, все операторы в регулярной форме удаляются из набора.

Раздел §12.8.20 будет изменен, чтобы отразить влияние, которое оказывает контекст, когда он установлен или снят, на разрешение перегрузки унарного оператора.

Разрешение перегрузки двоичных операторов

Первый пункт в разделе §12.4.5 будет изменен следующим образом (дополнения выделены полужирным шрифтом).

  • Определяется набор пользовательских определённых операторов, предложенных X и Y для операции operator op(x,y). Набор состоит из объединения множеств операторов-кандидатов, предоставляемых X, и операторов-кандидатов, предоставляемых Y, каждый из которых определяется с использованием правил, приведенных в разделе "Кандидаты определяемых пользователем операторов" ниже. Если X и Y одного типа или X и Y производные от общего базового типа, то общие оператор-кандидаты встречаются в объединенной совокупности только один раз. Если набор содержит по крайней мере один оператор в проверенной форме, все операторы в регулярной форме удаляются из набора.

Операторы с проверкой и без проверки §12.8.20 будут скорректированы, чтобы отразить влияние проверяемого или непроверяемого контекста на разрешение перегрузки двоичных операторов.

Операторы, определенные пользователем и являющиеся кандидатами

§12.4.6 Раздел " - кандидатов на определяемые пользователем операторы" будет изменён следующим образом (новые добавления выделены полужирным шрифтом).

Учитывая тип T и операцию operator op(A), где op является перегруженным оператором и A — списком аргументов, набор кандидатов пользовательских операторов, предоставляемых T для operator op(A), определяется следующим образом:

  • Определите тип T0. Если T является типом, допускаемым значением NULL, T0 является его базовым типом, в противном случае T0 равно T.
  • Для всех объявлений operator opв их проверенной и регулярной формах в контексте оценки checked и в их регулярной форме только в контексте оценки unchecked в T0 и всех поднятых формах таких операторов, если хотя бы один оператор применим (§12.6.4.2) относительно списка аргументов A, то набор операторов-кандидатов состоит из всех таких применимых операторов в T0.
  • В противном случае, если T0 равно object, набор кандидатных операторов пуст.
  • В противном случае набор операторов кандидатов, предоставляемых T0, является набором операторов кандидатов, предоставляемых прямым базовым классом T0, или эффективным базовым классом T0, если T0 является параметром типа.

При определении набора операторов-кандидатов в интерфейсах https://github.com/dotnet/csharplang/blob/main/meetings/2017/LDM-2017-06-27.md#shadowing-within-interfacesприменяется аналогичная фильтрация.

Раздел §12.8.20 будет изменен, чтобы отразить эффект, который установленный или снятый контекст имеет для разрешения перегрузки унарных и бинарных операторов.

Пример #1:
public class MyClass
{
    public static void Add(Int128 lhs, Int128 rhs)
    {
        // Resolves to `op_CheckedAddition`
        Int128 r1 = checked(lhs + rhs);

        // Resolves to `op_Addition`
        Int128 r2 = unchecked(lhs + rhs);

        // Resolve to `op_Subtraction`
        Int128 r3 = checked(lhs - rhs);

        // Resolve to `op_Subtraction`
        Int128 r4 = unchecked(lhs - rhs);

        // Resolves to `op_CheckedMultiply`
        Int128 r5 = checked(lhs * rhs);

        // Error: Operator '*' cannot be applied to operands of type 'Int128' and 'Int128'
        Int128 r5 = unchecked(lhs * rhs);
    }

    public static void Divide(Int128 lhs, byte rhs)
    {
        // Resolves to `op_CheckedDivision`
        Int128 r4 = checked(lhs / rhs);
    }
}

public struct Int128
{
    public static Int128 operator checked +(Int128 lhs, Int128 rhs);
    public static Int128 operator +(Int128 lhs, Int128 rhs);

    public static Int128 operator -(Int128 lhs, Int128 rhs);

    public static Int128 operator checked *(Int128 lhs, Int128 rhs);

    public static Int128 operator checked /(Int128 lhs, int rhs);
    public static Int128 operator /(Int128 lhs, byte rhs);
}
Пример #2:
class C
{
    static void Add(C2 x, C3 y)
    {
        object o;
        
        // C1.op_CheckedAddition
        o = checked(x + y);
        
        // C2.op_Addition
        o = unchecked(x + y);
    }
}

class C1
{
    public static C1 operator checked + (C1 x, C3 y) => new C3();
}

class C2 : C1
{
    public static C2 operator + (C2 x, C1 y) => new C2();
}

class C3 : C1
{
}
Пример #3:
class C
{
    static void Add(C2 x, C3 y)
    {
        object o;
        
        // C2.op_CheckedAddition
        o = checked(x + y);
        
        // C1.op_Addition
        o = unchecked(x + y);
    }
}

class C1
{
    public static C1 operator + (C1 x, C3 y) => new C3();
}

class C2 : C1
{
    public static C2 operator checked + (C2 x, C1 y) => new C2();
}

class C3 : C1
{
}
Пример #4:
class C
{
    static void Add(C2 x, byte y)
    {
        object o;
        
        // C2.op_Addition
        o = checked(x + y);
        
        // C2.op_Addition
        o = unchecked(x + y);
    }

    static void Add2(C2 x, int y)
    {
        object o;
        
        // C2.op_Addition
        o = checked(x + y);
        
        // C2.op_Addition
        o = unchecked(x + y);
    }
}

class C1
{
    public static C1 operator checked + (C1 x, byte y) => new C1();
}

class C2 : C1
{
    public static C2 operator + (C2 x, int y) => new C2();
}
Пример #5:
class C
{
    static void Add(C2 x, byte y)
    {
        object o;
        
        // C2.op_CheckedAddition
        o = checked(x + y);
        
        // C1.op_Addition
        o = unchecked(x + y);
    }

    static void Add2(C2 x, int y)
    {
        object o;
        
        // C1.op_Addition
        o = checked(x + y);
        
        // C1.op_Addition
        o = unchecked(x + y);
    }
}

class C1
{
    public static C1 operator + (C1 x, int y) => new C1();
}

class C2 : C1
{
    public static C2 operator checked + (C2 x, byte y) => new C2();
}

Обработка явных преобразований, определяемых пользователем

Третий маркер в §10.5.5:

  • Найдите набор применимых пользовательских определённых и поднятых операторов преобразования, U. Этот набор состоит из определяемых пользователем и поднятых неявных или явных операторов преобразования, объявленных классами или структурами в D, которые преобразуются из типа, охватывающего или охватываемого S, в тип, охватывающий или охватываемый T. Если U пуст, преобразование не определено и возникает ошибка во время компиляции.

будет заменено следующими маркированными пунктами:

  • Найдите набор применимых пользовательских и снятых явных операторов преобразования, U0. Этот набор состоит из определяемых пользователем и снятых явных операторов преобразования, объявленных классами или структурами в Dв их проверяемых и обычных формах в контексте оценки checked и только в обычной форме в unchecked в контексте оценки, и преобразование из типа, охватывающего или охватываемого S, в тип, охватывающий или охватываемый T.
  • Если U0 содержит по крайней мере один оператор в проверенной форме, все операторы в регулярной форме удаляются из набора.
  • Найдите набор применимых пользовательских и снятых операторов преобразования, U. Этот набор состоит из операторов из U0, а также из пользовательских и поднятых неявных операторов преобразования, объявленных классами или структурами в D, которые преобразуют из типа, охватывающего или охватываемого S, в тип, охватывающий или охватываемый T. Если U пуст, преобразование не определено и возникает ошибка во время компиляции.

Операторы checked и unchecked §12.8.20 будут изменены, чтобы отразить влияние, которое контекст checked/unchecked оказывает на обработку определяемых пользователем явных преобразований.

Проверенный или непроверенный контекст в checked operator

Компилятор может рассматривать контекст по умолчанию checked operator как проверенный. Разработчику потребуется явно использовать unchecked, если часть алгоритма не должна участвовать в checked context. Однако это может не сработать в будущем, если мы начнем разрешать использовать токены checked/unchecked в качестве модификаторов операторов, чтобы установить контекст внутри тела. Модификатор и ключевое слово могут противоречить друг другу. Кроме того, мы не смогли бы иметь ту же возможность (рассматривать контекст по умолчанию как неотмеченный) для regular operator, потому что это будет ломающее изменение.

Неразрешенные вопросы

Следует ли языку разрешать checked и unchecked модификаторы методов (например, static checked void M())? Это позволит удалить уровни вложенности для методов, которые в этом нуждаются.

Проверенное разделение в деревах выражений Linq

Нет фабричного метода для создания проверяемого узла деления, и нет элемента ExpressionType.DivideChecked. Мы все еще можем использовать следующий фабричный метод для создания обычного узла деления, где MethodInfo указывает на метод op_CheckedDivision. Потребителям придется проверить имя для вывода контекста.

public static BinaryExpression Divide (Expression left, Expression right, MethodInfo? method);

Обратите внимание, что хотя §12.8.20 раздел перечисляет оператор / в качестве одного из операторов, затронутых контекстом оценки с проверкой/без проверки, IL не имеет специального кода для выполнения проверяемого деления. Сегодня компилятор всегда использует метод фабрики независимо от контекста.

предложение : проверенное пользователем представление не будет поддерживаться в деревах выражений Linq.

(Разрешено) Следует ли поддерживать неявные проверяемые операторы преобразования?

Как правило, неявные операторы преобразования не должны вызываться.

предложение : Нет.

Решение : утверждено — https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-02-07.md#checked-implicit-conversions

Совещания по дизайну

https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-02-07.md https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-02-09.md https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-02-14.md https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-02-23.md