C# 14 (.NET 10 世代の C# ) の機能、ユーザー定義複合代入演算子 / user-defined-compound-assignment-operators を見てみます。
■ これまでは
+ や - などの演算の結果が新しいインスタンスが作られる演算子をユーザー定義することができた。既存のインスタンスの値を変更する演算子はユーザー定義することができなかった。
■ これからは
+= や -= など既存のインスタンスの値を変更する演算子をユーザー定義できる。
■ コードで例
{ C c = new() { Value = 32 }; Console.WriteLine(++c); } // 33 = 32 + 1
{ C c = new() { Value = 32 }; Console.WriteLine(--c); } // 31 = 32 - 1
{ C c = new() { Value = 32 }; Console.WriteLine(c += new C() { Value = 2 }); } // 34 = 32 + 2
{ C c = new() { Value = 32 }; Console.WriteLine(c -= new C() { Value = 2 }); } // 30 = 32 - 2
{ C c = new() { Value = 32 }; Console.WriteLine(c *= new C() { Value = 2 }); } // 64 = 32 * 2
{ C c = new() { Value = 32 }; Console.WriteLine(c /= new C() { Value = 2 }); } // 16 = 32 / 2
{ C c = new() { Value = 32 }; Console.WriteLine(c %= new C() { Value = 3 }); } // 2 = 32 % 3
{ C c = new() { Value = 39 }; Console.WriteLine(c &= new C() { Value = 7 }); } // 7 = 0b100111 & 0b111
{ C c = new() { Value = 32 }; Console.WriteLine(c |= new C() { Value = 2 }); } // 34 = 0b100000 | 0b10
{ C c = new() { Value = 33 }; Console.WriteLine(c ^= new C() { Value = 3 }); } // 34 = 0b100001 ^ 0b11
{ C c = new() { Value = 32 }; Console.WriteLine(c <<= 1); } // 64 = 32 << 1
{ C c = new() { Value = 32 }; Console.WriteLine(c >>= 1); } // 16 = 32 >> 1
// ユーザー定義の演算子を持つクラス
class C
{
public int? Value { get; set; }
public override string? ToString() => Value?.ToString();
///
// これらは以前から書けた
///
public static C operator +(C c1, C c2) => new C() { Value = c1?.Value + c2?.Value };
public static C operator -(C c1, C c2) => new C() { Value = c1?.Value - c2?.Value };
public static C operator *(C c1, C c2) => new C() { Value = c1?.Value * c2?.Value };
public static C operator /(C c1, C c2) => new C() { Value = c1?.Value / c2?.Value };
public static C operator %(C c1, C c2) => new C() { Value = c1?.Value % c2?.Value };
public static C operator &(C c1, C c2) => new C() { Value = c1?.Value & c2?.Value };
public static C operator |(C c1, C c2) => new C() { Value = c1?.Value | c2?.Value };
public static C operator ^(C c1, C c2) => new C() { Value = c1?.Value ^ c2?.Value };
public static C operator <<(C c, int shift) => new C() { Value = c?.Value << shift };
public static C operator >>(C c, int shift) => new C() { Value = c?.Value >> shift };
public static C operator ++(C c) => new C() { Value = c?.Value + 100 }; // 下の ++() があるとこちらは使われない
public static C operator --(C c) => new C() { Value = c?.Value - 100 }; // 下の --() があるとこちらは使われない
///
// これらが書けるようになった
///
// 現時点で Visual Studio でビルドできず、コマンドラインでのビルド (Visual Studio ではエラーになる)
// > dotnet build
public void operator ++() { Value += 1; }
public void operator --() { Value -= 1; }
public void operator +=(C c) { Value += c?.Value; }
public void operator -=(C c) { Value -= c?.Value; }
public void operator *=(C c) { Value *= c?.Value; }
public void operator /=(C c) { Value /= c?.Value; }
public void operator %=(C c) { Value %= c?.Value; }
public void operator &=(C c) { Value &= c?.Value; }
public void operator |=(C c) { Value |= c?.Value; }
public void operator ^=(C c) { Value ^= c?.Value; }
public void operator <<=(int shift) { Value <<= shift; }
public void operator >>=(int shift) { Value >>= shift; }
// Preview バージョンを指定していない場合のエラー
// 機能 'user-defined compound assignment operators' は現在、プレビュー段階であり、*サポートされていません*。プレビュー機能を使用するには、'preview' 言語バージョンを使用してください。
}
■ 注意点
現時点で Visual Studio でビルドできないようです。ビルドを試みるとエラーになります。
.NET の最新 Preview 版をインストールしてコマンドラインでビルドします。
dotnet build
■ 以前からの演算子と、複合代入演算子
以前からかける演算子だけでも += のような書き方はできました。では今回のユーザー定義の複合代入演算子でどう変わるのでしょう?
// ユーザー定義の演算子を持つクラス class C { public int? Value { get; set; } public override string? ToString() => Value?.ToString(); public void operator +=(C c) { Value += c?.Value; } }
// ユーザー定義の複合代入演算子を持たないクラス class C2 { public int? Value { get; set; } public override string? ToString() => Value?.ToString(); public static C2 operator +(C2 c1, C2 c2) => new C2() { Value = c1?.Value + c2?.Value }; }
ユーザー定義の演算子を持つクラスの += の動作例
+= の結果は新しいインスタンスにならず、既存のインスタンスが使われています。
// ユーザー定義の += を定義している場合 C c = new() { Value = 32 }; C c2 = c; Console.WriteLine(c == c2); // ture (c と c2 は同じインスタンス) Console.WriteLine(c += new C() { Value = 2 }); // 34 Console.WriteLine(c2); // 34 ( += でインスタンスの値が変わっている ) Console.WriteLine(c == c2); // ture
ユーザー定義の演算子を持たないクラスの += の動作例
+= の結果は新しいインスタンスになっています。
多くの場合、+= をした場合、演算前のインスタンスはもう使うことはないと思います。ということはもう使わないインスタンスとこれから使う新しいインスタンスで、インスタンスが2つになるということです。
// ユーザー定義の += を定義していない場合 C2 c = new() { Value = 32 }; C2 c2 = c; Console.WriteLine(c == c2); // ture (c と c2 は同じインスタンス) Console.WriteLine(c += new C2() { Value = 2 }); // 34 Console.WriteLine(c2); // 32 ( += で新しいインスタンスが作られ。インスタンスの値は変わらない) Console.WriteLine(c == c2); // false (c と c2 は別のインスタンスになっている)
■ 使い方次第
ユーザー定義の複合代入演算子を使えば生まれるインスタンスが少なくなって良いのですが、前述の例の後者のような場合はおそらく意図した挙動ではないと思います。
後者のパターンでは、変数 c2 のメンバーの値が変わることを想定して C2 クラスを使うのはなかなかな暗黙知でしょう。
君は注意しながら効率化してもよいし、既存のコード感覚を大事に効率化しなくてもよい。使い方次第です。
■ 今回のコード
GitHub に上げています。
■ 備えよう
新機能、いいですね。リリースに備えましょう。