我想要分享一个新形式,我开发来用于在 C# 中应用运转时编译实行泛型盘算。
过来的几年里我曾经在编程而且看到很多在 C# 中完成泛型数学的例子,可是没有一个能做得十分地好。在第一部分中我将一步步地讲解我看到的一些例子,同时也会阐明为何他们没有向泛函供给好的形式。
我开发了这个形式以作为我的 Seven Framework 框架工程的一部分。假如你感兴味的话你可以点击:https://github.com/53V3N1X/SevenFramework。
起首,基本的问题是在 C# 中处置泛型就像基类一样。除 System.Object 基类,他们都没有隐式成员。也就是说,相对规范数值类型(int,double,decimal,等)他们没有算数运算符和隐式转换。EXAMPLE 1 是一种幻想状况,可是这段代码在规范 C# 中是不克不及编译的。
// EXAMPLE 1 ---------------------------------------- namespace ConsoleApplication { public class Program { public static void Main(string[] args) { Sum<int>(new int[] { 1, 2, 3, 4, 5 }); } public static T Sum<T>(T[] array) { T sum = 0; // (1) cannot convert int to generic for (int i = 0; i < array.Length; i++) sum += array[i]; // (2) cannot assume addition operator on generic return sum; } } }
的确如斯,EXAMPLE 1 不克不及经过编译由于:
类型"T"不克不及确保int数值的隐式转换。
类型"T"不克不及确保一个加法操作。
如今我们了解了基本问题后,让我们Start寻觅一些办法克制它。
其它翻译版本 (1) 加载中接口化处理办法
C#中的 where 子句是一种逼迫泛型知足某品种型的束缚。但是,用这类办法就请求有一种不存在于C#中的根本数值类型。C#有如许一种根本数值类型是最靠近强迫泛型成为数值类型的可能了,但这其实不能在数学上协助我们。EXAMPLE 2 依然不克不及够经过编译,但假如我们发明出了我们本人的根本数值类型,这将成为可能。
Hide Copy Code
// EXAMPLE 2 ---------------------------------------- namespace ConsoleApplication { public class Program { public static void Main(string[] args) { Sum<int>(new int[] { 1, 2, 3, 4, 5 }); } public static T Sum<T>(T[] array) where T : number // (1) there is no base "number" type in C# { T sum = 0; for (int i = 0; i < array.Length; i++) sum += array[i]; return sum; } } }
如今 EXAMPLE 2 还不克不及编译由于:
在C#中没有根本“数值”类型。
假如我们完成了我们本人的根本“数值”类型,就能够让它经过编译。我们所必须做的就是迫使这个数值类型具有C#根本数值类型普通的算数运算符和隐式转换。逻辑上来说,这应当是一个接口。
其它翻译版本 (1) 加载中但是,即便我们本人做数值接口,我们依然有一个严重问题。我们将不克不及够对 C# 中的根本类型做通用数学盘算,由于我们不克不及改动 int,double,decimal 等的源代码来完成我们的接口。所以,我们不只必需编写本人的根本接口,还需求为C#中的原始类型编写包装器。
在例3中,我们有我们本人的数值接口,“数字”,和原始类型int的包装器,Integer32。
// EXAMPLE 3 ---------------------------------------- namespace ConsoleApplication { public class Program { public static void Main(string[] args) { Sum(new Number[] { new Integer32(1), // (1) initialization nightmares... new Integer32(2), new Integer32(3), new Integer32(4), new Integer32(5) }); } public static Number Sum(Number[] array) { Number sum = array[0].GetZero(); // (2) instance-based factory methods are terrible design for (int i = 0; i < array.Length; i++) sum = sum.Add(array[i]); return sum; } } public interface Number { Number GetZero(); // (2) again... instance based factory methods are awful Number Add(Number other); } public struct Integer32 : Number // (3) C# primitives cannot implement "Number" { int _value; public Integer32(int value) { this._value = value; } Number Number.GetZero() { return new Integer32(0); } // (4) you will have to re-write these functions for every single type Number Number.Add(Number other) { return new Integer32(_value + ((Integer32)other)._value); } } } // (5) this code is incredibly slow
好的,如许 EXAMPLE 3 就编译了,可是它有点糟,为何呢:
编程时用接口初始化变量长短常丑陋的。
你不应用工场办法或结构函数作为一个实例化办法,由于它是一种蹩脚的设计而且很轻易在顺序遍地形成空援用异常。
你不克不及让C#根本类型去完成“Number”接口所以只能运用自定义类型任务。
它不是泛函由于你必需每步都写一个自定义的完成。
这段代码因封装了根本类型任务极端慢。
假如你的泛函库不克不及使在 C# 中完成泛型数学运算,没有人会对此买单。因而,接下里让我们处置这个问题。假如不克不及够修正 C# 原始数据类型去完成想要的接口,那末我们就发明另外一品种型可以处置那些类型具有的一切数学运算。这就是在 .Net 框架中普遍运用的规范供给者形式。
其它翻译版本 (1) 加载中
边注/宣泄:就我个人来讲,我憎恶供给者形式。但我至今没有有发明运用拜托处置欠好的例子。当创立大量供给者时,他们没有运用拜托。
当我们运用供给者形式,实质上还是做和之前异样的事,但一个供给者类就可以处置一切的数学运算。在EXAMPLE 4中查验它:
/EXAMPLE 4 ---------------------------------------- namespace ConsoleApplication { public class Program { public static void Main(string[] args) { Sum<int>(new int[] { 1, 2, 3, 4, 5}, new MathProvider_int()); } // (1) all the methods need access to the provider public static T Sum<T>(T[] array, MathProvider<T> mathProvider) { T sum = mathProvider.GetZero(); for (int i = 0; i < array.Length; i++) sum = mathProvider.Add(sum, array[i]); return sum; } public interface MathProvider<T> { T GetZero(); // (2) you still need instance factory methods T Add(T left, T right); } public class MathProvider_int : MathProvider<int> { public MathProvider_int() { } int MathProvider<int>.GetZero() { return 0; } // (3) you still have to implement each function for every single type int MathProvider<int>.Add(int left, int right) { return left + right; } } } // (4) can be slow depending on implementation (this version is slow)
EXAMPLE 4 经过把一切的泛函性质Mobile到协助类中,我们可使用C#根本类型履行数学运算。但是,这仅仅修复 EXMAPLE 3 中的第一个问题。我们依旧需求处理以下问题:
一切办法都必需拜访 mathProvider 类。固然您可以编写代码,让其不用在每一个函数间通报,这个准绳异样实用于其它相似的构造。
你的实例化依然基于工场办法。在上面的状况中它是一个来自于int的转换。
在原始代码中你依然需求为每个容易的类型中完成泛函性。
这依然相当慢,除非你为 provider 做一些”聪慧的“缓存。provider 的通报和查找加起来真的非常多。
如今我们曾经测验考试过在数值类型自身(EXAMPLE 3)和内部 provider(EXAMPLE 4)上运用接口。运用接口我们曾经不克不及做更多了。可以断定的是我们可以应用一些聪慧奇妙的存储办法,但终极仍会见临类似的问题:必需在每步都支撑定制的完成。
最初说一句...在 C# 中接口不合适用在高效的泛函盘算中。
在 C# 中一切事物都可以转换成 System.Object 类型。因而,我只需把每个事物转换成一个工具然后用把持流处置它就能够了。让我们试一试。
Hide Shrink Copy Code
// EXAMPLE 5 ---------------------------------------- namespace ConsoleApplication { public class Program { public static void Main(string[] args) { MathProvider<int>.Sum(new int[] { 1, 2, 3, 4, 5}); } } public static class MathProvider<T> { public static T Sum(T[] array) { // (1) still requires a custom implementation for every type if (typeof(T) == typeof(int)) { T sum = (T)(object)0; // (2) largescale casting is very glitch prone for (int i = 0; i < array.Length; i++) sum = (T)(object)((int)(object)sum + ((int)(object)array[i])); return sum; } t hrow new System.Exception("UNSUPPORTED TYPE"); // (3) runtime errors } } } // (4) horribly slow...
现实是,这看起来要比接口化办法好。代码很容易而且轻易运用。但是,和之前一样我们仍是有非常多问题:
我们仍要为每品种型创立一个定制的完成。
同时我们有大量的类型转换可能形成异常并且很慢。
和对不支撑的类型有运转时错误。
功能低下。
注:我不晓得他们能否依然在用 F# 来做这个,可是当我阅读 F# 中普通的规范数学函数时,他们所做的看起来像是最低程度的工具转换。好笑至极!
工具转换是另外一个死胡同,但它最少十分容易易用。
代办署理
代办署理......真的很棒!
我们不克不及像运用原始类型那样高效地完成数学运算。假如没有每种承继关系编译器将不克不及作出判别,而且我们也不克不及让C#的原始类型承继我们本人的类。
所以,我们把普通类外的普通代码的功用都移除吧。我们需求怎样做呢?代办署理!只需在普通的类里设置一个代办署理,并在运转时从内部分派,这些类型就可以够被编译器所辨认。
但是,我们可以把拜托(代办署理)放到泛型类中,在内部编译拜托(代办署理)然后在运转时分派。
Hide Shrink Copy Code
// EXAMPLE 5 ---------------------------------------- namespace ConsoleApplication { public class Program { public static void Main(string[] args) { // (1) requires a constructor method to be called prior to use MathProviderConstructors.Construct(); MathProvider<int>.Sum(new int[] { 1, 2, 3, 4, 5}); } } public static class MathProviderConstructors { public static void Construct() { // (2) still requires a custom implementation for every type MathProvider<int>.Sum = (int[] array) => { int sum = 0; for (int i = 0; i < array.Length; i++) sum = sum + array[i]; return sum; }; } } public static class MathProvider<T> { // (3) still have runtime errors for non-implemented types (null-ref) public static System.Func<T[], T> Sum; } }
EXMAPLE 5 是今朝最好的泛函例子。它运转很快,实用于任何类型,而且除要确保静态结构办法必需被挪用外很轻易运用。可是,它仍有一些瑕疵...(1)结构办法必需被挪用,(2)对每个类型照旧需要自定义一个完成,(3)还会抛出运转时错误。
在这一点上我们必需做出一些让步。起首,运转时异常时简直不成防止的。我能想到的独一办法是制造一个自定义插件参加到 Visual Studio 中那将会抛出额定的编译错误。那为了这篇文章的目标,就必需处置这个运转时异常。但是最大的问题是我们仍是要为我们需求支撑的每个给类型写一个函数。必定会有处理这个问题的方法!
这是我今朝的一个通用的数学形式的版本:
using Microsoft.CSharp; using System; using System.CodeDom.Compiler; using System.Reflection; namespace RuntimeCodeCompiling { public static class Program { public static Action action; public static void Main(string[] args) { Console.WriteLine("Sum(double): " + Generic_Math<double>.Sum(new double[] { 1, 2, 3, 4, 5 })); Console.WriteLine("Sum(int): " + Generic_Math<int>.Sum(new int[] { 1, 2, 3, 4, 5 })); Console.WriteLine("Sum(decimal): " + Generic_Math<decimal>.Sum(new decimal[] { 1, 2, 3, 4, 5 })); Console.ReadLine(); } #region Generic Math Library Example public static class Generic_Math<T> { public static Func<T[], T> Sum = (T[] array) => { // This implementation will make this string be stored in memory during runtime, //so it might be better to read it from a file string code = "(System.Func<NUMBER[], NUMBER>)((NUMBER[] array) => { NUMBER sum = 0; for (int i = 0; i < array.Length; i++) sum += array[i]; return sum; })"; // This requires that "T" has an implicit converter from int values and a "+" operator code = code.Replace("NUMBER", typeof(T).ToString()); // This small of an example requires no namspaces or references Generic_Math<T>.Sum = Generate.Object<Func<T[], T>>(new string[] { }, new string[] { }, code); return Generic_Math<T>.Sum(array); }; } /// <summary>Generates objects at runtime.</summary> internal static class Generate { /// <summary>Generates a generic object at runtime.</summary> /// <typeparam name="T">The type of the generic object to create.</typeparam> /// <param name="references">The required assembly references.</param> /// <param name="name_spaces">The required namespaces.</param> /// <param name="code">The object to generate.</param> /// <returns>The generated object.</returns> internal static T Object<T>(string[] references, string[] name_spaces, string code) { string full_code = string.Empty; if (name_spaces != null) for (int i = 0; i < name_spaces.Length; i++) full_code += "using " + name_spaces[i] + ";"; full_code += "namespace Seven.Generated {"; full_code += "public class Generator {"; full_code += "public static object Generate() { return " + code + "; } } }"; CompilerParameters parameters = new CompilerParameters(); foreach (string reference in references) parameters.ReferencedAssemblies.Add(reference); parameters.GenerateInMemory = true; CompilerResults results = new CSharpCodeProvider().CompileAssemblyFromSource(parameters, full_code); if (results.Errors.HasErrors) { string error = string.Empty; foreach (CompilerError compiler_error in results.Errors) error += compiler_error.ErrorText.ToString() + " "; throw new Exception(error); } MethodInfo generate = results.CompiledAssembly.GetType("Seven.Generated.Generator").GetMethod("Generate"); return (T)generate.Invoke(null, null); } } #endregion } }
代码任务道理:
假如通用数学代码存储为一个字符串,可使用 string hacking(别名macros aka string replace),以改动运转时的代码。我们可以写一个函数,然后在运用该函数时改动该函数的类型。因而,我们可以认定泛型有必需的数学操作符来完成该函数。
在第一次挪用函泛时,它会构建本人和从头主动分派。如许,我们就不用处置一个愚笨的结构函数,它只需要依据我们所需结构泛函便可。
其它翻译版本 (1) 加载中 本文中的一切译文仅用于进修和交换目标,转载请务必注明文章译者、出处、和本文链接。 2KB翻译任务按照 CC 协议,假如我们的任务有进犯到您的权益,请实时联络我们。2KB项目(www.2kb.com,源码交易平台),提供担保交易、源码交易、虚拟商品、在家创业、在线创业、任务交易、网站设计、软件设计、网络兼职、站长交易、域名交易、链接买卖、网站交易、广告买卖、站长培训、建站美工等服务