如果你曾经想使用通用类型的运算符,或者认为接口可以通过支持定义静态方法作为其契约的一部分来改进,那么这篇博文就是为你准备的。随着.NET 6的推出,我们将推出新的通用数学和接口中的静态抽象功能的预览版。 这些功能以预览的形式发布,以便我们能够从社区获得反馈,并建立一个更有说服力的功能。因此,它们不支持在.NET 6的生产环境中使用。我们强烈建议你试用该功能,如果你觉得有什么场景或功能缺失或可以改进,请提供反馈。
要求预览功能的属性
其他一切的核心是新的RequiresPreviewFeatures属性和相应的分析器。这个属性允许我们注释新的预览类型和现有类型上的新预览成员。有了这个能力,我们就可以在一个支持的主要版本中发送一个不支持的预览功能。分析器寻找被消费的具有RequiresPreviewFeatures 属性的类型和成员,如果消费者本身没有被标记为RequiresPreviewFeatures ,将给出一个诊断。为了在预览功能的范围内提供灵活性,该属性可以在成员、类型或组件级别上应用。
因为预览功能不支持在生产中使用,而且API在被支持之前可能会有突破性的变化,你必须选择使用它们。对于没有选择使用预览功能的任何调用站点,分析器将产生构建错误。分析器在.NET 6 Preview 7中不可用,但将包含在.NET 6 RC1中。
接口中的静态摘要
C#正计划引入一项新的功能,即接口中的静态摘要。如其名所示,这意味着你现在可以将静态抽象方法作为接口的一部分来声明,并在派生类型中实现它们。一个简单但强大的例子是在IParseable ,它是现有IFormattable 的对应版本。IFormattable 允许你定义一个为给定类型生成格式化字符串的契约,而IParseable 允许你定义一个解析字符串的契约,以创建一个给定类型:
public interface IParseable<TSelf>
where TSelf : IParseable<TSelf>
{
static abstract TSelf Parse(string s, IFormatProvider? provider);
static abstract bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, out TSelf result);
}
public readonly struct Guid : IParseable<Guid>
{
public static Guid Parse(string s, IFormatProvider? provider)
{
/* Implementation */
}
public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, out Guid result)
{
/* Implementation */
}
}
:。
- 你现在可以声明同时是
static和的接口成员。abstract - 这些成员目前不支持默认接口方法(DIM),因此
static和virtual不是一个有效的组合 - 这个功能只适用于接口,不适用于其他类型,如
abstract class - 这些成员不能通过接口访问,也就是
IParseable<Guid>.Parse(someString, null),会导致编译错误。
为了详细说明最后一点,通常abstract 或virtual 成员是通过某种虚拟调度来调用的。对于静态方法,我们没有任何对象或实例可以携带相关的状态来进行真正的虚拟调度,因此运行时无法确定IParseable<Guid>.Parse(...) 应该解析为Guid.Parse 。为了使其发挥作用,我们需要在某个地方指定实际的类型,这可以通过泛型来实现:
public static T InvariantParse<T>(string s)
where T : IParseable<T>
{
return T.Parse(s, CultureInfo.InvariantCulture);
}
通过以上述方式使用泛型,运行时能够通过在所使用的具体T 上查找来确定应该解析哪个Parse 方法。如果用户指定了InvariantParse<int>(someString) ,它将解析到System.Int32 上的解析方法,如果他们指定了InvariantParse<Guid>(someString) ,它将解析到System.Guid 上的解析方法,以此类推。这种一般模式有时被称为奇怪的重复模板模式(CRTP),是允许该功能工作的关键。
关于为支持该功能而进行的运行时修改的更多细节可以在这里找到。
通用数学
.NET中的一个长期要求的功能是能够在通用类型上使用运算符。使用接口中的静态抽象和.NET中公开的新接口,你现在可以编写这种代码:
public static TResult Sum<T, TResult>(IEnumerable<T> values)
where T : INumber<T>
where TResult : INumber<TResult>
{
TResult result = TResult.Zero;
foreach (var value in values)
{
result += TResult.Create(value);
}
return result;
}
public static TResult Average<T, TResult>(IEnumerable<T> values)
where T : INumber<T>
where TResult : INumber<TResult>
{
TResult sum = Sum<T, TResult>(values);
return TResult.Create(sum) / TResult.Create(values.Count());
}
public static TResult StandardDeviation<T, TResult>(IEnumerable<T> values)
where T : INumber<T>
where TResult : IFloatingPoint<TResult>
{
TResult standardDeviation = TResult.Zero;
if (values.Any())
{
TResult average = Average<T, TResult>(values);
TResult sum = Sum<TResult, TResult>(values.Select((value) => {
var deviation = TResult.Create(value) - average;
return deviation * deviation;
}));
standardDeviation = TResult.Sqrt(sum / TResult.Create(values.Count() - 1));
}
return standardDeviation;
}
这是通过暴露几个新的静态抽象接口来实现的,这些接口对应于语言中可用的各种运算符,并提供一些其他接口来代表常见的功能,如解析或处理数字、整数和浮点类型。这些接口是为可扩展性和可重用性而设计的,因此通常代表单个运算符或属性。它们明确地不配对操作,如乘法和除法,因为这对所有类型都不正确。例如,Matrix4x4 * Matrix4x4 是有效的,Matrix4x4 / Matrix4x4 是无效的。同样地,它们通常允许输入和结果类型不同,以支持诸如double = TimeSpan / TimeSpan 或Vector4 = Vector4 * float 的情况。
如果你有兴趣了解我们所暴露的接口,请看一下设计文件,它对所暴露的内容做了更详细的说明:
| 操作接口名称 | 总结 | |
|---|---|---|
| IParseable | Parse(string, IFormatProvider) | |
| ISpanParseable | Parse(ReadOnlySpan<char>, IFormatProvider) | |
| IAdditionOperators | x + y | |
| IBitwiseOperators | x & y, `x | y, x ^ y, and ~x` |
| IComparisonOperators | x < y, x > y, x <= y, and x >= y | |
| IDecrementOperators | --x and x-- | |
| IDivisionOperators | x / y | |
| IEqualityOperators | x == y and x != y | |
| IIncrementOperators | ++x and x++ | |
| IModulusOperators | x % y | |
| IMultiplyOperators | x * y | |
| IShiftOperators | x << y and x >> y | |
| ISubtractionOperators | x - y | |
| IUnaryNegationOperators | -x | |
| IUnaryPlusOperators | +x | |
| IAdditiveIdentity | (x + T.AdditiveIdentity) == x | |
| IMinMaxValue | T.MinValue and T.MaxValue | |
| IMultiplicativeIdentity | (x * T.MultiplicativeIdentity) == x | |
| IBinaryFloatingPoint | Members common to binary floating-point types | |
| IBinaryInteger | Members common to binary integer types | |
| IBinaryNumber | Members common to binary number types | |
| IFloatingPoint | Members common to floating-point types | |
| INumber | Members common to number types | |
| ISignedNumber | Members common to signed number types | |
| IUnsignedNumber | Members common to unsigned number types |
二进制浮点类型是System.Double (double),System.Half, 和System.Single (float).ushort System.UInt32 二进制整数类型有System.Byte (byte),System.Int16 (short),System.Int32 (int),System.Int64 (long),System.IntPtr (nint),System.SByte (sbyte),System.UInt16 (uint),System.UInt64 (ulong), 和System.UIntPtr (nuint)。上述几个接口也由其他各种类型实现,包括System.Char,System.DateOnly,System.DateTime,System.DateTimeOffset,System.Decimal,System.Guid,System.TimeOnly, 和System.TimeSpan 。
由于该功能处于预览阶段,有很多方面仍在飞行中,在下一次预览或该功能正式发布前可能会发生变化。例如,根据已经收到的反馈,我们可能会将INumber<TSelf>.Create 改为INumber<TSelf>.CreateChecked ,将INumber<TSelf>.CreateSaturating 改为INumber<TSelf>.CreateClamped 。我们还可能公开新的或额外的概念,如IConvertible<TSelf> 或支持矢量类型和操作的接口。
如果上述任何一项或其他功能对你来说很重要,或者你觉得可能会影响该功能在你自己代码中的可用性,请务必提供反馈(.NET运行时或库、C#语言和C#编译器通常是不错的选择)。特别是:
- 检验过的运算符目前无法实现,因此
checked(x + y),无法检测到溢出:csharplang#4665 - 没有简单的方法可以从有符号类型到无符号类型,反之亦然,因此选择逻辑(无符号)与算术(有符号)移位是不可能的:csharplang#4682
- 移位要求右手边是
System.Int32,因此可能需要额外的转换:csharplang#4666 - 所有的API目前都是显式实现的,其中许多可能会在功能发布时成为隐式可用的类型
试用这些功能
为了试用这些功能,需要几个步骤:
- 在命令行或你喜欢的IDE中创建一个新的针对.NET 6的C#控制台应用程序




- 编辑项目文件,通过设置
EnablePreviewFeatures属性为true来选择使用预览功能,并引用System.Runtime.ExperimentalNuGet包。

<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<EnablePreviewFeatures>true</EnablePreviewFeatures>
<LangVersion>preview</LangVersion>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.Runtime.Experimental" Version="6.0.0-preview.7.21377.19" />
</ItemGroup>
</Project>
- 创建一个通用类型或方法,并将其限制在一个新的静态抽象接口中。
// See https://aka.ms/new-console-template for more information
using System.Globalization;
static T Add<T>(T left, T right)
where T : INumber<T>
{
return left + right;
}
static T ParseInvariant<T>(string s)
where T : IParseable<T>
{
return T.Parse(s, CultureInfo.InvariantCulture);
}
Console.Write("First number: ");
var left = ParseInvariant<float>(Console.ReadLine());
Console.Write("Second number: ");
var right = ParseInvariant<float>(Console.ReadLine());
Console.WriteLine($"Result: {Add(left, right)}");
- 运行该程序并观察输出结果:
第一个数字:5
第二个数字:3.14
结果:8.14
结束
虽然我们只是简单地介绍了新的类型,并给出了一个简单的使用例子,但潜在的应用要广泛得多。我们期待着你的反馈,看看你能用什么了不起的方法来改进你现有的代码或创建新的代码。你可以在上面链接的任何现有问题上记录反馈,或者酌情在相关的GitHub仓库(.NET运行时或库、C#语言和C#编译器通常是不错的选择)上开立新问题。