Freigeben über


Überprüfte benutzerdefinierte Operatoren

Hinweis

Dieser Artikel ist eine Feature-Spezifikation. Die Spezifikation dient als Designdokument für das Feature. Es enthält vorgeschlagene Spezifikationsänderungen sowie Informationen, die während des Entwurfs und der Entwicklung des Features erforderlich sind. Diese Artikel werden veröffentlicht, bis die vorgeschlagenen Spezifikationsänderungen abgeschlossen und in die aktuelle ECMA-Spezifikation aufgenommen werden.

Es kann einige Abweichungen zwischen der Feature-Spezifikation und der abgeschlossenen Implementierung geben. Diese Unterschiede sind in den entsprechenden Hinweisen zum Language Design Meeting (LDM) festgehalten.

Weitere Informationen zur Einführung von Funktionen in den C#-Sprachstandard finden Sie im Artikel zu den Spezifikationen.

Champion Issue: https://github.com/dotnet/csharplang/issues/4665

Zusammenfassung

C# sollte das Definieren von checked-Varianten der folgenden benutzerdefinierten Operatoren unterstützen, damit Benutzer das Überlaufverhalten entsprechend ein- oder ausschalten können.

  • Die unären Operatoren ++ und -- (§12.8.16 und §12.9.6)
  • Der unäre Operator - (§12.9.3)
  • Die binären Operatoren +, -, * und / (§12.10)
  • Explizite Konvertierungsoperatoren.

Motivation

Es gibt keine Möglichkeit für einen Benutzer, einen Typ zu deklarieren und sowohl geprüfte als auch ungeprüfte Versionen eines Operators zu unterstützen. Dies wird es erschweren, verschiedene Algorithmen zu portieren, um die vorgeschlagenen generic math-Schnittstellen zu verwenden, die vom Bibliothekenteam bereitgestellt werden. Um Breaking Changes zu vermeiden, ist es dadurch auch unmöglich, einen Typ wie Int128 oder UInt128 verfügbar zu machen, ohne dass die Sprache gleichzeitig eine eigene entsprechende Unterstützung bietet.

Detaillierter Entwurf

Syntax

Die Grammatik bei Operatoren (§15.10) wird angepasst, um die Angabe des checked-Schlüsselworts nach dem operator-Schlüsselwort direkt vor dem Operatortoken zuzulassen.

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

Zum Beispiel:

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) {...}

Der Kürze halber wird im Folgenden ein Operator mit dem checked-Schlüsselwort als checked operator bezeichnet und ein Operator ohne dieses Schlüsselwort als regular operator. Diese Begriffe sind nicht auf Operatoren anwendbar, die keine checked-Form haben.

Semantik

Ein benutzerdefinierter checked operator soll eine Ausnahme auslösen, wenn das Ergebnis eines Vorgangs zu groß ist, um es im Zieltyp darzustellen. Was es bedeutet, dass ein Ergebnis zu groß ist, hängt von der Art des Zieltyps ab und ist nicht durch die Sprache vorgegeben. In der Regel ist die ausgelöste Ausnahme eine System.OverflowException, die Sprache hat diesbezüglich jedoch keine spezifischen Anforderungen.

Ein benutzerdefinierter regular operator soll keine Ausnahme auslösen, wenn das Ergebnis eines Vorgangs zu groß ist, um es im Zieltyp darzustellen. Stattdessen wird erwartet, dass er eine Instanz zurückgibt, die ein abgeschnittenes Ergebnis darstellt. Was es bedeutet, dass ein Ergebnis zu groß ist und abgeschnitten wird, hängt von der Art des Zieltyps ab und ist nicht durch die Sprache vorgegeben.

Alle vorhandenen benutzerdefinierten Operatoren, die in der checked-Form unterstützt werden, fallen in die Kategorie regular operators. Es ist klar, dass viele von ihnen wahrscheinlich nicht der oben angegebenen Semantik entsprechen, aber zum Zweck der semantischen Analyse wird der Compiler davon ausgehen, dass sie es tun.

Checked- und unchecked-Kontext in einem checked operator im Vergleich

Der checked/unchecked-Kontext im Text eines checked operator wird nicht durch das Vorhandensein des checked-Schlüsselworts beeinflusst. Mit anderen Worten: Der Kontext ist derselbe wie unmittelbar am Anfang der Operatordeklaration. Der Entwickler müsste den Kontext explizit wechseln, wenn sich ein Element seines Algorithmus nicht auf den Standardkontext verlassen kann.

Namen in Metadaten

Abschnitt „I.10.3.1 Unary operators” (Unäre Operatoren) des ECMA-335-Standards wird angepasst, sodass er op_CheckedIncrement, op_CheckedDecrement und op_CheckedUnaryNegation als Namen für Methoden enthält, die die unären checked-Operatoren ++, -- und - implementieren.

Abschnitt „I.10.3.2 Binary operators” (Binäre Operatoren) des ECMA-335-Standards wird angepasst, sodass er op_CheckedAddition, op_CheckedSubtraction, op_CheckedMultiply und op_CheckedDivision als Namen für Methoden enthält, die die binären checked-Operatoren +, -, * und / implementieren.

Abschnitt „I.10.3.3 Conversion operators” (Konvertierungsoperatoren) des ECMA-335-Standards wird angepasst, sodass er op_CheckedExplicit als Namen für eine Methode enthält, die einen expliziten checked-Konvertierungsoperator implementiert.

Unäre Operatoren

Unary checked operators folgt den Regeln aus §15.10.2.

Außerdem erfordert die Deklaration eines checked operator die paarweise Deklaration eines regular operator (der Rückgabetyp sollte dabei ebenfalls übereinstimmen). Andernfalls tritt ein Kompilierfehler auf.

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

Binäre Operatoren

Binäre checked operators folgen den Regeln in §15.10.3.

Außerdem erfordert die Deklaration eines checked operator die paarweise Deklaration eines regular operator (der Rückgabetyp sollte dabei ebenfalls übereinstimmen). Andernfalls tritt ein Kompilierfehler auf.

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

Mögliche benutzerdefinierte Operatoren

Der Abschnitt „Mögliche benutzerdefinierte Operatoren” (§12.4.6) wird wie folgt angepasst (Ergänzungen/Änderungen sind fett formatiert).

Bei einem Typ T und einem Vorgang operator op(A), wobei op ein überladbarer Operator und A eine Argumentliste ist, wird die Gruppe möglicher benutzerdefinierter Operatoren, die T für operator op(A) bereitstellt, wie folgt bestimmt:

  • Bestimmen Sie den Typ T0. Wenn T ein nullbarer Typ ist, ist T0 sein zugrunde liegender Typ, andernfalls ist T0 gleich T.
  • Finden Sie das Set der benutzerdefinierten Operatoren, U. Diese Gruppe besteht aus Folgendem:
    • Im unchecked-Auswertungskontext aus allen regulären operator op-Deklarationen in T0
    • Im checked-Auswertungskontext aus allen geprüften (checked) und regulären operator op-Deklarationen in T0 mit Ausnahme von regulären Deklarationen, die über eine paarweise übereinstimmende Deklaration eines checked operator verfügen
  • Für alle operator op-Deklarationen in U und alle aufgehobenen („lifted”) Formen solcher Operatoren besteht die Gruppe möglicher Operatoren aus allen anwendbaren Operatoren in , sofern mindestens ein Operator anwendbar ist (A) in Bezug auf die Argumentliste T0.
  • Andernfalls, wenn es sich bei T0 um object handelt, ist die Gruppe der möglichen Operatoren leer.
  • Andernfalls ist die Gruppe möglicher Operatoren, die T0 bereitstellt, die von der direkten Basisklasse von T0 oder der effektiven Basisklasse von T0 bereitgestellte Gruppe möglicher Operatoren, sofern T0 ein Typparameter ist.

Ähnliche Regeln werden beim Bestimmen der Gruppe möglicher Operatoren in Schnittstellen angewendet (https://github.com/dotnet/csharplang/blob/main/meetings/2017/LDM-2017-06-27.md#shadowing-within-interfaces).

Der Abschnitt §12.8.20 wird angepasst, um die Auswirkung widerzuspiegeln, die der checked/unchecked-Kontext auf die Überladungsauflösung für unäre und binäre Operatoren hat.

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

Beispiel 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
{
}

Beispiel 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
{
}

Konvertierungsoperatoren

Die checked operators für die Konvertierung folgen den Regeln in §15.10.4.

Die Deklaration eines checked operator erfordert jedoch eine paarweise Deklaration eines regular operator. Andernfalls tritt ein Kompilierfehler auf.

Der folgende Abschnitt

Die Signatur eines Konvertierungsoperators besteht aus dem Quelltyp und dem Zieltyp. (Dies ist die einzige Memberform, bei der der Rückgabetyp in der Signatur enthalten ist.) Die implizite oder explizite Klassifizierung eines Konvertierungsoperators ist nicht Teil der Signatur des Operators. Daher kann eine Klasse oder ein Struct nicht sowohl einen impliziten als auch einen expliziten Konvertierungsoperator mit demselben Quell- und Zieltyp deklarieren.

wird angepasst, um einem Typ die Möglichkeit zu bieten, geprüfte und reguläre Formen von expliziten Konversionen mit demselben Quell- und Zieltyp zu deklarieren. Ein Typ wird nicht die Möglichkeit bieten, sowohl einen impliziten als auch einen geprüften expliziten Konvertierungsoperator mit demselben Quell- und Zieltyp zu deklarieren.

Verarbeitung von benutzerdefinierten expliziten Konvertierungen

Der dritte Punkt in §10.5.5:

  • Ermitteln Sie die Gruppe anwendbarer benutzerdefinierter und gelifteter Umwandlungsoperatoren, U. Diese Gruppe besteht aus den von den Klassen oder Strukturen in D deklarierten benutzerdefinierten und aufgehobenen impliziten oder expliziten Konvertierungsoperatoren, die einen Typ, der S umfasst oder darin enthalten ist, in einen Typ konvertieren, der T umfasst oder darin enthalten ist. Wenn U leer ist, ist die Konversion undefiniert und es tritt ein Kompilierfehler auf.

wird durch die folgenden Punkte ersetzt:

  • Ermitteln Sie die Gruppe von Konvertierungsoperatoren, U0. Diese Gruppe besteht aus Folgendem:
    • Im unchecked-Auswertungskontext aus den benutzerdefinierten impliziten oder regulären expliziten Konvertierungsoperatoren, die von den Klassen oder Strukturen in D deklariert werden
    • Im checked-Auswertungskontext aus den benutzerdefinierten impliziten oder expliziten regulären/checked-Konvertierungsoperatoren, die von den Klassen oder Strukturen in D deklariert werden, mit Ausnahme von expliziten regulären Konvertierungsoperatoren, die über eine paarweise übereinstimmende Deklaration eines checked operator innerhalb desselben deklarierenden Typs verfügen
  • Ermitteln Sie die Gruppe anwendbarer benutzerdefinierter und gelifteter Umwandlungsoperatoren, U. Diese Gruppe besteht aus den benutzerdefinierten und aufgehobenen impliziten oder expliziten Konvertierungsoperatoren in U0, die einen Typ, der S umfasst oder darin enthalten ist, in einen Typ konvertieren, der T umfasst oder darin enthalten ist. Wenn U leer ist, ist die Konversion undefiniert und es tritt ein Kompilierfehler auf.

Der Abschnitt „Checked- und Unchecked-Operatoren” (§11.8.20) wird angepasst, um die Auswirkung widerzuspiegeln, die der checked/unchecked-Kontext auf die Verarbeitung von benutzerdefinierten expliziten Konvertierungen hat.

Implementieren von Operatoren

Ein checked operator implementiert keinen regular operator und umgekehrt.

Linq-Ausdrucksbaumstruktur

Checked operators werden Linq-Ausdrucksbaumstrukturen unterstützen. Ein UnaryExpression/BinaryExpression-Knoten wird mit entsprechender MethodInfo erstellt. Die folgenden Factorymethoden werden verwendet:

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

Beachten Sie, dass C# keine Zuweisungen in Ausdrucksbaumstrukturen unterstützt. Daher wird das geprüfte (checked) Inkrementieren/Dekrementieren ebenfalls nicht unterstützt.

Es gibt keine Factorymethode für die geprüfte (checked) Division. Diesbezüglich besteht eine offene Frage – Geprüfte (checked) Division in Linq-Ausdrucksbaumstrukturen.

Dynamisch

Wir werden die Kosten für das Hinzufügen von Unterstützung für checked-Operatoren in dynamischen Aufrufen in CoreCLR prüfen und diese implementieren, sofern die Kosten nicht zu hoch sind. Dies ist ein Zitat aus https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-02-09.md.

Nachteile

Dies erhöht die Komplexität der Sprache und ermöglicht es Benutzern, weitere Arten von Breaking Changes an ihren Typen vorzunehmen.

Alternativen

Die generischen mathematischen Schnittstellen, die die Bibliotheken planen, könnten benannte Methoden (wie z. B. AddChecked) zur Verfügung stellen. Der primäre Nachteil ist, dass dies weniger lesbar/wartbar ist und nicht von den Prioritätsregeln der Sprache für Operatoren profitiert.

In diesem Abschnitt werden Alternativen erörtert, aber nicht implementiert.

Platzierung des checked-Schlüsselworts

Alternativ kann das Schlüsselwort checked an die Stelle direkt vor dem Schlüsselwort operator verschoben werden.

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) {...}

Alternativ könnte es in die Gruppe der Operatormodifizierer verschoben werden:

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-Schlüsselwort

Es gab Vorschläge, das unchecked-Schlüsselwort mit den folgenden möglichen Bedeutungen an derselben Position wie das checked-Schlüsselwort zu unterstützen:

  • Um einfach explizit anzugeben, dass es sich um einen regulären Operator handelt, oder
  • Um ggf. eine bestimmte Variante eines Operators anzugeben, der in einem unchecked-Kontext verwendet wird Die Sprache kann op_Addition, op_CheckedAddition und op_UncheckedAddition unterstützen, um die Anzahl von Breaking Changes zu begrenzen. Dadurch wird eine weitere Schicht an Komplexität hinzugefügt, die in den meisten Fällen nicht notwendig ist.

Operatornamen in ECMA-335

Alternativ könnten die Operatornamen op_UnaryNegationChecked, op_AdditionChecked, op_SubtractionChecked, op_MultiplyChecked und op_DivisionChecked lauten, mit Checked am Ende. Es scheint sich jedoch bereits das Muster herausgebildet zu haben, das Operatorwort am Ende des Namens zu platzieren. Beispielsweise gibt es einen op_UnsignedRightShift-Operator und keinen op_RightShiftUnsigned-Operator.

Checked operators sind in einem unchecked-Kontext nicht anwendbar.

Der Compiler könnte bei der Membersuche zum Ermitteln möglicher benutzerdefinierter Operatoren innerhalb eines unchecked-Kontexts checked operators ignorieren. Wenn Metadaten gefunden werden, die nur checked operator definieren, wird ein Kompilierungsfehler auftreten.

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

Komplexere Regeln für die Operatorsuche und Überladungsauflösung in einem checked-Kontext

Der Compiler berücksichtigt bei der Membersuche zum Ermitteln möglicher benutzerdefinierter Operatoren innerhalb eines checked-Kontexts auch anwendbare Operatoren, die mit Checked enden. Wenn der Compiler versuchte, die anwendbaren Funktionsmitglieder für den binären Additionsoperator zu finden, würde er sowohl nach op_Addition als auch nach op_AdditionCheckedsuchen. Wenn der einzige anwendbare Funktionsmember ein checked operator ist, wird er verwendet. Wenn sowohl ein regular operator als auch ein checked operator existieren und gleichermaßen anwendbar sind, wird das checked operator bevorzugt. Wenn sowohl ein regular operator als auch ein checked operator vorhanden sind, aber der regular operator eine genaue Übereinstimmung ist und der checked operator nicht, bevorzugt der Compiler den 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);
}

Eine weitere Möglichkeit zum Erstellen der Gruppe möglicher benutzerdefinierter Operatoren

Überladungsauflösung für unäre Operatoren

Wenn der regular operator mit dem unchecked-Auswertungskontext übereinstimmt, der checked operator dem checked-Auswertungskontext entspricht und ein Operator, der nicht die checked-Form aufweist (z. B. +), mit beiden Kontexten übereinstimmt, wird der erste Punkt in §12.4.4 – Überladungsauflösung für unäre Operatoren:

durch die folgenden zwei Punkte ersetzt:

  • Die von X für den Vorgang operator op(x) bereitgestellte Gruppe möglicher benutzerdefinierter Operatoren, die dem aktuellen checked/unchecked-Kontext entsprechen, wird anhand der Regeln in Mögliche benutzerdefinierte Operatoren bestimmt.
  • Wenn das Set der in Frage kommenden benutzerdefinierten Operatoren nicht leer ist, wird dieses Set zum Set der in Frage kommenden Operatoren für die Operation. Andernfalls wird die von X für den Vorgang operator op(x) bereitgestellte Gruppe möglicher benutzerdefinierter Operatoren, die dem entgegengesetzten checked/unchecked-Kontext entsprechen, anhand der Regeln in §12.4.6 – Mögliche benutzerdefinierte Operatoren bestimmt.

Überladungsauflösung für binäre Operatoren

Wenn der regular operator mit dem unchecked-Auswertungskontext übereinstimmt, der checked operator dem checked-Auswertungskontext entspricht und ein Operator, der keine checked-Form aufweist (z. B. %), mit beiden Kontexten übereinstimmt, wird der erste Punkt in §12.4.5 – Überladungsauflösung für binäre Operatoren:

  • Die von X und Y bereitgestellte Gruppe möglicher benutzerdefinierter Operatoren für den Vorgang operator op(x,y) wird bestimmt. Die Gruppe besteht aus der Vereinigung der von X und von Y bereitgestellten möglichen Operatoren, die jeweils durch die Regeln in §12.4.6 – Mögliche benutzerdefinierte Operatoren bestimmt werden. Wenn X und Y vom gleichen Typ sind oder wenn X und Y von einem gemeinsamen Basistyp abgeleitet sind, dann kommen gemeinsame Kandidatenoperatoren nur einmal im kombinierten Set vor.

durch die folgenden zwei Punkte ersetzt:

  • Die von X und Y bereitgestellte Gruppe möglicher benutzerdefinierter Operatoren für den Vorgang operator op(x,y), die dem aktuellen checked/unchecked-Kontext entsprechen, wird bestimmt. Die Gruppe besteht aus der Vereinigung der von X und von Y bereitgestellten möglichen Operatoren, die jeweils durch die Regeln in §12.4.6 – Mögliche benutzerdefinierte Operatoren bestimmt werden. Wenn X und Y vom gleichen Typ sind oder wenn X und Y von einem gemeinsamen Basistyp abgeleitet sind, dann kommen gemeinsame Kandidatenoperatoren nur einmal im kombinierten Set vor.
  • Wenn das Set der in Frage kommenden benutzerdefinierten Operatoren nicht leer ist, wird dieses Set zum Set der in Frage kommenden Operatoren für die Operation. Andernfalls wird die von X und Y bereitgestellte Gruppe möglicher benutzerdefinierter Operatoren für den Vorgang operator op(x,y), die dem entgegengesetzten checked/unchecked-Kontext entsprechen, bestimmt. Die Gruppe besteht aus der Vereinigung der von X und von Y bereitgestellten möglichen Operatoren, die jeweils durch die Regeln in §12.4.6 – Mögliche benutzerdefinierte Operatoren bestimmt werden. Wenn X und Y vom gleichen Typ sind oder wenn X und Y von einem gemeinsamen Basistyp abgeleitet sind, dann kommen gemeinsame Kandidatenoperatoren nur einmal im kombinierten Set vor.
Beispiel 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);
}
Beispiel 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
{
}
Beispiel 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
{
}
Beispiel 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();
}
Beispiel 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();
}

Verarbeitung von benutzerdefinierten expliziten Konvertierungen

Wenn der regular operator mit dem unchecked-Auswertungskontext übereinstimmt und der checked operator dem checked-Auswertungskontext entspricht, wird der dritte Punkt in §10.5.3 Auswertung von benutzerdefinierten Konvertierungen:

  • Ermitteln Sie die Gruppe anwendbarer benutzerdefinierter und gelifteter Umwandlungsoperatoren, U. Diese Gruppe besteht aus den von den Klassen oder Strukturen in D deklarierten benutzerdefinierten und aufgehobenen impliziten oder expliziten Konvertierungsoperatoren, die einen Typ, der S umfasst oder darin enthalten ist, in einen Typ konvertieren, der T umfasst oder darin enthalten ist. Wenn U leer ist, ist die Konversion undefiniert und es tritt ein Kompilierfehler auf.

wird durch die folgenden Punkte ersetzt:

  • Ermitteln Sie die Gruppe anwendbarer benutzerdefinierter und aufgehobener expliziter Konvertierungsoperatoren, die dem aktuellen checked/unchecked-Kontext entsprechen, U0. Diese Gruppe besteht aus den von den Klassen oder Strukturen in D deklarierten benutzerdefinierten und aufgehobenen expliziten Konvertierungsoperatoren, die dem aktuellen checked/unchecked-Kontext entsprechen und einen Typ, der S umfasst oder darin enthalten ist, in einen Typ konvertieren, der T umfasst oder darin enthalten ist.
  • Ermitteln Sie die Gruppe anwendbarer benutzerdefinierter und aufgehobener expliziter Konvertierungsoperatoren, die dem entgegengesetzten checked/unchecked-Kontext entsprechen, U1. Wenn U0 nicht leer ist, ist U1 leer. Andernfalls besteht diese Gruppe aus den von den Klassen oder Strukturen in D deklarierten benutzerdefinierten und aufgehobenen expliziten Konvertierungsoperatoren, die dem entgegengesetzten checked/unchecked-Kontext entsprechen und einen Typ, der S umfasst oder darin enthalten ist, in einen Typ konvertieren, der T umfasst oder darin enthalten ist.
  • Ermitteln Sie die Gruppe anwendbarer benutzerdefinierter und gelifteter Umwandlungsoperatoren, U. Diese Gruppe besteht aus Operatoren aus U0 und U1 sowie den von den Klassen oder Strukturen in D deklarierten benutzerdefinierten und aufgehobenen impliziten Konvertierungsoperatoren, die einen Typ, der S umfasst oder darin enthalten ist, in einen Typ konvertieren, der T umfasst oder darin enthalten ist. Wenn U leer ist, ist die Konversion undefiniert und es tritt ein Kompilierfehler auf.

Eine weitere Möglichkeit zum Erstellen der Gruppe möglicher benutzerdefinierter Operatoren

Überladungsauflösung für unäre Operatoren

Der erste Punkt in Abschnitt §12.4.4 wird wie folgt angepasst (Ergänzungen sind fett formatiert).

  • Der Satz von benutzerdefinierten Kandidatenoperatoren, die von X für den Vorgang operator op(x) bereitgestellt werden, wird anhand der im Abschnitt "Benutzerdefinierte Kandidatenoperatoren" aufgeführten Regeln bestimmt. Wenn die Gruppe mindestens einen Operator in der geprüften (checked) Form enthält, werden alle Operatoren in der regulären Form aus der Gruppe entfernt.

Der Abschnitt §12.8.20 wird angepasst, um die Auswirkung widerzuspiegeln, die der checked/unchecked-Kontext auf die Überladungsauflösung für unäre Operatoren hat.

Überladungsauflösung für binäre Operatoren

Der erste Punkt in Abschnitt §12.4.5 wird wie folgt angepasst (Ergänzungen sind fett formatiert).

  • Die von X und Y bereitgestellte Gruppe möglicher benutzerdefinierter Operatoren für den Vorgang operator op(x,y) wird bestimmt. Die Gruppe besteht aus der Vereinigung der von X und von Y bereitgestellten möglichen Operatoren, die jeweils anhand der Regeln im Abschnitt „Mögliche benutzerdefinierte Operatoren” unten bestimmt werden. Wenn X und Y vom gleichen Typ sind oder wenn X und Y von einem gemeinsamen Basistyp abgeleitet sind, dann kommen gemeinsame Kandidatenoperatoren nur einmal im kombinierten Set vor. Wenn die Gruppe mindestens einen Operator in der geprüften (checked) Form enthält, werden alle Operatoren in der regulären Form aus der Gruppe entfernt.

Der Abschnitt „Checked- und Unchecked-Operatoren” (§12.8.20) wird angepasst, um die Auswirkung widerzuspiegeln, die der checked/unchecked-Kontext auf die Überladungsauflösung für binäre Operatoren hat.

Mögliche benutzerdefinierte Operatoren

Der Abschnitt §12.4.6 – Mögliche benutzerdefinierte Operatoren wird wie folgt angepasst (Ergänzungen sind fett formatiert).

Bei einem Typ T und einem Vorgang operator op(A), wobei op ein überladbarer Operator und A eine Argumentliste ist, wird die Gruppe möglicher benutzerdefinierter Operatoren, die T für operator op(A) bereitstellt, wie folgt bestimmt:

  • Bestimmen Sie den Typ T0. Wenn T ein nullbarer Typ ist, ist T0 sein zugrunde liegender Typ, andernfalls ist T0 gleich T.
  • Für alle operator op-Deklarationen in der geprüften (checked) und regulären Form im checked-Auswertungskontext und nur in der regulären Form im unchecked-Auswertungskontext in T0 sowie alle aufgehobenen Formen solcher Operatoren besteht die Gruppe möglicher Operatoren aus allen anwendbaren Operatoren in , sofern mindestens ein Operator anwendbar ist (A) in Bezug auf die Argumentliste T0.
  • Andernfalls, wenn es sich bei T0 um object handelt, ist die Gruppe der möglichen Operatoren leer.
  • Andernfalls ist die Gruppe möglicher Operatoren, die T0 bereitstellt, die von der direkten Basisklasse von T0 oder der effektiven Basisklasse von T0 bereitgestellte Gruppe möglicher Operatoren, sofern T0 ein Typparameter ist.

Eine ähnliche Filterung wird beim Bestimmen der Gruppe möglicher Operatoren in Schnittstellen angewendet (https://github.com/dotnet/csharplang/blob/main/meetings/2017/LDM-2017-06-27.md#shadowing-within-interfaces).

Der Abschnitt §12.8.20 wird angepasst, um die Auswirkung des checked/unchecked-Kontexts auf die Überladungsauflösung für unäre und binäre Operatoren widerzuspiegeln.

Beispiel 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);
}
Beispiel 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
{
}
Beispiel 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
{
}
Beispiel 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();
}
Beispiel 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();
}

Verarbeitung von benutzerdefinierten expliziten Konvertierungen

Der dritte Punkt in §10.5.5:

  • Ermitteln Sie die Gruppe anwendbarer benutzerdefinierter und gelifteter Umwandlungsoperatoren, U. Diese Gruppe besteht aus den von den Klassen oder Strukturen in D deklarierten benutzerdefinierten und aufgehobenen impliziten oder expliziten Konvertierungsoperatoren, die einen Typ, der S umfasst oder darin enthalten ist, in einen Typ konvertieren, der T umfasst oder darin enthalten ist. Wenn U leer ist, ist die Konversion undefiniert und es tritt ein Kompilierfehler auf.

wird durch die folgenden Punkte ersetzt:

  • Ermitteln Sie die Gruppe anwendbarer benutzerdefinierter und aufgehobener expliziter Konvertierungsoperatoren, U0. Diese Gruppe besteht aus den von den Klassen oder Strukturen in D deklarierten benutzerdefinierten und aufgehobenen expliziten Konvertierungsoperatoren in der geprüften (checked) und regulären Form im checked-Auswertungskontext und nur in der regulären Form im unchecked-Auswertungskontext, die einen Typ, der S umfasst oder darin enthalten ist, in einen Typ konvertieren, der T umfasst oder darin enthalten ist.
  • Wenn U0 mindestens einen Operator in der geprüften (checked) Form enthält, werden alle Operatoren in der regulären Form aus der Gruppe entfernt.
  • Ermitteln Sie die Gruppe anwendbarer benutzerdefinierter und gelifteter Umwandlungsoperatoren, U. Diese Gruppe besteht aus Operatoren aus U0 und den von den Klassen oder Strukturen in D deklarierten benutzerdefinierten und aufgehobenen impliziten Konvertierungsoperatoren, die einen Typ, der S umfasst oder darin enthalten ist, in einen Typ konvertieren, der T umfasst oder darin enthalten ist. Wenn U leer ist, ist die Konversion undefiniert und es tritt ein Kompilierfehler auf.

Der Abschnitt „Checked- und Unchecked-Operatoren” (§12.8.20) wird angepasst, um die Auswirkung widerzuspiegeln, die der checked/unchecked-Kontext auf die Verarbeitung von benutzerdefinierten expliziten Konvertierungen hat.

Checked- und unchecked-Kontext in einem checked operator im Vergleich

Der Compiler kann den Standardkontext eines checked operator als „checked” (geprüft) behandeln. Entwickler müssen unchecked explizit verwenden, wenn ein Teil ihres Algorithmus nicht am checked context teilnehmen soll. Dies könnte jedoch in Zukunft nicht gut funktionieren, wenn wir anfangen, die Verwendung von checked/unchecked-Token als Modifizierer für Operatoren zuzulassen, um den Kontext im Text festzulegen. Der Modifikator und das Schlüsselwort können sich widersprechen. Außerdem wäre dies (das Behandeln des Standardkontexts als „unchecked”) für einen regular operator ebenfalls nicht möglich, da dies ein Breaking Change wäre.

Ungelöste Fragen

Sollte die Sprache checked- und unchecked-Modifizierer für Methoden zulassen (z. B. static checked void M())? Dies würde es ermöglichen, Schachtelungsebenen für Methoden zu entfernen, die diese erfordern.

Geprüfte (checked) Division in Linq-Ausdrucksbaumstrukturen

Es gibt keine Factorymethode zum Erstellen eines geprüften (checked) Divisionsknotens und keinen ExpressionType.DivideChecked-Member. Wir könnten weiterhin die folgende Factorymethode verwenden, um einen regulären Divisionsknoten mit MethodInfo zu erstellen, der auf die op_CheckedDivision-Methode zeigt. Die Consumer müssen den Namen überprüfen, um den Kontext abzuleiten.

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

Beachten Sie, dass in Abschnitt §12.8.20 der /-Operator zwar als einer der Operatoren aufgeführt ist, die vom checked/unchecked-Auswertungskontext betroffen sind, die Zwischensprache (Intermediate Language, IL) aber nicht über einen speziellen Op-Code zum Durchführen einer geprüften (checked) Division verfügt. Der Compiler verwendet unabhängig vom aktuellen Kontext immer die Factorymethode.

Vorschlag: Die geprüfte (checked) benutzerdefinierte Division wird in Linq-Ausdrucksbaumstrukturen nicht unterstützt.

(Gelöst) Sollten implizite checked-Konvertierungsoperatoren unterstützt werden?

Im Allgemeinen sollten implizite Konvertierungsoperatoren keine Ausnahme auslösen.

Vorschlag: Nein

Lösung: Genehmigt – https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-02-07.md#checked-implicit-conversions

Design-Meetings

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