使用接口静态虚拟成员 可以定义包含 重载运算符 或其他静态成员的接口。 使用静态成员定义接口后,可以使用这些接口作为 约束 来创建使用运算符或其他静态方法的泛型类型。 即使未使用重载运算符创建接口,也可能会受益于此功能和语言更新启用的泛型数学类。
本教程中,您将学习如何:
- 定义具有静态成员的接口。
- 使用接口来定义那些已定义运算符的接口的实现类。
- 创建依赖于静态接口方法的泛型算法。
先决条件
- 最新的 .NET SDK
- Visual Studio Code 编辑器
- C# 开发套件
静态抽象接口方法
让我们从一个示例开始。 以下方法返回两 double 个数字的中点:
public static double MidPoint(double left, double right) =>
(left + right) / (2.0);
同一逻辑适用于任何数字类型:int、、shortlong、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值,这要求分母与这两个参数的类型相同。
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> 类型:两者都存储两个值,表示数据存储,而不是复杂的行为。 实现 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);
前面的代码无法编译,因为它依赖于类型。 答案:使用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 存储库中创建新 问题 。 生成可用于任何数值类型的泛型算法。 使用这些接口生成算法,其中类型参数仅实现一部分类似数字的功能。 即使没有生成使用这些功能的新接口,也可以尝试在算法中使用它们。