共用方式為


教學:探索介面中的靜態虛擬成員

介面靜態虛擬成員 允許您定義包含 過載運算子 或其他靜態成員的介面。 一旦你定義了帶有靜態成員的介面,就可以將這些介面作為 限制 ,建立使用運算子或其他靜態方法的通用型別。 即使你沒有建立過載運算子的介面,你也很可能會從這個功能和語言更新中啟用的通用數學類別中受益。

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

  • 定義與靜態成員的介面。
  • 使用介面來定義實作帶有運算子的介面的類別。
  • 建立依賴靜態介面方法的通用演算法。

先決條件

靜態抽象介面方法

讓我們先從一個例子開始。 以下方法回傳兩個 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 的值都足夠大,此實現可能會發生溢位。有其他替代演算法可以避免此潛在問題。)

你可以在介面中用熟悉的語法定義靜態抽象成員:只要靜態成員沒有實作,你就要加上 static and 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 基類庫包含許多算術運算子的介面定義,還有將多個算術運算子結合於一個介面中的衍生的介面。 我們來套用這些類型來建立一個Point<T>紀錄,可以使用任意的數字類型T。 你可以用運算子把點移動一點 XOffsetYOffset+

先從建立一個新的 Console 應用程式開始,可以用 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> 兩種類型:這兩者都儲存兩個值,且它們代表的是資料儲存,而非複雜的行為。 的 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 + 態方法。 它宣告三個型態參數:一個用於左操作元,一個用於右操作元,以及一個用於結果。 有些類型為了處理不同的運算元和結果類型而實作+。 新增一個宣告,表示型別參數 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 這取決於類型。 答案是:使用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> 的方法,使該方法能用於任何數字型別。
  • 建立一個依賴加法介面來實作只支援一種數學運算的型別。 該型別宣告支援這些介面,因此可以以其他方式組合。 這些演算法使用最自然的數學運算子語法來撰寫。

多嘗試這些功能並收集回饋。 你可以使用 Visual Studio 的 「傳送回饋 」選單項目,或在 GitHub 的 roslyn 倉庫中建立新 議題 。 建立能處理任何數值類型的通用演算法。 利用這些介面建立演算法,其中型別參數僅實作部分類數字能力。 即使你沒有開發出使用這些功能的新介面,也可以嘗試在演算法中運用它們。

另請參閱