次の方法で共有


ユーザー定義複合代入演算子

この記事は機能仕様です。 仕様は、機能の設計ドキュメントとして機能します。 これには、提案された仕様の変更と、機能の設計と開発時に必要な情報が含まれます。 これらの記事は、提案された仕様の変更が最終決定され、現在の ECMA 仕様に組み込まれるまで公開されます。

機能の仕様と完成した実装の間には、いくつかの違いがある可能性があります。 これらの違いは、関連する 言語設計会議 (LDM) ノートでキャプチャされます。

機能仕様を C# 言語標準に導入するプロセスの詳細については、仕様に関する記事を参照してください。

チャンピオン号: https://github.com/dotnet/csharplang/issues/9101

概要

割り当てのターゲットがインプレースで変更されるように、複合代入演算子の動作をユーザーの型でカスタマイズできるようにします。

モチベーション

C# では、ユーザー定義型の開発者オーバーロード演算子の実装がサポートされています。 さらに、ユーザーがx += yではなく、x = x + yと同様にコードを記述できるようにする "複合代入演算子" のサポートも提供します。 ただし、現在、この言語では、開発者がこれらの複合代入演算子をオーバーロードすることは許可されていませんが、既定の動作では適切な処理が行われますが、特に不変の値型に関連するため、常に "最適" であるとは限りません。

次の例を考えると

class C1
{
    static void Main()
    {
        var c1 = new C1();
        c1 += 1;
        System.Console.Write(c1);
    }
    
    public static C1 operator+(C1 x, int y) => new C1();
}

現在の言語規則では、複合代入演算子 c1 += 1 はユーザー定義の + 演算子を呼び出し、その戻り値をローカル変数 c1に割り当てます。 演算子の実装では、 C1の新しいインスタンスを割り当てて返す必要があることに注意してください。一方、コンシューマーの観点からは、 C1 の元のインスタンスに対するインプレース変更は、代わりに適切に機能します (割り当て後には使用されません)。追加の割り当てを回避するという利点があります。

プログラムが複合代入演算を利用する場合、最も一般的な効果は、元の値が "失われた" ものであり、プログラムで使用できなくなるということです。 データが大きい型 (BigInteger、Tensors など) では、新しいコピー先の生成、反復処理、メモリのコピーにかかるコストはかなり高くなる傾向があります。 インプレース変異を使用すると、多くの場合、この費用をスキップできるため、このようなシナリオが大幅に改善される可能性があります。

そのため、C# では、ユーザー型が複合代入演算子の動作をカスタマイズし、それ以外の場合は割り当てとコピーが必要になるシナリオを最適化することが有益な場合があります。

詳細な設計

構文

https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/classes.md#15101-generalでの文法は次のように調整されます。

演算子は、 operator_declarationを使用して宣言されます。

operator_declaration
    : attributes? operator_modifier+ operator_declarator operator_body
    ;

operator_modifier
    : 'public'
    | 'static'
    | 'extern'
    | unsafe_modifier   // unsafe code support
    | 'abstract'
    | 'virtual'
    | 'sealed'
+   | 'override'
+   | 'new'
+   | 'readonly'
    ;

operator_declarator
    : unary_operator_declarator
    | binary_operator_declarator
    | conversion_operator_declarator
+   | increment_operator_declarator
+   | compound_assignment_operator_declarator
    ;

unary_operator_declarator
    : type 'operator' overloadable_unary_operator '(' fixed_parameter ')'
    ;

logical_negation_operator
    : '!'
    ;

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

binary_operator_declarator
    : type 'operator' overloadable_binary_operator
        '(' fixed_parameter ',' fixed_parameter ')'
    ;

overloadable_binary_operator
    : 'checked'? '+'  | 'checked'? '-'  | 'checked'? '*'  | 'checked'? '/'  | '%'  | '&' | '|' | '^'  | '<<'
    | right_shift | '==' | '!=' | '>' | '<' | '>=' | '<='
    ;

conversion_operator_declarator
    : 'implicit' 'operator' type '(' fixed_parameter ')'
    | 'explicit' 'operator' type '(' fixed_parameter ')'
    ;

+increment_operator_declarator
+   : type 'operator' overloadable_increment_operator '(' fixed_parameter ')'
+   | 'void' 'operator' overloadable_increment_operator '(' ')'
+   ;

+overloadable_increment_operator
+   : 'checked'? '++' | 'checked'? '--'
+    ;

+compound_assignment_operator_declarator
+   : 'void' 'operator' overloadable_compound_assignment_operator
+       '(' fixed_parameter ')'
+   ;

+overloadable_compound_assignment_operator
+   : 'checked'? '+=' | 'checked'? '-=' | 'checked'? '*=' | 'checked'? '/=' | '%=' | '&=' | '|=' | '^=' | '<<='
+   | right_shift_assignment
+   | unsigned_right_shift_assignment
+   ;

operator_body
    : block
    | '=>' expression ';'
    | ';'
    ;

オーバーロード可能な演算子には、 非演算子binary 演算子変換演算子increment 演算子compound 代入演算子の 5 つのカテゴリがあります

次の規則は、すべての演算子宣言に適用されます。

  • 演算子宣言には、bothpublicstatic修飾子を含める必要があります。

複合代入演算子とインスタンスインクリメント演算子は、基底クラスで宣言された演算子を非表示にすることができます。 したがって、次の段落は正確ではなくなり、それに応じて調整するか、削除することができます。

演算子宣言では、演算子が宣言されているクラスまたは構造体が常に演算子のシグネチャに参加する必要があるため、派生クラスで宣言された演算子が基底クラスで宣言された演算子を非表示にすることはできません。 したがって、演算子宣言では、 new 修飾子は必要ないため、許可されません。

単項演算子

https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/classes.md#15102-unary-operatorsを参照してください。

演算子宣言には static 修飾子を含める必要があり、 override 修飾子は含めないものとします。

次の箇条書きが削除されます。

  • 単項 ++ または -- 演算子は、 T 型または T? 型の 1 つのパラメーターを受け取り、同じ型またはそこから派生した型を返す必要があります。

次の段落は、 ++ および -- 演算子トークンについて言及しなくなったものに調整されています。

単項演算子のシグネチャは、演算子トークン (+-!~++--true、または false) と単一パラメーターの型で構成されます。 戻り値の型は、単項演算子のシグネチャの一部ではなく、パラメーターの名前でもありません。

セクションの例は、ユーザー定義インクリメント演算子を使用しないように調整する必要があります。

バイナリ演算子

https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/classes.md#15103-binary-operatorsを参照してください。

演算子宣言には static 修飾子を含める必要があり、 override 修飾子は含めないものとします。

変換演算子

https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/classes.md#15104-conversion-operatorsを参照してください。

演算子宣言には static 修飾子を含める必要があり、 override 修飾子は含めないものとします。

インクリメント演算子

次の規則は、静的インクリメント演算子の宣言に適用されます。 T は、演算子宣言を含むクラスまたは構造体のインスタンス型を表します。

  • 演算子宣言には static 修飾子を含める必要があり、 override 修飾子は含めないものとします。
  • 演算子は、 T 型または T? 型の 1 つのパラメーターを受け取り、同じ型またはそこから派生した型を返す必要があります。

静的インクリメント演算子のシグネチャは、演算子トークン ('checked'? ++,'checked'? --) と単一パラメーターの型で構成されます。 戻り値の型は、静的インクリメント演算子のシグネチャの一部ではなく、パラメーターの名前でもありません。

静的インクリメント演算子は、 非項演算子とよく似ています

インスタンスインクリメント演算子の宣言には、次の規則が適用されます。

  • 演算子宣言には、 static 修飾子を含めてはならない。
  • 演算子はパラメーターを受け取らなくなります。
  • 演算子には戻り値 void 型を持つ必要があります。

実際には、インスタンスインクリメント演算子は、パラメーターがなく、メタデータに特別な名前を持つインスタンス メソッドを返す void です。

インスタンスインクリメント演算子のシグネチャは、演算子トークン ('checked'? '++' | 'checked'? '--') で構成されます。

checked operator宣言には、regular operatorのペアごとの宣言が必要です。 それ以外の場合は、コンパイル時エラーが発生します。 https://github.com/dotnet/csharplang/blob/main/proposals/csharp-11.0/checked-user-defined-operators.md#semantics も参照してください。

このメソッドの目的は、インスタンスの値を、要求されたインクリメント操作の結果に合わせて調整することです。つまり、宣言する型のコンテキストで意味するものは何でもします。

例:

class C1
{
    public int Value;

    public void operator ++()
    {
        Value++;
    }
}

インスタンスインクリメント演算子は、基底クラスで宣言されたのと同じシグネチャを持つ演算子をオーバーライドできます。この目的には override 修飾子を使用できます。

インクリメント演算子/デクリメント演算子のインスタンス バージョンをサポートするため、次の「予約済み」の特殊名を ECMA-335 に追加する必要があります: | Name | Operator | | -----| -------- | |op_DecrementAssignment| -- | |op_IncrementAssignment| ++ | |op_CheckedDecrementAssignment| checked -- | |op_CheckedIncrementAssignment| checked ++ |

複合代入演算子

複合代入演算子の宣言には、次の規則が適用されます。

  • 演算子宣言には、 static 修飾子を含めてはならない。
  • 演算子は、1 つのパラメーターを受け取ります。
  • 演算子には戻り値 void 型を持つ必要があります。

実際には、複合代入演算子は、1 つのパラメーターを受け取り、メタデータに特別な名前を持つ void 戻りインスタンス メソッドです。

複合代入演算子のシグネチャは、演算子トークン ('checked'? '+='、'checked'? '-='、'checked'? '*='、'checked'? '/='、'%='、'&='、'|='、'^='、'<<='、right_shift_assignment、unsigned_right_shift_assignment) 及び単一パラメーターの型で構成されています。 パラメーターの名前は、複合代入演算子のシグネチャの一部ではありません。

checked operator宣言には、regular operatorのペアごとの宣言が必要です。 それ以外の場合は、コンパイル時エラーが発生します。 https://github.com/dotnet/csharplang/blob/main/proposals/csharp-11.0/checked-user-defined-operators.md#semantics も参照してください。

このメソッドの目的は、インスタンスの値を <instance> <binary operator token> parameterの結果に合わせて調整することです。

例:

class C1
{
    public int Value;

    public void operator +=(int x)
    {
        Value+=x;
    }
}

複合代入演算子は、基底クラスで宣言されたのと同じシグネチャを持つ演算子をオーバーライドできます。この目的には、 override 修飾子を使用できます。

ECMA-335 は、ユーザー定義インクリメント演算子に対して次の特別名を既に「予約」しています: | Name | Operator | | -----| -------- | |op_AdditionAssignment|'+=' | |op_SubtractionAssignment|'-=' | |op_MultiplicationAssignment|'*=' | |op_DivisionAssignment|'/=' | |op_ModulusAssignment|'%=' | |op_BitwiseAndAssignment|'&=' | |op_BitwiseOrAssignment|'|=' | |op_ExclusiveOrAssignment|'^=' | |op_LeftShiftAssignment|'<<='| |op_RightShiftAssignment| right_shift_assignment| |op_UnsignedRightShiftAssignment|unsigned_right_shift_assignment|

ただし、CLS 準拠では、演算子メソッドが 2 つのパラメーターを持つ非 void 静的メソッドである必要があります。つまり、C# 二項演算子と一致します。 演算子が単一のパラメーターを持つインスタンス メソッドを返す無効にできるように、CLS コンプライアンス要件を緩和することを検討する必要があります。

チェックされたバージョンの演算子をサポートするには、次の名前を追加する必要があります: | Name | Operator | | -----| -------- | |op_CheckedAdditionAssignment| checked '+=' | |op_CheckedSubtractionAssignment| checked '-=' | |op_CheckedMultiplicationAssignment| checked '*=' | |op_CheckedDivisionAssignment| checked '/=' |

前置インクリメント演算子と前置デクリメント演算子

https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/expressions.md#1296-prefix-increment-and-decrement-operators を参照してください

x«op» xが変数として分類され、新しい言語バージョンが対象の場合、次のようにインクリメント演算子が優先されます

最初に、 instance インクリメント演算子のオーバーロード解決を適用して、操作を処理しようとします。 プロセスが結果を生成せず、エラーが発生しない場合、現在指定されている単項演算子のオーバーロード解決 https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/expressions.md#1296-prefix-increment-and-decrement-operators 適用することで、操作が処理されます。

それ以外の場合、操作 «op»x は次のように評価されます。

xの型が参照型であることがわかっている場合、xはインスタンスx₀を取得するために評価され、そのインスタンスに対して演算子メソッドが呼び出され、操作の結果としてx₀が返されます。 x₀null場合、演算子メソッドの呼び出しは NullReferenceException をスローします。

例えば次が挙げられます。

var a = ++(new C()); // error: not a variable
var b = ++a; // var temp = a; temp.op_Increment(); b = temp; 
++b; // b.op_Increment();
var d = ++C.P1; // error: setter is missing
++C.P1; // error: setter is missing
var e = ++C.P2; // var temp = C.op_Increment(C.get_P2()); C.set_P2(temp); e = temp;
++C.P2; // var temp = C.op_Increment(C.get_P2()); C.set_P2(temp);

class C
{
    public static C P1 { get; } = new C();
    public static C P2 { get; set; } = new C();

    public static C operator ++(C x) => ...;
    public void operator ++() => ...;
}

xの型が参照型であることが不明な場合:

  • インクリメントの結果が使用されると、 x はインスタンス x₀を取得するために評価され、そのインスタンスに対して演算子メソッドが呼び出され、 x₀x に割り当てられ、複合代入の結果として x₀ が返されます。
  • それ以外の場合は、 xで演算子メソッドが呼び出されます。

xの副作用は、プロセスで 1 回だけ評価されることに注意してください。

例えば次が挙げられます。

var a = ++(new S()); // error: not a variable
var b = ++S.P2; // var temp = S.op_Increment(S.get_P2()); S.set_P2(temp); b = temp;
++S.P2; // var temp = S.op_Increment(S.get_P2()); S.set_P2(temp);
++b; // b.op_Increment(); 
var d = ++S.P1; // error: set is missing
++S.P1; // error: set is missing
var e = ++b; // var temp = b; temp.op_Increment(); e = (b = temp); 

struct S
{
    public static S P1 { get; } = new S();
    public static S P2 { get; set; } = new S();

    public static S operator ++(S x) => ...;
    public void operator ++() => ...;
}

後置インクリメント演算子と後置デクリメント演算子

https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/expressions.md#12816-postfix-increment-and-decrement-operators を参照してください

操作の結果が使用されるか、xx «op»が変数として分類されていないか、古い言語バージョンが対象になっている場合、現在指定されている単項演算子オーバーロード解決を適用することによって、操作https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/expressions.md#12816-postfix-increment-and-decrement-operators処理されます。 結果を使用するときにインスタンスインクリメント演算子を試していないのは、参照型を処理している場合、インプレースで変更された場合、操作の前に x の値を生成できないという事実です。 値型を扱う場合は、とにかくコピーを作成する必要があります。

それ以外の場合は、次のようにインクリメント演算子に優先順位が与えられます。

最初に、 instance インクリメント演算子のオーバーロード解決を適用して、操作を処理しようとします。 プロセスが結果を生成せず、エラーが発生しない場合、現在指定されている単項演算子のオーバーロード解決 https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/expressions.md#12816-postfix-increment-and-decrement-operators 適用することで、操作が処理されます。

それ以外の場合、操作 x«op» は次のように評価されます。

xの型が参照型であることがわかっていれば、xに対して演算子メソッドが呼び出されます。 xnull場合、演算子メソッドの呼び出しは NullReferenceException をスローします。

例えば次が挙げられます。

var a = (new C())++; // error: not a variable
var b = new C(); 
var c = b++; // var temp = b; b = C.op_Increment(temp); c = temp; 
b++; // b.op_Increment();
var d = C.P1++; // error: missing setter
C.P1++; // error: missing setter
var e = C.P2++; // var temp = C.get_P2(); C.set_P2(C.op_Increment(temp)); e = temp;
C.P2++; // var temp = C.get_P2(); C.set_P2(C.op_Increment(temp));

class C
{
    public static C P1 { get; } = new C();
    public static C P2 { get; set; } = new C();

    public static C operator ++(C x) => ...; 
    public void operator ++() => ...;
}

xの型が参照型でない場合は、xで演算子メソッドが呼び出されます。

例えば次が挙げられます。

var a = (new S())++; // error: not a variable
var b = S.P2++; // var temp = S.get_P2(); S.set_P2(S.op_Increment(temp)); b = temp;
S.P2++; // var temp = S.get_P2(); S.set_P2(S.op_Increment(temp));
b++; // b.op_Increment(); 
var d = S.P1++; // error: set is missing
S.P1++; // error: missing setter
var e = b++; // var temp = b; b = S.op_Increment(temp); e = temp; 

struct S
{
    public static S P1 { get; } = new S();
    public static S P2 { get; set; } = new S();

    public static S operator ++(S x) => ...; 
    public void operator ++() => ...;
}

インスタンスインクリメント演算子のオーバーロード解決

フォーム «op» x または x «op»の演算。«op» はオーバーロード可能なインスタンスインクリメント演算子で、 xX型の式であり、次のように処理されます。

  • 操作Xoperator «op»(x)によって提供される候補ユーザー定義演算子のセットは、candidate インスタンスインクリメント演算子の規則を使用して決定されます。
  • 候補のユーザー定義演算子のセットが空でない場合、これは演算の候補演算子のセットになります。 それ以外の場合、オーバーロードの解決では結果は生成されません。
  • オーバーロード解決ルールは候補演算子のセットに適用され、最適な演算子を選択します。この演算子はオーバーロード解決プロセスの結果になります。 オーバーロードの解決で最適な演算子を 1 つ選択できない場合は、バインド時エラーが発生します。

候補インスタンスインクリメント演算子

T と操作 «op»( «op» がオーバーロード可能なインスタンスインクリメント演算子である場合)、 T によって提供される候補のユーザー定義演算子のセットは次のように決定されます。

  • unchecked評価コンテキストでは、インスタンス演算子のみがターゲット名のoperator «op»()と一致すると見なされた場合に、Nプロセスによって生成される演算子のグループです。
  • checked評価コンテキストでは、Member 参照によって生成される演算子のグループですインスタンス operator «op»()演算子とインスタンス operator checked «op»()演算子のみがターゲット名のNと一致すると見なされた場合に処理されます。 ペアごとの一致operator «op»()宣言を持つoperator checked «op»()演算子は、グループから除外されます。

複合代入。

https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/expressions.md#12214-compound-assignment を参照してください

dynamicを扱う先頭の段落はそのまま適用されます。

それ以外の場合、xx «op»= yが変数として分類され、新しい言語バージョンが対象になっている場合は、次のように割り当て演算子優先されます。

まず、compound 代入演算子のオーバーロード解決を適用して、フォーム x «op»= yの操作を処理しようとします。 プロセスが結果を生成せず、エラーが発生しない場合は、現在指定されている 2 項演算子のオーバーロード解決 https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/expressions.md#12214-compound-assignment 適用することで、操作が処理されます。

それ以外の場合、操作は次のように評価されます。

xの型が参照型であることがわかっている場合、xはインスタンス x₀を取得するために評価され、yを引数として使用してそのインスタンスに対して演算子メソッドが呼び出され、複合代入の結果としてx₀が返されます。 x₀null場合、演算子メソッドの呼び出しは NullReferenceException をスローします。

例えば次が挙げられます。

var a = (new C())+=10; // error: not a variable
var b = a += 100; // var temp = a; temp.op_AdditionAssignment(100); b = temp; 
var c = b + 1000; // c = C.op_Addition(b, 1000)
c += 5; // c.op_AdditionAssignment(5);
var d = C.P1 += 11; // error: setter is missing
var e = C.P2 += 12; // var temp = C.op_Addition(C.get_P2(), 12); C.set_P2(temp); e = temp;
C.P2 += 13; // var temp = C.op_Addition(C.get_P2(), 13); C.set_P2(temp);

class C
{
    public static C P1 { get; } = new C();
    public static C P2 { get; set; } = new C();

    // op_Addition
    public static C operator +(C x, int y) => ...;

    // op_AdditionAssignment
    public void operator +=(int y) => ...;
}

xの型が参照型であることが不明な場合:

  • 複合代入の結果を使用した場合、 x はインスタンス x₀を取得するために評価され、引数として y を使用してそのインスタンスに対して演算子メソッドが呼び出され、 x₀x に割り当てられ、複合代入の結果として x₀ が返されます。
  • それ以外の場合は、xを引数として使用して、yで演算子メソッドが呼び出されます。

xの副作用は、プロセスで 1 回だけ評価されることに注意してください。

例えば次が挙げられます。

var a = (new S())+=10; // error: not a variable
var b = S.P2 += 100; // var temp = S.op_Addition(S.get_P2(), 100); S.set_P2(temp); b = temp;
S.P2 += 100; // var temp = S.op_Addition(S.get_P2(), 100); S.set_P2(temp);
var c = b + 1000; // c = S.op_Addition(b, 1000)
c += 5; // c.op_AdditionAssignment(5); 
var d = S.P1 += 11; // error: setter is missing
var e = c += 12; // var temp = c; temp.op_AdditionAssignment(12); e = (c = temp); 

struct S
{
    public static S P1 { get; } = new S();
    public static S P2 { get; set; } = new S();

    // op_Addition
    public static S operator +(S x, int y) => ...;

    // op_AdditionAssignment
    public void operator +=(int y) => ...;
}

複合代入演算子のオーバーロードの解決

x «op»= yがオーバーロード可能な複合代入演算子であるフォーム «op»=の演算x、次のように処理X型の式です。

  • 操作Xoperator «op»=(y)によって提供される候補のユーザー定義演算子のセットは、candidate 複合代入演算子の規則を使用して決定されます。
  • セット内の少なくとも 1 つの候補ユーザー定義演算子が引数リスト (y)に適用できる場合、これは操作の候補演算子のセットになります。 それ以外の場合、オーバーロードの解決では結果は生成されません。
  • オーバーロード解決ルールは候補演算子のセットに適用され、引数リスト(y)に対して最適な演算子を選択します。この演算子はオーバーロード解決プロセスの結果になります。 オーバーロードの解決で最適な演算子を 1 つ選択できない場合は、バインド時エラーが発生します。

候補複合代入演算子

T と演算 «op»=( «op»= がオーバーロード可能な複合代入演算子である場合)、 T によって提供される候補のユーザー定義演算子のセットは次のように決定されます。

  • unchecked評価コンテキストでは、インスタンス演算子のみがターゲット名のoperator «op»=(Y)と一致すると見なされた場合に、Nプロセスによって生成される演算子のグループです。
  • checked評価コンテキストでは、Member 参照によって生成される演算子のグループですインスタンス operator «op»=(Y)演算子とインスタンス operator checked «op»=(Y)演算子のみがターゲット名のNと一致すると見なされた場合に処理されます。 ペアごとの一致operator «op»=(Y)宣言を持つoperator checked «op»=(Y)演算子は、グループから除外されます。

未解決の質問

[解決済み]構造体 readonly 修飾子を許可する必要がありますか?

メソッドの目的全体がインスタンスを変更する場合に、メソッドを readonly でマークしてもメリットはないと感じます。

結論: 修飾子 readonly 許可されますが、現時点ではターゲット要件を緩和しません。

[解決済み]シャドウを許可する必要がありますか?

基底クラスと同じシグネチャを持つ '複合代入'/'instance increment' 演算子を派生クラスで宣言する場合は、 override 修飾子が必要ですか?

結論: シャドウは、メソッドと同じルールで許可されます。

[解決済み]宣言された += 演算子と + 演算子の間に一貫性を適用する必要がありますか?

LDM-2025-02-12 では、作成者が誤って+=が機能する可能性がある奇数のシナリオにユーザーをプッシュすることに懸念が生じていましたが、1 つのフォームでは他のフォームよりも余分な演算子が宣言されるため、+は動作しません (またはその逆も同様)。

結論: 異なる形式の演算子間の一貫性に関するチェックは行われません。

選択肢

静的メソッドを使用し続ける

変更するインスタンスが最初のパラメーターとして渡される静的演算子メソッドの使用を検討できます。 値型の場合、そのパラメーターは ref パラメーターである必要があります。 それ以外の場合、メソッドはターゲット変数を変更できません。 同時に、クラス型の場合、そのパラメーターを ref パラメーターにすることはできません。 クラスの場合は、インスタンスが格納されている場所ではなく、渡されたインスタンスを変更する必要があるためです。 ただし、インターフェイスで演算子が宣言されている場合、多くの場合、インターフェイスがクラスによってのみ実装されるか、構造体によってのみ実装されるかは不明です。 したがって、最初のパラメーターを ref パラメーターにする必要があるかどうかは明確ではありません。

デザインに関する会議