泛型代码中的算术
想在泛型代码中对任意类型 T 进行平方根运算,并将其乘以其自然对数?没问题。
引言
自 .NET 引入泛型(13 年前)以来,在泛型代码中进行算术运算一直都不容易。
public static T Sum<T>(T a, T b) => a + b; // Error, there's no + operator
这主要是因为像 int
这样的基本类型没有实现任何接口来暴露“加”、“乘”和“取反”等操作。即使是 .NET 4 或 .NET 4.5... 也没有。
public static T Sum<T>(T a, T b) where T: IMath<T> // no such thing
背景
2004 年,Rüdiger Klaehn 写了一篇文章,讲述了如何在泛型类中高效地进行算术运算。他的解决方案大致如下:
class Lists<T,M> where M: IMath<T>, new() { // M should be an empty structure, so we can create an instance "for free" private static M math = new M(); public static T Sum(List<T> list) { T sum = default(T); // zero for (int i = 0; i < list.Count; i++) sum = math.Add(sum, list[i]); return sum; } }
这里,IMath<T>
是一个类,它有一个 Add
方法,允许你将两个数字相加。在他写文章的时候,.NET JIT 无法高效地运行这段代码,但如今,这段代码的性能与专门用于整数加法的代码相当,因为 JIT 会为类型参数 M
(只要它是 struct)专门化类,并且对 Add
的调用可以被内联。
在此基础上扩展
我写了一个库 Loyc.Math,它提供了比 Rüdiger 最初设计的更广泛的数学运算。结合 Loyc.Essentials(定义了 Loyc.Math 实现的接口),你的泛型代码可以了解关于未知数值类型 T
的一切信息,并进行任何它想要的算术运算。例如,Loyc.Math 提供了移位操作(如果类型是整数,Shr(1) 是实际的右移;如果类型是浮点数,它会除以 2 的幂),你可以获取下一个更大的值(加一个 ULP),你可以计算整数中“1”的位数,还可以查询最大非无穷值和有效位数等属性。
在 Loyc.Math 中,MathI
是 int
的“数学提供者”结构,MathD
是 double
的提供者,MathL
是 long
的提供者,MathU8
是 byte
的提供者,依此类推。也提供了定点数甚至二维向量的数学结构。
不幸的是,为了获得高性能,你的进行数学运算的泛型类需要一个额外的泛型参数 M
,为用户提供这个参数往往是一个很大的麻烦。偶尔他们可以使用 var
来避免这种麻烦;例如,Range.Inclusive(1, 10)
返回一个从 1 到 10 的数字范围,表示为 NumRange<int,MathI>
。可以使用 var
来避免写出类型。
var range = Range.Inclusive(1, 10); foreach (int num in range) Console.WriteLine(num);
用户避免提及数学提供者的另一种方法是将启用了数学功能的实例保存在接口中。例如,NumRange<int,MathI>
实现 ICollection<int>
、IReadOnlyList<int>
和 IListSource<int>
。然而,这意味着你必须使用接口调用来访问对象,这在某些情况下可能很慢(例如,如果你每秒想进行数百万次调用)。此外,对于 NumRange<int,MathI>
来说,它是一个 struct
,将其视为像 ICollection<int>
这样的接口会导致装箱。
如果你的泛型类不需要进行高性能的算术运算,那么在使用 Loyc.Math
时,它就不需要额外的 M
泛型参数。相反,它可以从 Maths<T>
获取一个 IMath<T>
对象。例如,这里是上面代码的一个版本,它计算数字的总和速度较慢,但没有 M
参数:
class Lists<T> { private static IMath<T> math = Maths<T>.Math; private static IAdditionGroup<T> add = Maths<T>.AdditionGroup; public static T Sum(List<T> list) { T sum = default(T); // zero for (int i = 0; i < list.Count; i++) sum = add.Add(sum, list[i]); return sum; } }
该示例还添加了一个类型为 IAdditionGroup<T>
的新字段,并使用它来进行加法而不是 math
。 IMath<T>
提供了一整套数学运算,包括乘法、平方根、比较等,但由于 Sum
所需要做的只是加法,因此最好使用 IAdditionGroup
,这样它就可以与可以加减但不能例如排序的类型一起工作。考虑 Vector<T>
,它是一对数字 X
和 Y
;它们支持加减法,但很少有其他功能,因此整个 IMath
接口不可用。
注意:当所需的数学接口不可用时,Maths<T>
的成员为 null
。
注意:将来这段代码应该可以与 Vector<T>
一起工作,但截至 2016 年,Maths<T>
只支持基本类型。
示例:几何学
目前, Loyc.Math 的几种类型,如 Point<T>
、Vector<T>
和 BoundingBox<T>
,都使用 Maths<T>
来实现泛型算术。不幸的是,在使用这些数学对象时,算术运算很慢,因为没有独立的类型参数 M
。为了缓解这个问题,这些类具有扩展方法来为特定类型执行数学运算。这基本上是通过多次重复如下代码来实现的:
using T = System.Int32; using Point = Point<int>; using Vector = Vector<int>; using Point3 = Point3<int>; using Vector3 = Vector3<int>; using LineSegment = LineSegment<int>; using System; public static partial class PointMath { public static Vector Add(this Vector a, Vector b) { return new Vector(a.X+b.X, a.Y+b.Y); } public static Point Add(this Point a, Vector b) { return new Point(a.X+b.X, a.Y+b.Y); } public static Point Add(this Vector a, Point b) { return new Point(a.X+b.X, a.Y+b.Y); } public static Vector Sub(this Vector a, Vector b) { return new Vector(a.X-b.X, a.Y-b.Y); } public static Vector Sub(this Point a, Point b) { return new Vector(a.X-b.X, a.Y-b.Y); } public static Point Sub(this Point a, Vector b) { return new Point(a.X-b.X, a.Y-b.Y); } public static Vector Mul(this Vector p, T factor) { return new Vector(p.X*factor, p.Y*factor); } public static Point Mul(this Point p, T factor) { return new Point(p.X*factor, p.Y*factor); } public static Vector Div(this Vector p, T factor) { return new Vector(p.X/factor, p.Y/factor); } public static Point Div(this Point p, T factor) { return new Point(p.X/factor, p.Y/factor); } ... }
代码的每个副本都会改变 Point
的含义,以便为 Point<int>
、Point<double>
和 Point<float>
提供快速代码。
不幸的是,你必须使用丑陋的语法 pointA.Sub(pointB)
而不是 pointA - pointB
才能获得快速算术运算。对于 Point<T>
和 Vector<T>
也有运算符。
static IMath<T> m = Maths<T>.Math; public static Point<T> operator+(Point<T> a, Vector<T> b) { return new Point<T>(m.Add(a.X,b.X), m.Add(a.Y,b.Y)); } public static Point<T> operator+(Vector<T> a, Point<T> b) { return new Point<T>(m.Add(a.X,b.X), m.Add(a.Y,b.Y)); }
但这些运算符当然很慢。目前,C# 设计团队正在考虑是否向 C# 添加“所有内容扩展”,包括扩展运算符。这样的运算符将非常有用,允许运算符使用快速代码路径,而方法使用慢速路径(基于 Maths<T>.Math
的慢速方法仍然有用,以允许泛型代码轻松地对点和向量进行数学运算)。
下载
上面描述的泛型数学代码在 Loyc.Math.dll 中,已发布在 NuGet 上。源代码在此处:这里。
历史
我几年前就写了这个,但可以说我的一年新年决心之一是发布更多文章。