Shone.Math开源系列1 — 基于.NET 5实现Math<T>泛型数值计算
作者:Shone
.NET 5 preview 4已经可用了,从微软Build2020给出的信息看,.NET 5将实现框架统一,.NET 6将实现界面统一。开源的.NET更加有活力,咱们也从基础开始贡献一点微薄力量,拥抱开源,拥抱.NET未来。
Shone.Math是一个支持Math<T>泛型数值计算和Real实数运算(浮点数、分数、PI,E,Log,Exp等无理数)的轻量级基础数学库。该项目开源地址https://github.com/shonescript/Shone.Math,是本人把多年代码积累正式转向.NET 5,也是我的第一个开源项目,请大家多多支持了。
一、.NET泛型数值计算优势
.NET 2.0开始支持泛型编程,支持IEnumerable<T>, List<T>, Func<T,T,…>等各种泛型类型,提高了编程效率和质量,这是公认的价值。
但是对于基础类似的数值运算,.NET没有默认泛型实现方式。*上有大量关于泛型数值计算的讨论,C#9.0的部分草案建议也提出添加对泛型计算的支持。
在大量处理数据时,特别是几何或空间数据计算时,泛型数值计算的主要优势是:
(1)可重用:专注于数值计算算法,不用为每种数据编写实现,提高开发效率;
(2)无装箱:直接支持各种数值类型,减少struct数值类型无装箱和拆箱,提高运行效率;
(3)动态切换:可在运行时动态切换数据类型,从float, double, decimal,根据需要可随时提高计算精度,平衡计算性能和存储占用。
二、.NET泛型数值计算难点
泛型数值计算优势这么多,那就赶快实现吧。但是彻底实现有点难,真的难,需要语言、甚至编译器底层支持。对于.net和C#语言是这样,其他大部分语言也是这样。
泛型数值计算的难点在于:
(1)数值类型很多:.NET有13中基础数值类型,包括bool, char, byte, sbyte, short, ushort, int, uint, long, ulong, float, double, decimal。除此之外,还有我自己编写实数类Real及其派生类Ration,IrrationXXX等,另有11种。(C#比较全面,其他语言略有差异)
(2)运算能力不同:浮点数支持大部分计算,整数只支持+-*/,char和bool型支持的更少,不支持运算编译会报错。(各语言类似)
(3)运算实现差异:.NET CLR为了提高效率,int, float, double等符号运算直接使用指令实现,在类型定义中找不到方法,而decimal则使用运算符重载实现。(其他语言应该也有类似技巧)
(4)泛型实现机制:.NET泛型属于运行时泛型,泛型T的可使用方法需要从约束推导。由于int, float, double等是系统特殊类型,其基类直接是object,没有暴露.Add/Multiply虚方法,也没有提供静态运算符扩展重载,因此没法直接从object做泛型。其实通过dynamic也可以,但动态类型开销巨大,而且必须进行装箱、拆箱,不是好办法。(C++等采用编译时模板实现泛型的,比较容易实现数值泛型,其他语言java以及动态语言有装箱、拆箱问题)
(5)泛型数据转换:泛型T在动态运行时如何与其他数据进行转换也是个难题,而且要求避免装箱、拆箱问题。(编译时泛型语言实现比较困难,而动态语言有装箱、拆箱问题)
(6)动态类型切换:在运行时动态切换数据类型,这个更难(静态语言较难,动态语言有优势)。
总之,泛型数值计算确实很难,各种实现都有利弊,否则微软应该在.NET2.0推出时就有解决方案,肯定是经过平衡取舍,只好留给开发者根据需要自己实现了。
三、Shone.Math泛型实现方法
Shone.Math有针对性解决了大部分上述障碍,填了很多坑,尽量做到易用性、性能等各方面平衡。各位有兴趣可以到开源项目地址,下载dll试用或代码研究一下,有BUG、问题或建议可以在上面直接提出来,也可以pull参与项目代码完善和实现。
1、关键在于delegate和Math<T>
泛型数值计算实现方法很多,不外乎通过inerface、struct、以及delegate这三种进行各种姿势的补锅。我个人研究下来,interface实现很难避开装箱拆箱问题,struct需要组装等开销也不小使用不便,delegate是微软留给这个问题解决办法的一线生机,虽然还有小遗憾,但总体优雅直接。
delegate大家都知道,其实就是.NET托管世界的函数指针,对应C/C++函数指针功能。数值计算各类符号和函数说白了不就是函数调用,完全可以用函数指针、或delegate动态表达,不需要各种代码直接表达。那C#打开unsafe模式,也有函数指针,为什么不用呢?因为C#的指针是简化版,不支持泛型。现在很清楚了,只能用delegate,那么在哪里用呢?
平时不管大家编什么程序,应该都用过Math.Abs, Cos, Sin, Log, Exp等函数吧,主要支持double数值各种计算,.NET Core中后来还提供了MathF静态类,提供对应的float数值各种计算。看到这里应该有点明白了吧,与其每种数据类型写一个MathXXX静态类,不如直接提供一个Math<T>泛型静态类,包装所有计算的delegate,提供泛型调用就可以了,简单直接明了。
2、Math<T>有哪些内容
从上面叙述可以看出,Math<T>应该包含数值类型和MathXXX的常用方法,主要常量和方法分类列出如下:
public static class Math<T>
{
//各种常量
public static T MinValue;
public static T MaxValue;
public static T Epsilon;
public static T NegativeInfinity;
public static T PositiveInfinity;
public static T NaN;
public static T Zero;
public static T One;
public static T MinusOne;
public static T PI;
public static T E;
public static T RadFactor;
public static T DegFactor;
//各种方法
public static Func<T, bool> IsNormal = xTrue;
public static Func<T, bool> IsSubnormal = xFalse;
public static Func<T, bool> IsFinite = xTrue;
public static Func<T, bool> IsNaN = xFalse;
public static Func<T, bool> IsInfinity = xFalse;
public static Func<T, bool> IsPositiveInfinity = xFalse;
public static Func<T, bool> IsNegativeInfinity = xFalse;
public static Func<T, bool> IsNegative = x => LessThan(x, Zero);
public static Func<T, T> Negate = x => FromDecimal(-ToDecimal(x));
public static Func<T, T> Increase = x => FromDecimal(ToDecimal(x) + 1);
public static Func<T, T> Decrease = x => FromDecimal(ToDecimal(x) - 1);
public static Func<T, T> Comp = x => FromLong(~ToLong(x));
public static Func<T, bool> Not = x => !ToBool(x);
public static Func<T, T, T> Add = (x, y) => FromInt(ToInt(x) + ToInt(y));
public static Func<T, T, T> Subtract = (x, y) => FromInt(ToInt(x) - ToInt(y));
public static Func<T, T, T> Multiply = (x, y) => FromInt(ToInt(x) * ToInt(y));
public static Func<T, T, T> Divide = (x, y) => FromInt(ToInt(x) / ToInt(y));
public static Func<T, T, T> Modulus = (x, y) => FromInt(ToInt(x) % ToInt(y));
public static Func<T, T, T> BitAnd = (x, y) => FromLong(ToLong(x) & ToLong(y));
public static Func<T, T, T> BitOr = (x, y) => FromLong(ToLong(x) | ToLong(y));
public static Func<T, T, T> BitXOr = (x, y) => FromLong(ToLong(x) ^ ToLong(y));
public static Func<T, T, T> LeftShift = (x, y) => FromLong(ToLong(x) << ToInt(y));
public static Func<T, T, T> RightShif = (x, y) => FromLong(ToLong(x) >> ToInt(y));
public static Func<T, T, bool> And = (x, y) => ToBool(x) && ToBool(x);
public static Func<T, T, bool> Or = (x, y) => ToBool(x) || ToBool(x);
public static Func<T, T, bool> LessThan = (x, y) => ToInt(x) < ToInt(y);
public static Func<T, T, bool> GreatThan = (x, y) => ToInt(x) > ToInt(y);
public static Func<T, T, bool> LessEqual = (x, y) => ToInt(x) <= ToInt(y);
public static Func<T, T, bool> GreatEqual = (x, y) => ToInt(x) >= ToInt(y);
public static Func<T, T, bool> Equal;
public static Func<T, T, bool> NotEqual;
public static Func<bool, T> FromBool;
public static Func<char, T> FromChar;
public static Func<sbyte, T> FromSByte;
public static Func<byte, T> FromByte;
public static Func<short, T> FromShort;
public static Func<ushort, T> FromUShort;
public static Func<int, T> FromInt;
public static Func<uint, T> FromUInt;
public static Func<long, T> FromLong;
public static Func<ulong, T> FromULong;
public static Func<float, T> FromFloat;
public static Func<double, T> FromDouble;
public static Func<decimal, T> FromDecimal;
public static Func<Real, T> FromReal;
public static Func<T, bool> ToBool;
public static Func<T, char> ToChar;
public static Func<T, sbyte> ToSByte;
public static Func<T, byte> ToByte;
public static Func<T, short> ToShort;
public static Func<T, ushort> ToUShort;
public static Func<T, int> ToInt;
public static Func<T, uint> ToUInt;
public static Func<T, long> ToLong;
public static Func<T, ulong> ToULong;
public static Func<T, float> ToFloat;
public static Func<T, double> ToDouble;
public static Func<T, decimal> ToDecimal;
public static Func<T, Real> ToReal;
public static Func<string, T> Parse;
public static TryParseDelegate TryParse;
public static Func<T, int> Sign = x => Math.Sign(ToInt(x));
public static Func<T, T> Abs => x => FromInt(Math.Abs(ToInt(x)));
public static Func<T, T> Sqrt = x => FromDouble(Math.Sqrt(ToDouble(x)));
public static Func<T, T> Cbrt = x => FromDouble(Math.Pow(ToDouble(x), 1d / 3d));
public static Func<T, T> Exp = x => FromDouble(Math.Exp(ToDouble(x)));
public static Func<T, T, T> Pow = (x, y) => FromDouble(Math.Pow(ToDouble(x), ToDouble(y)));
public static Func<T, T> Log = x => FromDouble(Math.Log(ToDouble(x)));
public static Func<T, T> Log2 = x => FromDouble(Math.Log2(ToDouble(x)));
public static Func<T, T> Log10 = x => FromDouble(Math.Log10(ToDouble(x)));
public static Func<T, T, T> Logx = (x, y) => FromDouble(Math.Log(ToDouble(x), ToDouble(y)));
public static Func<T, T> Floor = xSelf;
public static Func<T, T> Ceiling = xSelf;
public static Func<T, T> Round = xSelf;
public static Func<T, T> Truncate = xSelf;
public static Func<T, T, T> Min = (x, y) => FromDouble(Math.Min(ToDouble(x), ToDouble(y)));
public static Func<T, T, T> Max = (x, y) => FromDouble(Math.Max(ToDouble(x), ToDouble(y)));
public static Func<T, T> Sin = x => FromDouble(Math.Sin(ToDouble(x)));
public static Func<T, T> Cos = x => FromDouble(Math.Cos(ToDouble(x)));
public static Func<T, T> Tan = x => FromDouble(Math.Tan(ToDouble(x)));
public static Func<T, T> Sinh = x => FromDouble(Math.Sinh(ToDouble(x)));
public static Func<T, T> Cosh = x => FromDouble(Math.Cosh(ToDouble(x)));
public static Func<T, T> Tanh = x => FromDouble(Math.Tanh(ToDouble(x)));
public static Func<T, T> Asin = x => FromDouble(Math.Asin(ToDouble(x)));
public static Func<T, T> Acos = x => FromDouble(Math.Acos(ToDouble(x)));
public static Func<T, T> Atan = x => FromDouble(Math.Atan(ToDouble(x)));
public static Func<T, T, T> Atan2 = (x, y) => FromDouble(Math.Atan2(ToDouble(x), ToDouble(y)));
public static Func<T, T> Asinh = x => FromDouble(Math.Asinh(ToDouble(x)));
public static Func<T, T> Acosh = x => FromDouble(Math.Acosh(ToDouble(x)));
public static Func<T, T> Atanh = x => FromDouble(Math.Atanh(ToDouble(x)));
public static Func<T, T> SinDeg = x => Sin(Multiply(x, RadFactor));
public static Func<T, T> CosDeg = x => Cos(Multiply(x, RadFactor));
public static Func<T, T> TanDeg = x => Tan(Multiply(x, RadFactor));
public static Func<T, T> SinhDeg = x => Sinh(Multiply(x, RadFactor));
public static Func<T, T> CoshDeg = x => Cosh(Multiply(x, RadFactor));
public static Func<T, T> TanhDeg = x => Tanh(Multiply(x, RadFactor));
public static Func<T, T> AsinDeg = x => Multiply(Asin(x), DegFactor);
public static Func<T, T> AcosDeg = x => Multiply(Acos(x), DegFactor);
public static Func<T, T> AtanDeg = x => Multiply(Atan(x), DegFactor);
public static Func<T, T, T> AtanDeg2 = (x, y) => Multiply(Atan2(x, y), DegFactor);
public static Func<T, T> AsinhDeg = x => Multiply(Asinh(x), DegFactor);
public static Func<T, T> AcoshDeg = x => Multiply(Acosh(x), DegFactor);
public static Func<T, T> AtanhDeg = x => Multiply(Atanh(x), DegFactor);
}
3、Math<T>实现原则
Math<T>实现还是有些技巧和原则的:
(1)有默认实现:所有常量都有默认值,方法都有默认实现,可能效率不高,但支持所有数据类型,而且可根据需要覆盖重载。这样整数(包括bool和char)也能进行各种Log, Sin运算,只不过运算结果进行了取整,不会报错。
(2)一次静态初始化:每个类型的初始化放在Math<T>的静态构造函数中,只有第一次使用时有点开销,后续调用没有任何性能损失。
4、Math<T>解决问题
(1)24个数值类型全部支持:其他自定义类型只要提供相关实现,也可以扩展支持到Math<T>中,我的Real类型就是这样干的。本系列博客会有专门文章介绍。
(2)统一提供所有运算符:不管数据类型,来者不拒,统统支持。
(3)共性默认,个性重载:所有实现方法提供默认算法实现,常用热点函数直接使用反射,从数据类型、Math、MathF、甚至DecimalEx等中抓取delegate进行覆盖重载,性能与原始实现接近。
(4)数值和引用泛型都支持:int, float, double等系统特殊类型为struct,直接按强类型运算,无装箱拆箱开销。Real等实数类型为object引用类型,可*转换,也无装箱拆箱开销。
(5)统一提供泛型数据转换:从上面的Math<T>可以看到,该类中包含了14个FromXXX和14个数据ToXXX进出函数,涵盖最常用的所有数据转换情况,使用起来非常方便。
(6)为动态切换奠定基础:有了Math<T>,可以调用typeof(Math<>).MakeGenericType()在运行时实现Math<T>的动态调用,当然要支持动态切换数据类型好需要一些技巧和实现。目前版本Shone.Math暂不支持,后续我会补充实现,并在系列中重点介绍。
四、Shone.Math泛型使用方法
Shone.Math只有一个dll文件,除了.NET5系统外无任何外部依赖。注意:Shone.Math支持.NET5以上版本,一方面是拥抱未来向前看,另一方面是开始时发现.NET4和.NET5差好多内容,如MathF类,Math.Asinh,Acosh,Atanh,还有各种Span<T>,Memory<T>等高级类型,这也符合.NET5一统江湖的趋势。
1、安装Visual Studio 2019
更新到最新版,在选项设置中打开.net preview支持。
2、下载nuget包或github代码
Nuget包:https://www.nuget.org/packages/Shone.Math/1.0.0
源代码:https://github.com/shonescript/Shone.Math/releases
3、引用nuget包或Shone.Math.dll到你的项目中
4、添加命名空间using Shone;
5、愉快地使用Math<T>方法或扩展
using Shone; // import Shone namespace
var d = Math<decimal>.Pow(5,3); // use just like Math.Pow, but it is generic now!
var x = 5m.Pow(3); // write in dot style
var ds = new double[]{5m, 6m, 7m}.Pow(3); // calculate array easily
五、Math<T>唯一遗憾
由于.NET目前暂不支持泛型静态运算符扩展重载,因此还无法使用+,-,*,/等符号书写泛型计算表达式,编程代码有所冗余。不过据说C#9.0会解决该问题,那就拭目以待,如果有Shone.Math会站第一排给予支持了。
没有运算符,做一下sin((x+y)/2)泛型计算的代码刚开始是这样:
Math<T>.Sin(Math<T>.Divide(Math<T>.Add(x, y), FromInt(2))
这很罗嗦了,为此Shone.Math专门提供了MyNum的扩展类,可以简化成那样:
x.Add(y).Divide(FromInt(2)).Sin()
这不就是传说中的Linq流派写法,已经比较接近符号写法了,你说还要哪样。
六、小结
Shone.Math通过各种精巧实现,提供了统一的泛型数值计算静态类Math<T>,为开发各类自定义数值、几何、空间、公式解析等泛型数值应用打下了坚实基础。本系列下一章节将介绍Shone.Math的一些.NET5专用高级特性如ref, Span, Memory的泛型数值计算扩展。
今年初我个人开始全面转向使用.NET 5开发,感觉非常简洁顺畅,结合C#语言新特性nuget和github工作流。基于.NET和C#语言层面开发已经酸爽无比,社区各类开源项目也在不断增强,希望也从自己做起,通过Shone.Math为.NET社区做点贡献。
声明:原创文章欢迎转载,但请注明出处,https://www.cnblogs.com/ShoneSharp。