教程:探索 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 修饰符添加到不提供实现的任何静态成员。 下面的示例定义了一个 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> 接口中组合许多算术运算符的派生接口。 让我们应用这些类型来生成可对 T 使用任何数值类型的 Point<T> 记录。 可使用 + 运算符按一定的 XOffsetYOffset 来移动点。

首先使用 dotnet new 或 Visual Studio 创建一个新的控制台应用程序。 将 C# 语言版本设置为“预览版”,这将启用 C# 11 预览功能。 将以下元素添加到 <PropertyGroup> 元素中的 csproj 文件:

<LangVersion>preview</LangVersion>

注意

不能使用 Visual Studio UI 设置此元素。 你需要直接编辑项目文件。

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 };

为了使上述代码进行编译,你需要声明 T 支持 IAdditionOperators<TSelf, TOther, TResult> 接口。 该接口包含 operator + 静态方法。 它声明 3 个类型参数:一个用于左操作数,一个用于右操作数,一个用于结果。 某些类型对不同的操作数和结果类型实现 +。 添加要求类型参数 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 取决于类型。 答案:对 0 使用 IAdditiveIdentity<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 存储库中创建新问题。 生成适用于任何数值类型的泛型算法。 使用这些接口生成算法,其中类型参数只能实现一部分类似于数字的功能。 即使不生成使用这些功能的新接口,也可尝试在算法中使用它们。

请参阅