C# 中利用運行時編譯實現泛函

jopen 9年前發布 | 10K 次閱讀 C#

引言

我想要分享一個新模式,我開發來用于在 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 不能通過編譯因為:

  1. 類型"T"不能確保int數值的隱式轉換。

    </li>

  2. 類型"T"不能確保一個加法操作。

    </li> </ol>

    現在我們了解了根本問題后,讓我們開始尋找一些方法克服它。

    接口化解決方法

    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 還不能編譯因為:

    1. 在C#中沒有基本“數值”類型。

      </li> </ol>

      如果我們實現了我們自己的基本“數值”類型,就可以讓它通過編譯。我們所必需做的就是迫使這個數值類型擁有C#基本數值類型一般的算數運算符和隱式轉換。邏輯上來講,這應該是一個接口。

      然而,即使我們自己做數值接口,我們仍然有一個重大問題。我們將不能夠對 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 就編譯了,但是它有點糟,為什么呢:

      1. 編程時用接口初始化變量是非常丑陋的。

        </li>

      2. 你不該用工廠方法或構造函數作為一個實例化方法,因為它是一種糟糕的設計并且很容易在程序各處造成空引用異常。

        </li>

      3. 你不能讓C#基本類型去實現“Number”接口所以只能使用自定義類型工作。

        </li>

      4. 它不是泛函因為你必須每一步都寫一個自定義的實現。

        </li>

      5. 這段代碼因封裝了基本類型工作極其慢。

        </li> </ol>

        如果你的泛函庫不能使在 C# 中完成泛型數學運算,沒有人會對此買單。因此,接下里讓我們處理這個問題。如果不能夠修改 C# 原始數據類型去實現想要的接口,那么我們就創造另一種類型能夠處理那些類型具有的所有數學運算。這就是在 .Net 框架中廣泛使用的標準提供者模式。

        邊注/發泄:就我個人來說,我憎恨提供者模式。但我至今沒有發現使用委托有處理不好的例子。當大量創建大量提供者時,他們沒有使用委托。

        當我們使用提供者模式,本質上仍是做和以前同樣的事,但一個提供者類就能處理所有的數學運算。在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 通過把所有的泛函性質移動到幫助類中,我們可以使用C#基本類型執行數學運算。然而,這僅僅修復 EXMAPLE 3 中的第一個問題。我們仍舊需要解決以下問題:

        1. 所有方法都必須訪問 mathProvider 類。雖然您可以編寫代碼,讓其不必在每個函數間傳遞,這個原則同樣適用于其它類似的結構。

          </li>

        2. 你的實例化仍然基于工廠方法。在上面的情況中它是一個來自于int的轉換。

          </li>

        3. 在原始代碼中你仍然需要為每一個簡單的類型中實現泛函性。

          </li>

        4. 這仍然相當慢,除非你為 provider 做一些”聰明的“緩存。provider 的傳遞和查找加起來真的很多。

          </li> </ol>

          現在我們已經嘗試過在數值類型本身(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...

          事實是,這看起來要比接口化方法好。代碼很簡單并且容易使用。然而,和以前一樣我們還是有很多問題:

          1. 我們仍要為每一種類型創建一個定制的實現。

            </li>

          2. 同時我們有大量的類型轉換可能造成異常而且很慢。

            </li>

          3. 以及對不支持的類型有運行時錯誤。

            </li>

          4. 性能低下。

            </li> </ol>

            注:我不知道他們是否仍然在用 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() + "\n";            throw new Exception(error);          }          MethodInfo generate = results.CompiledAssembly.GetType("Seven.Generated.Generator").GetMethod("Generate");          return (T)generate.Invoke(null, null);        }      }    

            endregion   

            } }</pre>

            代碼工作原理:

            如果通用數學代碼存儲為一個字符串,可以使用 string hacking(又名macros aka string replace),以改變運行時的代碼。我們可以寫一個函數,然后在使用該函數時改變該函數的類型。因此,我們可以認定泛型有必須的數學操作符來實現該函數。

            在第一次調用函泛時,它會構建自己和重新自動分配。這樣,我們就不必處理一個愚蠢的構造函數,它只須要根據我們所需構造函即可。

            據我所知,你不能編譯使用運行時編譯器編譯單個對象,我只是編譯了一個返回我需要的值類型的方法。可能存在替代方法,尤其是當你使用序列化技術的時候,但是我不是很熟悉學歷惡化格式,所以這種方法對我來說可能更容易。
            優點:

            1. 每一種類型只需要代碼的一個版本。

              </li>

            2. 有沒有構造方法或設置方法調用,方法會像我們所希望的那樣自我構造。

              </li>

            3. 快!這種方法據我所知唯一的開銷調用委托的開銷。

              </li> </ol>

              小缺點:(這些“缺點”可以克服)
              1.它可以是惱人的編寫通用的數學函數作為一個字符串。 解決辦法:我建議在單獨的文件編寫通用代碼并解析。這樣的字符串不是永久被保存在內存,你仍然可以編輯它,就像在 Visual Studio 中使用標準的C#一樣。

              2.這不是一個跨平臺的例子。補丁:它很容易實現跨平臺功能。根據他們的網站所述,這像一個包含反射和運行時編譯庫的 Mono 項目。因此只要動態查找運行時編譯器就能讓“生成的”功能類跨平臺。

              3.如果泛型的“typeof(T).ToString()”被嵌入到泛型中,現在的這些代碼將會崩潰。補丁:使用某種類型創建一個函數 再創建一個適當的字符串來表示這種類型達到和原始代碼一樣的目的。

              4.我們還是有編譯錯誤。告訴我們有自定義類型”struct Fraction128“忘記重載”+“運算符。同時也會拋出運行時錯誤。補丁:這個問題可以通過在編譯時寫一個 VS 插件去檢測泛函運算中使用的類型是否包含基本數值操作符而被修復。我只是把這些問題指出來告訴你們它是可修復的,我不會去做這些。到用的時候,不要干蠢事,:P

              結論

              通過使用運行時編譯,您可以將存在數學運算符和值類型轉換的假設強制認定為成立,使你能夠做一些真正意義生的數學計算。它是快速的,相對于本文中介紹的其他方法是易于維護的,而且非常強大。

              雖然這不是全部...我心中已經有了一些改進的想法。如果您有任何建議,我很愿意虛心聆聽。謝謝!

              版權

              本文及相關源代碼及文件屬于 The Code Project Open License (CPOL)

               本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
               轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
               本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!