.NET 7 Preview 5 - 通用数学

107 阅读16分钟

在.NET 6中,我们预览了一个被称为通用数学的功能。从那时起,我们对该功能的实现进行了不断的改进,并对社区的各种反馈做出了回应,以确保相关的场景是可能的,而且必要的API是可用的。

如果你错过了最初的博文,Generic Math结合了泛型的力量和一个被称为static virtuals in interfaces 的新功能,允许.NET开发者从泛型代码中利用静态API,包括运算符。这意味着你可以获得泛型的所有功能,但现在可以将输入限制在类似数字的类型上,所以你不再需要为了支持多种类型而编写或维护许多几乎相同的实现。这也意味着你可以访问所有你喜欢的运算符,并且可以从泛型上下文中使用它们。也就是说,你现在可以有static T Add<T>(T left, T right) where T : INumber<T> => left + right; ,而在以前,这是不可能定义的。

就像泛型一样,这个功能对API作者来说是最有利的,他们可以简化他们需要维护的代码量。.NET图书馆就是这样做的,以简化作为LINQ一部分的Enumerable.MinEnumerable.Max API。其他开发者将间接受益,因为他们使用的API可能会开始支持更多的类型,而不需要对每一个数字类型进行明确支持。一旦一个API支持INumber<T> ,那么它应该与任何实现所需接口的类型一起工作。所有的开发者同样也会从拥有一个更一致的API表面和默认的更多功能中受益。例如,所有实现了IBinaryInteger<T> 的类型都将支持诸如+ (加法)、- (减法)、<< (左移)和LeadingZeroCount 等操作。

通用数学

让我们看看一个计算标准差的代码例子。对于那些不熟悉的人来说,这是一个用于统计学的数学函数,它建立在两个更简单的方法之上:SumAverage 。它基本上是用来确定一组数值的分布情况。

我们要看的第一种方法是Sum ,它只是把一组数值加在一起。该方法接收一个IEnumerable<T> ,其中T 必须是一个实现了INumber<T> 接口的类型。它返回一个具有类似约束的TResult (它必须是一个实现了INumber<TResult> 的类型)。因为这里有两个通用参数,所以它允许返回一个与它作为输入的不同类型。这意味着,例如,你可以做Sum<int, long> ,这将允许对一个int[] 的值进行求和,并返回一个64位的结果,以帮助避免溢出。TResult.Zero 有效地将0 的值作为TResultTResult.CreateCheckedvalueT 转换为TResult ,如果它太大或太小,不能适应目标格式,则抛出一个OverflowException 。这意味着,例如,如果其中一个输入值为负数或大于255Sum<int, byte> 将被抛出。

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.CreateChecked(value);
    }

    return result;
}

下一个方法是Average ,它只是将一组数值加在一起(调用Sum ),然后除以数值的数量。除了在Sum 中使用的概念外,它没有引入任何额外的概念。它确实显示了除法运算符的使用。

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.CreateChecked(sum) / TResult.CreateChecked(values.Count());
}

StandardDeviation 是最后一种方法,如上所述,它基本上确定了一组数值之间的距离。例如, 与 的偏差很大;另一方面, 的偏差要小得多,只有 。这个方法引入了一个不同的约束条件 ,表明返回类型必须是 浮点类型,如 ( ) 或 ( )。它引入了一个新的API ,在溢出时明确地饱和,或夹住值。也就是说,对于 ,它会将 转换为 ,因为 小于 的最小值。同样,它会将 转换为 ,因为 大于 的最大值。饱和是 浮点类型的默认行为,因为它们可以将正负无穷大作为各自的最小和最大值。其他唯一的新API是 ,它的行为与 或 一样,计算浮点值的 。{ 0, 50, 100 } 49.501 { 0, 5, 10 } 4.5092 IFloatingPointIeee754 IEEE 754 double``System.Double float``System.Single CreateSaturating byte.CreateSaturating<int>(value) -1 0 -1 0 256 255 256 255 IEEE 754 Sqrt Math.Sqrt MathF.Sqrt square root

public static TResult StandardDeviation<T, TResult>(IEnumerable<T> values)
    where T : INumber<T>
    where TResult : IFloatingPointIeee754<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.CreateSaturating(value) - average;
            return deviation * deviation;
        }));
        standardDeviation = TResult.Sqrt(sum / TResult.CreateSaturating(values.Count() - 1));
    }

    return standardDeviation;
}

这些方法可以用于任何实现了所需接口的类型,在.NET 7预览版5中,我们有20种类型实现了这些接口的开箱即用。下表给出了这些类型的简要描述、C#和F#的相应语言关键字(如果存在)以及它们实现的主要通用数学接口。关于这些接口的更多细节以及它们存在的原因将在后面的可用API部分提供。

.NET类型名称C#关键字F# 关键字实现的通用数学接口
System.BytebytebyteIBinaryInteger, IMinMaxValue, IUnsignedNumber
系统.Charchar字符二进制整数,IMinMaxValue,IUnsignedNumber
系统.小数十进制小数IFloatingPoint, IMinMaxValue
系统.双数双重浮点,双倍IBinaryFloatingPointIeee754, IMinMaxValue
系统.半数二进制浮点Ieee754, IMinMaxValue
系统.Int16短码二进制整数,IMinMaxValue,ISignedNumber
系统.Int32intint二进制整数,IMinMaxValue,ISignedNumber
系统.Int64int64二进制整数,IMinMaxValue,ISignedNumber
系统.Int128二进制整数,IMinMaxValue,ISignedNumber
系统.IntPtr编码本征二进制eger,IMinMaxValue,ISignedNumber
系统.数字.大整数二进制整数,无符号数
系统.数值.复数INumberBase, ISignedNumber
系统.Runtime.InteropServices.NFloatIBinaryFloatingPointIeee754, IMinMaxValue
系统.SByte编码编码二进制整数,IMinMaxValue,ISignedNumber
系统.单数浮点float32, singleIBinaryFloatingPointIeee754, IMinMaxValue
系统.UInt16ushortuint16IBinaryInteger, IMinMaxValue, IUnsignedNumber
系统.UInt32uintuint二进制整数,IMinMaxValue,无符号数
系统.UInt64ulonguint64二进制整数,IMinMaxValue, IUnsignedNumber
系统.UInt128二进制整数,IMinMaxValue,无符号数
系统.UIntPtr编码无符号IBinaryInteger, IMinMaxValue, IUnsignedNumber

这意味着,开箱即用的用户可以得到一套广泛的通用数学支持。随着社区为他们自己的类型采用这些接口,支持将继续增加。

没有语言支持的类型

读者可能会注意到,这里有几个类型在C# KeywordF# Keyword 栏中没有条目。虽然这些类型存在并且在BCL中被完全支持,但是像C#和F#这样的语言今天并没有为它们提供任何额外的支持,所以当某些语言功能不能与它们一起工作时,用户可能会感到惊讶。一些例子是,语言不会提供对字面意义(Int128 value = 0xF_FFFF_FFFF_FFFF_FFFF 无效)、常量(const Int128 Value = 0; 无效)、常量折叠(Int128 value = 5; 是在运行时评估的,而不是在编译时)的支持,或者其他各种仅限于有相应语言关键字的类型的功能。

没有语言支持的类型是。

  • System.Half 是一个16位的二进制浮点类型,实现了IEEE 754标准,与 和 很相似。它最初是在.NET 5中引入的。System.Double System.Single
  • System.Numerics.BigInteger 是一个任意精度的整数类型,自动增长以适应所代表的值。它最初是在.NET框架4.0中引入的。
  • System.Numerics.Complex 可以表示表达式 ,其中 和 是 , 是虚数单位。它最初是在.NET框架4.0中引入的。a + bi a b System.Double i
  • System.Runtime.InteropServices.NFloat 是一个可变精度的二进制浮点类型,实现了IEEE 754标准,和 很像,在32位平台上是32位(相当于 ),在64位平台上是64位(相当于 )。它最初在.NET 6中引入,主要是为了互操作。System.IntPtr System.Single System.Double
  • System.Int128 是一个128位有符号的整数类型。它在.NET 7中是新的。
  • System.UInt128 是一个128位无符号的整数类型。它在.NET 7中是新的。

自.NET 6以来的突破性变化

在.NET 6中推出的功能是一个预览版,因此,根据社区的反馈,对API表面进行了一些改变。这包括,但不限于。

  • System.IParseable 改名为System.IParsable
  • 将所有其他新的数字接口移至System.Numerics 命名空间
  • 引入INumberBase ,以便可以表示像System.Numerics.Complex 这样的类型。
  • 将IEEE 754的特定API分割成他们自己的IFloatingPointIeee754 接口,以便可以表示像System.Decimal 这样的类型。
  • 将各种API移到类型层次中的较低位置,如IsNaNMaxNumber APIs
    • 许多概念将返回一个常量值或成为各种类型的no-op
    • 尽管如此,它们的可用性仍然很重要,因为通用的确切类型是未知的,而且这些概念中的许多对更通用的算法很重要。

.NET的API审查是公开进行的,并通过现场直播的方式让所有人观看和参与其中。过去的API审查视频可以在我们的YouTube频道上找到。

通用数学功能的设计文档可以在GitHub上的dotnet/designsrepo中找到。

相应的更新文档的PR,围绕该功能的一般讨论,以及回溯到相关的API审查的链接也都可以找到

对其他语言的支持

F#也开始支持接口中的静态虚拟,GitHub上的fsharp/fslang-designrepo中应该很快会有更多细节。

使用建议的F#语法对C#的Sum 方法进行1对1的翻译,预计将是。

let Sum<'T, 'TResult when 'T :> INumber<'T> and 'TResult :> INumber<'TResult>>(values : IEnumerable<'T>) =
    let mutable result = 'TResult.Zero
    for value in values do
        result <- result 'TResult.CreateChecked(value)
    result

可用的API

数字和数学都是相当复杂的话题,可以深入的程度几乎没有限制。在编程中,通常只有一个与学校所学数学的松散映射,并且可能存在特殊的规则或考虑,因为执行是在一个资源有限的系统中进行的。因此,语言暴露了许多只有在某些类型的数字背景下才有意义的操作,或者主要作为硬件实际工作方式的性能优化而存在。它们所暴露的类型通常有明确的限制,它们所代表的数据的明确布局,围绕四舍五入或转换的不同行为,等等。

正因为如此,我们仍然需要既支持抽象意义上的数字,又支持特定的编程结构,比如浮点与整数、溢出、不可表示的结果;因此,作为设计这一功能的一部分,重要的是所暴露的接口既要足够精细,让用户可以在上面定义自己的接口,又要足够细化,使其易于使用。在这个范围内,有几个核心的数字接口,大多数用户会与之互动,如System.Numerics.INumberSystem.Numerics.IBinaryInteger ;还有更多的接口支持这些类型,并支持开发者为他们的领域定义自己的数字接口,如IAdditionOperatorsITrigonometricFunctions

哪些接口会被使用取决于声明的API的需求和它所依赖的功能。有一系列强大的API暴露出来,以帮助用户有效地理解他们所得到的值,并决定适当的方式来处理它,包括处理边缘情况(如负数、NaN、无穷大或虚值),有正确的转换(包括抛出、饱和或溢出时截断),并通过利用默认接口方法,使接口有足够的扩展性,以便向前发展。

数值接口

大多数用户将与之交互的类型是numeric interfaces 。这些定义了描述类似数字类型的核心接口以及它们可用的功能。

接口名称摘要
System.Numerics.IAdditiveIdentity暴露了以下的概念(x + T.AdditiveIdentity) == x
System.Numerics.IMinMaxValue暴露了T.MinValueT.MaxValue 的概念(像BigInteger这样的类型没有Min/MaxValue)。
System.Numerics.IMultiplicativeIdentity暴露了以下概念(x * T.MultiplicativeIdentity) == x
System.Numerics.IBinaryFloatingPointIeee754暴露了实现IEEE754标准的二进制浮点类型所共有的API
系统.数值.IB二进制整数暴露了二进制整数的通用API
System.Numerics.IBinaryNumber揭示了二进制数的通用API
系统.数值.IFloatingPoint揭示浮点类型的通用API
System.Numerics.IFloatingPointIeee754公开实现IEEE754标准的浮点类型的通用API
System.Numerics.INumber暴露了可比较的数字类型(实际上是 "Real "数域)所共有的API。
System.Numerics.INumberBase暴露了所有数字类型的通用API(实际上是 "复数 "领域)。
系统.Numerics.ISignedNumber暴露所有有符号的数字类型所共有的API(例如NegativeOne 的概念)。
System.Numerics.IUnsignedNumber暴露了所有无符号数类型的通用API

虽然这里有一些不同的类型,但大多数用户可能会直接使用INumber<TSelf> 。这大致相当于一些用户可能认识到的 "实数",意味着该值有一个符号和明确的顺序,使其成为IComparableINumberBase<TSelf> 对话更高级的概念,包括 "复数 "和 "虚数"。

大多数其他接口,如IBinaryNumberIFloatingPointIBinaryInteger ,之所以存在,是因为并非所有的操作对所有的数字都有意义。也就是说,有些地方的API只对已知的基于二进制的数值有意义,而有些地方的API只对浮点类型有意义。IAdditiveIdentity,IMinMaxValue, 和IMultiplicativeIdentity 接口的存在是为了涵盖类似数字类型的核心属性。特别是对于IMinMaxValue ,它的存在是为了允许访问一个类型的上界 (MaxValue) 和下界 (MinValue) 。某些类型如System.Numerics.BigInteger ,可能没有这样的边界,因此不实现这个接口。

IFloatingPoint<TSelf> 覆盖了 类型,如 , , 和 ,以及其他类型,如 。它所提供的API数量要少得多,预计大多数明确需要类似浮点类型的用户会使用 。目前还没有任何接口来描述 "定点 "类型,但是如果有足够的需求,未来可能会有这样的定义。IEEE 754 System.Double System.Half System.Single System.Decimal IFloatingPointIeee754

这些接口暴露了以前只在System.MathSystem.MathF 、和System.Numerics.BitOperations 中提供的API。这意味着像T.Sqrt(value) 这样的功能现在可以被任何实现IFloatingPointIeee754<T> (或者更具体的说是下面提到的IRootFunctions<T> 接口)的东西使用。

每个接口所暴露的一些核心API包括,但不限于以下内容。

接口名称API名称摘要
二进制整数(IBinaryInteger剩余数(DivRem同时计算商和余数。
前导零位数计算二进制表示中前导零位的数量。
流行计数计算二进制表示中的设置位数
左旋将比特向左旋转,有时也称为循环左移
右旋转将比特向右旋转,有时也称为循环右移
尾随零计数计算二进制表示中尾随零位的数量
浮动点(IFloatingPoint顶点将数值向正无穷大进发。+4.5变成+5,-4.5变成-4
下限向负无穷大的方向取值。+4.5变成+4,-4.5变成-5
四舍五入使用指定的舍入模式对数值进行舍入。
截断将数值向零舍入。+4.5变成+4,-4.5变成-4
IFloatingPointIeee754E获取一个代表欧拉数的值,其类型为
Epsilon获取大于零的最小可表示值,类型为
钠(NaN)获取一个代表NaN类型的值
负无穷获取一个代表-无限的类型的值。
负零获取一个代表-零的类型的值。
获取一个代表+Pi类型的值
正无穷大获取一个代表+无限的类型的值
获取一个代表+Tau的值,或者2 * Pi ,类型为+Tau。
-其他-补充下面函数中定义的全部接口。
数值钳制将一个值限制在不超过和不低于指定的最小和最大值。
复制符号将一个给定值的符号设置为与另一个指定值相同。
最大值返回两个值中较大的一个,如果其中一个输入值是NaN,则返回NaN。
MaxNumber返回两个值中的较大值,如果其中一个输入是NaN,则返回数字。
最小值返回两个数值中较小的一个,如果其中一个输入是NaN,则返回NaN。
最小数返回两个数值中较小的一个,如果一个输入是NaN,则返回数字。
符号负值返回-1,0代表零,正值返回+1
数基一个获取数值1 ,类型为
弧度获取该类型的小数,或基数。Int32返回2。十进制返回10
获取该类型的值0
创建检查从另一个值创建一个值,如果另一个值不能被表示,则抛出。
创建饱和的从另一个值创建一个值,如果另一个值不能被表示,则饱和。
创建截断型从另一个值中创建一个值,如果另一个值不能被表示,则截断该值。
是复数如果该值有一个非零的实数部分和一个非零的虚数部分,则返回真。
是偶数如果该值是偶数,则返回真。2.0返回真,2.2返回假。
是无限的如果该值不是无限的,也不是NaN,则返回真。
IsImaginaryNumber如果数值的实数部分为零,则返回 true。这意味着0是虚数,而1 + 1i 则不是。
是无限的如果该值代表无穷大,则返回真。
IsInteger如果该值是一个整数,则返回真。2.0和3.0返回真,2.2和3.1返回假。
IsNaN如果该值代表NaN,则返回true。
IsNegative如果值是负的,返回true,这包括-0.0。
IsPositive如果值是正的,则返回真,这包括0和+0.0。
真数(IsRealNumber如果该值的虚部为零,则返回真。这意味着0是实数,所有INumber<T>
是零如果值代表零,则返回真,这包括0、+0.0和-0.0。
MaxMagnitude返回具有更大绝对值的数值,如果任一输入为NaN,则返回NaN。
MaxMagnitudeNumber返回具有更大绝对值的数值,如果一个输入是NaN,则返回数字。
最小量级返回绝对值较小的数值,如果任一输入为NaN,则返回NaN。
最小量级数返回绝对值较小的数值,如果其中一个输入是NaN,则返回数字。
无符号数(ISignedNumber负一获取该类型的值-1

函数

函数接口定义了常见的数学API,其适用范围可能比特定的数字接口更广。它们目前都是由IFloatingPointIeee754 ,将来也可能由其他相关类型实现。

接口名称摘要
System.Numerics.IExponentialFunctions公开指数函数,支持e^x,e^x - 1,2^x,2^x - 1,10^x, 和10^x - 1
System.Numerics.IHyperbolicFunctions暴露了支持acosh(x),asinh(x),atanh(x),cosh(x),sinh(x), 和的双曲函数。tanh(x)
系统.Numerics.ILogarithmicFunctions暴露了对数函数,支持ln(x),ln(x + 1),log2(x),log2(x + 1),log10(x), 和log10(x + 1)
系统.Numerics.IPowerFunctions暴露了支持的幂函数x^y
系统.Numerics.IRootFunctions暴露了支持cbrt(x)sqrt(x)
系统.Numerics.ITrigonometricFunctions暴露了支持acos(x),asin(x),atan(x),cos(x),sin(x), 和的三角函数。tan(x)

解析和格式化

解析和格式化是编程中的核心概念。它们通常用于支持将用户输入转换为给定类型或向用户显示给定类型。

接口名称摘要
System.IFormattable暴露了对value.ToString(string, IFormatProvider)
系统.ISpanFormattable提供了对value.TryFormat(Span<char>, out int, ReadOnlySpan<char>, IFormatProvider)
对System.IParseable的支持提供对T.Parse(string, IFormatProvider)
支持System.ISpanParseable暴露了对T.Parse(ReadOnlySpan<char>, IFormatProvider)

运算器

通用数学的核心是能够将运算符作为接口的一部分来公开。.NET 7提供了以下接口,暴露了大多数语言所支持的核心运算符。这还包括以user-defined checked operatorsunsigned right shift 形式的新功能。

接口名称摘要
System.Numerics.IAdditionOperators暴露了x + ychecked(x + y) 操作符。
System.Numerics.IBitwiseOperators公开了x & y,`xy,x ^ y, 和~x` 操作符。
System.Numerics.IComparisonOperators公开了x < y,X > y,x <= y, 和x >= y 操作符。
System.Numerics.IDecrementOperators公开了--x,checked(--x),x--, 和checked(x--) 操作符。
System.Numerics.ID DivisionOperators公开了x / ychecked(x / y) 操作符
System.Numerics.IEqualityOperators揭示了x == yx != y 操作符
System.Numerics.IIncrementOperators暴露了++x,checked(++x),x++, 和checked(x++) 操作符。
System.Numerics.IModulusOperators公开了x % y 操作符
System.Numerics.IMultiplyOperators暴露了x * ychecked(x * y) 操作符
System.Numerics.IShiftOperators公开了x << y,x >> y, 和x >>> y 操作符。
System.Numerics.ISsubtractionOperators公开了x - ychecked(x - y) 操作符
System.Numerics.IUnaryNegationOperators公开了-xchecked(-x) 操作符。
系统.数值.I单项加法运算符暴露了+x 操作符

用户定义的检查运算符

User-defined checked operators 允许提供一个不同的实现,它将抛出 ,而不是默默地截断其结果。通过使用 关键字或在你的项目设置中设置 ,C#代码可以使用这些替代实现。截断的版本可以通过使用 关键字或确保 是 (这是新项目的默认经验)。System.OverflowException checked <CheckForOverflowUnderflow>true</CheckForOverflowUnderflow> unchecked CheckForOverflowUnderflow false

一些类型,如浮点类型,可能没有不同的行为,因为它们saturatePositiveInfinityNegativeInfinity ,而不是截断。BigInteger 是另一种类型,在运算符的未选中和选中版本之间没有不同的行为,因为该类型只是增长到适合该值。第三方类型也可能有他们自己的独特行为。

开发人员可以通过将checked 关键字放在operator 关键字之后来声明他们自己的user-defined checked operators 。例如,public static Int128 operator checked +(Int128 left, Int128 right) 声明了一个checked addition 操作符,public static explicit operator checked int(Int128 value) 声明了一个checked explicit conversion 操作符。

无符号右移

无符号右移(>>>) 允许发生不带符号的移位。也就是说,对于-8 >> 2 ,结果是-2 ,而-8 >>> 2+1073741822

当看十六进制或二进制表示时,这更容易理解。对于x >> y ,值的符号是保留的,因此对于正值0 ,而对于负值1 ,则移入。然而,对于x >>> y ,值的符号被忽略了,0 总是移入。这类似于首先将值转换为相同符号的unsigned 类型,然后进行移位,也就是类似于(int)((uint)x >> y) 对于int

表达式十进制十六进制二进制
-8-80xFFFF_FFF80b1111_1111_1111_1111_1111_1111_1111_1000
-8 >> 2-20xFFFF_FFFE0b1111_1111_1111_1111_1111_1111_1111_1110
-8 >>> 2+1,073,741,8220x3FFF_FFFE0b0011_1111_1111_1111_1111_1111_1111_1110

关闭

现在在泛型上下文中可用的功能量相当大,允许你的代码更简单、更可维护、更有表现力。泛型数学将使每个开发人员能够实现更多的功能,我们很高兴看到你决定如何利用它!

The post.NET 7 Preview 5 - Generic Mathappeared first on.NET Blog.