インターフェイスの静的仮想メンバー を使用すると、 オーバーロードされた演算子 またはその他の静的メンバーを含むインターフェイスを定義できます。 静的メンバーを持つインターフェイスを定義したら、それらのインターフェイスを 制約 として使用して、演算子またはその他の静的メソッドを使用するジェネリック型を作成できます。 オーバーロードされた演算子を含むインターフェイスを作成しない場合でも、この機能と、言語更新によって有効になるジェネリック数学クラスの利点が得られる可能性があります。
このチュートリアルでは、以下の内容を学習します。
- 静的メンバーを持つインターフェイスを定義します。
- インターフェイスを使用して、定義された演算子を持つインターフェイスを実装するクラスを定義します。
- 静的インターフェイス メソッドに依存する汎用アルゴリズムを作成します。
[前提条件]
- 最新の .NET SDK
- Visual Studio Code エディター
- C# DevKit
静的抽象インターフェイス メソッド
例から始めましょう。 次のメソッドは、2 つの double 数値の中間点を返します。
public static double MidPoint(double left, double right) =>
(left + right) / (2.0);
数値型 ( int、 short、 long、 floatdecimal、または数値を表す任意の型) に対しても、同じロジックが機能します。
+演算子と/演算子を使用し、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 をスローします。 (この実装では、 left と right の両方が十分な大きさの値である場合、オーバーフローの可能性があります。この潜在的な問題を回避できる別のアルゴリズムがあります)。)
インターフェイスでは、使い慣れた構文を使用して静的抽象メンバーを定義します。実装を提供しない静的メンバーに static および abstract 修飾子を追加します。 次の例では、IGetNext<T>をオーバーライドする任意の型に適用できるoperator ++ インターフェイスを定義します。
public interface IGetNext<T> where T : IGetNext<T>
{
static abstract T operator ++(T other);
}
型引数 Tが実装する制約 IGetNext<T> 、演算子のシグネチャに含まれる型またはその型引数が確実に含まれるようにします。 多くの演算子では、そのパラメーターが型と一致する必要があります。または、包含型を実装するために制約された型パラメーターである必要があります。 この制約がないと、 ++ 演算子を IGetNext<T> インターフェイスで定義できませんでした。
次のコードを使用して、各インクリメントによって文字列に別の文字が追加される 、'A' 文字の文字列を作成する構造体を作成できます。
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> インターフェイスで多数の算術演算子を組み合わせた派生インターフェイスが含まれています。 これらの型を適用して、Point<T>に任意の数値型を使用できるT レコードを作成してみましょう。
XOffset演算子を使用して、YOffsetと+でポイントを移動できます。
まず、 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);
}
record型とTranslation<T>型の両方にPoint<T>型を使用します。どちらも 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 };
前のコードをコンパイルするには、 T が IAdditionOperators<TSelf, TOther, TResult> インターフェイスをサポートしていることを宣言する必要があります。 そのインターフェイスには、 operator + 静的メソッドが含まれています。 3 つの型パラメーターを宣言します。1 つは左オペランド用、1 つは右オペランド用、1 つは結果用です。 一部の型では、オペランドと結果の型ごとに + が実装されます。 型引数TがIAdditionOperators<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 TranslationのPointとの宣言の上に次のようなコードを追加することでテストできます。
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>
最後に、追加を実行するときは、その型の追加 ID 値を定義するプロパティを用意すると便利です。 その機能の新しいインターフェイスがあります: IAdditiveIdentity<TSelf,TResult>。
{0, 0}の変換は加法 ID です。結果のポイントは左オペランドと同じです。
IAdditiveIdentity<TSelf, TResult> インターフェイスは、ID 値を返す 1 つの読み取りonly プロパティ (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 は型によって異なるためです。 答え: IAdditiveIdentity<T>.AdditiveIdentityに0を使用します。 この変更は、 T が IAdditiveIdentity<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 リポジトリに新しい 問題 を作成することもできます。 任意の数値型で動作する汎用アルゴリズムを構築します。 型引数が数値に似た機能のサブセットのみを実装するこれらのインターフェイスを使用してアルゴリズムを構築します。 これらの機能を使用する新しいインターフェイスを構築しない場合でも、アルゴリズムでそれらを使用して実験できます。
こちらも参照ください
.NET