介面靜態虛擬成員 允許您定義包含 過載運算子 或其他靜態成員的介面。 一旦你定義了帶有靜態成員的介面,就可以將這些介面作為 限制 ,建立使用運算子或其他靜態方法的通用型別。 即使你沒有建立過載運算子的介面,你也很可能會從這個功能和語言更新中啟用的通用數學類別中受益。
在本教學課程中,您將瞭解如何:
- 定義與靜態成員的介面。
- 使用介面來定義實作帶有運算子的介面的類別。
- 建立依賴靜態介面方法的通用演算法。
先決條件
- 最新 .NET SDK
- Visual Studio Code 編輯器
- C# 開發套件
靜態抽象介面方法
讓我們先從一個例子開始。 以下方法回傳兩個 double 數字的中點:
public static double MidPoint(double left, double right) =>
(left + right) / (2.0);
同樣的邏輯適用於任何數字類型:int、shortlongfloatdecimal或任何代表數字的類型。 你需要有一種方式來使用 + 和 / 運算子,並為 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 (若 left 和 right 的值都足夠大,此實現可能會發生溢位。有其他替代演算法可以避免此潛在問題。)
你可以在介面中用熟悉的語法定義靜態抽象成員:只要靜態成員沒有實作,你就要加上 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 檔案中在宣告 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>
最後,當你執行加法時,擁有一個定義該類型加法恆等值的屬性會很有用。 這個功能有新的介面: 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 倉庫中建立新 議題 。 建立能處理任何數值類型的通用演算法。 利用這些介面建立演算法,其中型別參數僅實作部分類數字能力。 即使你沒有開發出使用這些功能的新介面,也可以嘗試在演算法中運用它們。