チュートリアル: C# 11 の機能を調べる - インターフェイスの静的仮想メンバー

C# 11 と .NET 7 には、インターフェイスの静的仮想メンバーが含まれています。 この機能を使用すると、オーバーロードされた演算子などの静的メンバーを含むインターフェイスを定義できます。 静的メンバーを含むインターフェイスを定義したら、それらのインターフェイスを制約として使用して、演算子や他の静的メソッドを使用するジェネリック型を作成できます。 オーバーロードされた演算子を含むインターフェイスを作成しない場合でも、言語の更新によって有効になるこの機能とジェネリック型数値演算クラスからメリットが得られる可能性があります。

このチュートリアルで学習する内容は次のとおりです。

  • 静的メンバーを含むインターフェイスを定義する。
  • インターフェイスを使用して、演算子が定義されたインターフェイスを実装するクラスを定義する。
  • 静的インターフェイス メソッドを使用する汎用アルゴリズムを作成する。

前提条件

C# 11 をサポートする .NET 7 を実行するようにお使いのマシンを設定する必要があります。 C# 11 コンパイラは、Visual Studio 2022 バージョン 17.3 以降または .NET 7 SDK 以降で使用できます。

静的抽象インターフェイス メソッド

最初に例を見てみましょう。 次のメソッドは、double 型の 2 つの数値の中間値を返します。

public static double MidPoint(double left, double right) =>
    (left + right) / (2.0);

数値型 (intshortlongfloatdecimal)、または数値を表す任意の型にも同じロジックを使用できます。 + 演算子と / 演算子を使用し、2 の値を定義する方法が必要です。 System.Numerics.INumber<TSelf> インターフェイスを使用すると、上記のメソッドを次のジェネリック メソッドとして記述できます。

public static T MidPoint<T>(T left, T right)
    where T : INumber<T> => (left + right) / T.CreateChecked(2);  // note: the addition of left and right may overflow here; it's just for demonstration purposes

INumber<TSelf> インターフェイスを実装する型に、operator +operator / の定義を含める必要があります。 分母は T.CreateChecked(2) によって定義され、あらゆる数値型の値 2 を作成します。分母は 2 つのパラメーターと同じ型になるように強制されます。 INumberBase<TSelf>.CreateChecked<TOther>(TOther) によって指定の値から型のインスタンスが作成され、表せる範囲に入らない値の場合、OverflowException がスローされます。 (この実装では、leftright が両方とも相当大きな値の場合、オーバーフローする可能性があります。この潜在的な問題を回避できる代替アルゴリズムがあります)

使い慣れた構文を使用して、インターフェイスの静的抽象メンバーを定義します。実装を提供しない静的メンバーに、static および abstract 修飾子を追加します。 次の例では、operator ++ をオーバーライドする任意の型に適用できる IGetNext<T> インターフェイスを定義しています。

public interface IGetNext<T> where T : IGetNext<T>
{
    static abstract T operator ++(T other);
}

型引数 TIGetNext<T> を実装するという制約により、演算子のシグネチャに、含んでいる型またはその型引数が含まれることが保証されます。 多くの演算子では、そのパラメーターが型と一致するか、または含んでいる型を実装するように制約された型パラメーターでなければならないことが強制されます。 この制約がない場合、IGetNext<T> インターフェイスで ++ 演算子を定義することはできません。

次のコードを使用して、文字 "A" の文字列を作成し、インクリメントするたびにその文字列に文字をもう 1 つ追加する構造体を作成できます。

public struct RepeatSequence : IGetNext<RepeatSequence>
{
    private const char Ch = 'A';
    public string Text = new string(Ch, 1);

    public RepeatSequence() {}

    public static RepeatSequence operator ++(RepeatSequence other)
        => other with { Text = other.Text + Ch };

    public override string ToString() => Text;
}

一般的には、"この型の次の値を生成する" ことを意味するように ++ を定義するアルゴリズムを構築できます。 このインターフェイスを使用すると、わかりやすいコードと結果が生成されます。

var str = new RepeatSequence();

for (int i = 0; i < 10; i++)
    Console.WriteLine(str++);

前の例では次の出力が生成されます。

A
AA
AAA
AAAA
AAAAA
AAAAAA
AAAAAAA
AAAAAAAA
AAAAAAAAA
AAAAAAAAAA

この簡単な例は、この機能の意図を示しています。 演算子、定数値、他の静的操作に自然な構文を使用できます。 オーバーロードされた演算子など、静的メンバーに依存する複数の型を作成するときに、これらの手法を検討できます。 使用する型の機能に合ったインターフェイスを定義し、それらの型がその新しいインターフェイスをサポートすることを宣言します。

ジェネリック型数値演算

演算子を含め、インターフェイスの静的メソッドを許可するシナリオの目的は、"ジェネリック型数値演算" アルゴリズムをサポートすることです。 .NET 7 基本クラス ライブラリには、多くの算術演算子のインターフェイス定義と、INumber<T> インターフェイスで多くの算術演算子を組み合わせる派生インターフェイスが含まれています。 それらの型を適用して、T に任意の数値型を使用できる Point<T> レコードを作成しましょう。 + 演算子を使用して、XOffsetYOffset によって点を移動できます。

まず、dotnet new または Visual Studio を使用して、新しいコンソール アプリケーションを作成します。

Translation<T>Point<T> のパブリック インターフェイスは、次のコードのようになります。

// Note: Not complete. This won't compile yet.
public record Translation<T>(T XOffset, T YOffset);

public record Point<T>(T X, T Y)
{
    public static Point<T> operator +(Point<T> left, Translation<T> right);
}

Translation<T>Point<T> の両方の型に record 型を使用します。どちらも 2 つの値を格納し、高度な動作ではなくデータ記憶域を表します。 operator + の実装は、次のコードのようになります。

public static Point<T> operator +(Point<T> left, Translation<T> right) =>
    left with { X = left.X + right.XOffset, Y = left.Y + right.YOffset };

前のコードをコンパイルするには、TIAdditionOperators<TSelf, TOther, TResult> インターフェイスをサポートすることを宣言する必要があります。 このインターフェイスには、operator + 静的メソッドが含まれています。 これは 3 つの型パラメーターを宣言します。1 つは左オペランド用、1 つは右オペランド用、もう 1 つは結果用です。 オペランドと結果の異なる型に対して + を実装する型もあります。 型引数 TIAdditionOperators<T, T, T> を実装するという宣言を追加します。

public record Point<T>(T X, T Y) where T : IAdditionOperators<T, T, T>

この制約を追加すると、Point<T> クラスではその加算演算子に + を使用できるようになります。 Translation<T> の宣言でも同じ制約を追加します。

public record Translation<T>(T XOffset, T YOffset) where T : IAdditionOperators<T, T, T>;

IAdditionOperators<T, T, T> 制約により、そのクラスを使用する開発者は、点に対する加算の制約を満たしていない型を使用して Translation を作成することはできなくなります。 Translation<T>Point<T> の型パラメーターに必要な制約が追加されたので、このコードは機能します。 Program.cs ファイルの TranslationPoint の上記の宣言に次のようなコードを追加することでテストできます。

var pt = new Point<int>(3, 4);

var translate = new Translation<int>(5, 10);

var final = pt + translate;

Console.WriteLine(pt);
Console.WriteLine(translate);
Console.WriteLine(final);

これらの型が適切な算術インターフェイスを実装することを宣言することによって、このコードを再利用しやすくすることができます。 最初に行う変更として、Point<T, T>IAdditionOperators<Point<T>, Translation<T>, Point<T>> インターフェイスを実装することを宣言します。 Point 型は、オペランドと結果に異なる型を使用します。 Point 型はそのシグネチャを持つ operator + を既に実装しているので、このインターフェイスを宣言に追加するだけで済みます。

public record Point<T>(T X, T Y) : IAdditionOperators<Point<T>, Translation<T>, Point<T>>
    where T : IAdditionOperators<T, T, T>

最後に、加算を実行するときは、その型の加法単位元の値を定義するプロパティがあると便利です。 この機能のための新しいインターフェイス (IAdditiveIdentity<TSelf,TResult>) があります。 {0, 0} の変換は加法単位元です。結果の点は左オペランドと同じです。 IAdditiveIdentity<TSelf, TResult> インターフェイスでは、単位元の値を返す 1 つの読み取り専用プロパティ AdditiveIdentity が定義されます。 このインターフェイスを実装するには、Translation<T> を少し変更する必要があります。

using System.Numerics;

public record Translation<T>(T XOffset, T YOffset) : IAdditiveIdentity<Translation<T>, Translation<T>>
    where T : IAdditionOperators<T, T, T>, IAdditiveIdentity<T, T>
{
    public static Translation<T> AdditiveIdentity =>
        new Translation<T>(XOffset: T.AdditiveIdentity, YOffset: T.AdditiveIdentity);
}

ここではいくつかの変更があるので、それらを 1 つずつ見ていきましょう。 まず、Translation 型が IAdditiveIdentity インターフェイスを実装することを宣言します。

public record Translation<T>(T XOffset, T YOffset) : IAdditiveIdentity<Translation<T>, Translation<T>>

次に、次のコードに示すように、インターフェイス メンバーを実装してみます。

public static Translation<T> AdditiveIdentity =>
    new Translation<T>(XOffset: 0, YOffset: 0);

0 は型に依存するため、上記のコードはコンパイルできません。 0 の代わりに IAdditiveIdentity<T>.AdditiveIdentity を使用します。 この変更は、TIAdditiveIdentity<T> を実装することを制約に含める必要があることを意味します。 その結果、実装は次のようになります。

public static Translation<T> AdditiveIdentity =>
    new Translation<T>(XOffset: T.AdditiveIdentity, YOffset: T.AdditiveIdentity);

Translation<T> にその制約を追加したので、同じ制約を Point<T> にも追加する必要があります。

using System.Numerics;

public record Point<T>(T X, T Y) : IAdditionOperators<Point<T>, Translation<T>, Point<T>>
    where T : IAdditionOperators<T, T, T>, IAdditiveIdentity<T, T>
{
    public static Point<T> operator +(Point<T> left, Translation<T> right) =>
        left with { X = left.X + right.XOffset, Y = left.Y + right.YOffset };
}

このサンプルでは、ジェネリック型数値演算のインターフェイスを作成する方法を示しました。 以下の方法を学習しました。

  • INumber<T> インターフェイスに依存するメソッドを記述して、そのメソッドを任意の数値型で使用できるようにします。
  • 加算インターフェイスに依存する型を作成して、1 つの算術演算のみをサポートする型を実装します。 その型で同じインターフェイスのサポートを宣言して、他の方法でそれを作成できるようにします。 アルゴリズムは、算術演算子の最も自然な構文を使用して記述されます。

これらの機能を試してフィードバックを登録してください。 Visual Studio の [フィードバックの送信] メニュー項目を使用することも、GitHub の roslyn リポジトリに新しいイシューを作成することもできます。 どの数値型でも機能する汎用アルゴリズムを構築します。 型引数が数値と同様の機能のサブセットのみを実装できるこれらのインターフェイスを使用してアルゴリズムを構築します。 これらの機能を使用する新しいインターフェイスを作成しなくても、アルゴリズムでそれらを使用してみることができます。

関連項目