代码改变世界

Expression Tree实践之通用Parse方法------"让CLR帮我写代码"

2012-05-07 23:54  stevey  阅读(2311)  评论(9编辑  收藏  举报

  近来特别关注了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的动态生成委托,你也可以尝试先写一写把。

 

 

如果您有任何建议和想法,请留言,我们一起讨论,共同进步!谢谢!