Shone.Math開源系列1 — 基於.NET 5實現Math泛型數值計算

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等無理數)的輕量級基礎數學庫。該項目開源地址//github.com/shonescript/Shone.Math,是本人把多年代碼積累正式轉向.NET 5,也是我的第一個開源項目,請大家多多支持了。

一、.NET泛型數值計算優勢

.NET 2.0開始支持泛型編程,支持IEnumerable<T>, List<T>, Func<T,T,…>等各種泛型類型,提高了編程效率和質量,這是公認的價值。

但是對於基礎類似的數值運算,.NET沒有默認泛型實現方式。StackOverflow上有大量關於泛型數值計算的討論,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包://www.nuget.org/packages/Shone.Math/1.0.0

源代碼://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社區做點貢獻。

聲明:原創文章歡迎轉載,但請註明出處,//www.cnblogs.com/ShoneSharp。