Expression Tree實踐之通用Parse方法------"讓CLR幫我寫代碼"


  近來特別關注了Expression Tree 這個c#3.0以來的新特性之一。也嘗試着尋找和使用它的最佳實踐,通過閱讀學習博客園內幾位大牛寫的相關文章,也算是入門了。它可以說功能強大,或許會讓你意外的驚嘆,比如:為什么之前有linq to everywhere的趨勢,為什么可以linq to entity等。它使得我們可以動態的創建代碼(匿名函數),而不是在編譯時就硬編碼這些代碼。

 

  下面就通過一個簡單的需求,來一步一步分析重構,最終得到一個較好的實現吧。

  題目:比如有這樣一個字符串(“1,2,3,4,5”或者"1.1#2.2#3.3#4.0#5.1"),需要將它按照分隔符轉換成一個數組存儲。該如何做呢?

第一個版本:

public static int[] ToIntArray(this string input, string splitString)
        {
            if (string.IsNullOrEmpty(splitString))
            {
                throw new ArgumentNullException("splitString");
            }

            int[] result = new int[0];
            if (string.IsNullOrEmpty(input))
            {
                return result;
            }
            string[] source = Regex.Split(input, splitString, RegexOptions.IgnoreCase);

            if (source != null && source.Length > 0)
            {
                result = Array.ConvertAll<string, int>(source, p => int.Parse(p));
            }
            return result;
        }

上面代碼可以實現上面題目的需求,如:

string source = "1,2,3,4,5";
int[] result = source.ToIntArray(",");

  那么如果需要將這樣的字符串“1.1#2.2#3.3#4.0#5.1”轉換成double[]數組呢?哦,你通過觀察可以知道,把上面代碼copy一份,修改方法名為ToDoubleArray.並且將所有是int的地方換成double,那么相應的result = Array.ConvertAll<string, int>(source, p => int.Parse(p));換成result = Array.ConvertAll<string,double>(source, p => double.Parse(p));ok,這樣也完成了。似乎沒什么問題,可有同學就會發現,如果隨着轉換為目標數組的類型變化時候,我們就要多寫一個方法(比如:轉換為數組float[],decimal[],bool[]等),而且int,double,float,....這樣的類型或許會寫不完,比如自定義struct類型。那么可不可以寫一個通用的方法呢?有同學或許想到了泛型方法,將目標類型作為參數傳遞進去,那么我們就來嘗試寫一寫,如下:

public static T[] ToArray<T>(this string input, string splitString); 將方法體中所以用到具體類型的地方都換成T,當改寫到這句時候,result = Array.ConvertAll<string, T>(source, p => T.Parse(p));似乎出現問題,T.Parse(p)這里,T是泛型類型參數,它里面是否有靜態方法Parse呢?這里無法得知,失敗。該怎么辦呢?想一想,有同學就想到了用反射。可以動態調用Parse方法,恩,如下:

MethodInfo mi = typeof(T).GetMethod("Parse", new Type[] { typeof(string) });
result = Array.ConvertAll<string, T>(source, p => (T)mi.Invoke(null, new object[] { p }));

  可是隨着字符串數組長度的增加,每個字符串元素都Invoke,然后再unboxing,才得到結果,性能會低下。有沒有優化的辦法呢?大家想想辦法吧^_^

通過觀察,我們發現執行轉換的地方即int.Parse(p),或者double.Parse(p)這里是變化點,我們可以把這塊邏輯由外部傳入,用內置泛型委托Func<string,T>作為參數,

改泛型方法改變為:public static T[] ToArray<T>(this string input, string splitString,Func<string,T> parse);其中內部轉換部分為:result = Array.ConvertAll<string, T>(source, p => parse(p));

調用代碼:

int[] result = "1,2,3,4,5".ToArray<int>(",", p => int.Parse(p));

  我們將每個元素的轉換邏輯,通過委托由外部傳遞。這里使用Lambda Expression作為匿名函數傳遞給Func委托參數。可以順利通過,這樣以來多了一步,需要調用者傳遞轉換邏輯,而該方法既然是將一個字符串轉換一個數組,轉換方法可以使用Parse方法,或許沒必要再傳遞一次int.Parse,再說所有的內置值類型都有Parse吧,我們是否可以都使用Parse方法呢?為了提供封裝更好的API,我們希望像上面那個反射版本一樣,不需要傳遞額外處理邏輯。但又需要較好的性能,該怎么做呢?能否動態構造Func<string,int>或者Func<string,double>,.....總之,當我們調用ToArray<int>()時,我們就能得到Func<string,int>這樣的匿名函數p=>int.Parse(p)呢?就是說變化點是p=>int.Parse(p)這塊,能否動態創建這樣的代碼呢?yes,當Expression Tree 是一個Lambda Expression時,我們可以調用它的Compile方法,得到一個委托對象。

下面就來得到一個Func<string,T>類型的委托對象。

 1 /// <summary>
 2         /// 為值類型提供Parse方法的動態委托生成
 3         /// </summary>
 4         /// <typeparam name="T"></typeparam>
 5         class ParseBuilder<T> where T:struct
 6         {
 7             private static readonly Func<string, T> s_Parse;
 8             static ParseBuilder()
 9             {
10                 ParameterExpression pExp = Expression.Parameter(typeof(string), "p");
11                 MethodInfo miParse=typeof(T).GetMethod("Parse",new Type[]{typeof(string)});
12                 MethodCallExpression mcExp = Expression.Call(null, miParse, pExp);
13                 Expression<Func<string,T>> exp=Expression.Lambda<Func<string, T>>((Expression)mcExp, pExp);
14 
15                 s_Parse = exp.Compile();
16             }
17             public static T Parse(string input)
18             {
19                 return s_Parse(input);
20             }
21         }

注:上面的Parse之前實現為靜態屬性,感覺不妥,原因:1、當vs智能感知時候,顯示為屬性,一般不會認為是一個委托調用。2、一般一個操作應該表現為方法。所以此處改為靜態方法。

  在這里就不一一展開了,可以將上面代碼作為一個內部類,它實現在運行時根據具體目標類型動態創建委托(Func<string,int>,Func<string,double>...),得到這樣的委托后,直接調用就可以執行轉換了。woo,It's cool!讓CLR幫我們寫這個委托,太酷啦!

  上面會為每種T緩存一份委托,因為是static readonly Func<string, T>,初始化放在靜態構造函數里面,所以每種類型只會運行一次,所以性能有保證。這個不是我想出來的,是看到老趙在InfoQ上寫的一篇文章(表達式即編譯器),從中學習到了很多,非常感謝!還有其他前輩寫的Expression Tree的相關文章,下次整理后,放出來,也非常感謝你們!

下面給出完整實現,請參考。

View Code
 1 public static T[] ToArray<T>(this string input, string splitString) where T : struct
 2         {
 3             T[] result = new T[0];
 4 
 5             if (string.IsNullOrEmpty(splitString))
 6             {
 7                 throw new ArgumentNullException("splitString");
 8             }
 9 
10             if (string.IsNullOrEmpty(input))
11             {
12                 return result;
13             }
14             string[] source = Regex.Split(input, splitString, RegexOptions.IgnoreCase);
15 
16             if (source != null && source.Length > 0)
17             {
18                 result = Array.ConvertAll<string, T>(source, s => ParseBuilder<T>.Parse(s));
19                 //result = source.Select(p=>ParseBuilder<T>.Parse(p)).ToArray();
20             }
21 
22             return result;
23         }

 

下篇,我准備實現一個通用的model相等性比較器(兩個實體的所有可讀屬性相等即為相等)。如:

/// <summary>
/// 通用model的邏輯上相等性比較器
/// </summary>
/// <typeparam name="T"></typeparam>
public class GenericEqualityComparer<T>:IEqualityComparer<T>

那么像IEnumerable.Distinct()就可以過濾邏輯上相同的model了,其中也用到Expression Tree的動態生成委托,你也可以嘗試先寫一寫把。

 

 

如果您有任何建議和想法,請留言,我們一起討論,共同進步!謝謝!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM