教學課程:探索 C# 11:介面中的靜態虛擬成員

C# 11 和 .NET 7 在介面中包含靜態虛擬成員。 這項功能可讓您定義包含多載運算子或其他靜態成員的介面。 定義具有靜態成員的介面之後,您可以使用這些介面作為限制式建立使用運算子或其他靜態方法的泛型型別。 即使您未建立具有多載運算子的介面,您也可能會受益於此功能和語言更新所啟用的泛型數學類別。

在本教學課程中,您將了解如何:

  • 定義具有靜態成員的介面。
  • 使用介面定義實作具有已定義運算子的介面類別。
  • 建立依賴靜態介面方法的泛型演算法。

必要條件

您必須將電腦設定為執行 .NET 7,支援 C# 11。 從 Visual Studio 2022 17.3 版.NET 7 SDK 開始,C# 11 編譯器可供使用。

靜態抽象介面方法

讓我們從下列範例開始。 下列方法會傳回兩個 double 數字的中間點:

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,強制分母與兩個參數的類型相同。 INumberBase<TSelf>.CreateChecked<TOther>(TOther) 會從指定的值建立型別的執行個體,如果值落在可表示的範圍之外,則會擲回 OverflowException。 (如果 leftright 都夠大,這個實作就有可能溢位。有一個替代演算法可以避免這個潛在的問題。)

您可以使用熟悉的語法在介面中定義靜態抽象成員:您會新增 staticabstract 修飾詞至未提供實作的任何靜態成員。 下列範例會定義可套用至覆寫 operator ++ 的任何型別的 IGetNext<T> 介面:

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> 介面中許多算術運算子的衍生介面。 讓我們套用這些型別組建 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 類型:兩者都儲存兩個值,而且它們代表資料儲存區,而不是複雜的行為。 operator + 的實作類似下列程式碼:

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

若要編譯先前的程式碼,您必須宣告支援 IAdditionOperators<TSelf, TOther, TResult> 介面的 T。 該介面包含 operator + 靜態方法。 它會宣告三個型別參數:用於左運算元、用於右運算元,及用於結果。 部分型別會針對不同的運算元和結果型別實作 + 。 新增型別引數 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 檔案中的 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> 介面會定義一個唯讀的屬性 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);
}

這裡有幾個變更,讓我們逐一解說。 首先,您會宣告 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 視型別而定,上述程式碼無法編譯。 答案:使用 0IAdditiveIdentity<T>.AdditiveIdentity。 此變更表示您的限制式現在必須包含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> 介面的方法,讓方法可以搭配任何數字型別使用。
  • 組建依賴加法介面的類型,實作只支援一個數學作業的類型。 該類型會宣告其對這些相同介面的支援,以便以其他方式寫入。 演算法使用最自然的數學運算子語法寫入。

試驗這些功能並註冊意見反應。 您可以使用 Visual Studio 中的 [傳送意見反應] 功能表項目,或在 GitHub 上的 roslyn 存放庫中建立新問題。 組建可搭配任何數字型別的泛型演算法。 使用這些介面組建演算法,其中型別引數只能實作類似數字的功能子集。 即使您未組建使用這些功能的新介面,您也可以在演算法中將其用於試驗。

另請參閱