zoukankan      html  css  js  c++  java
  • 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的动态生成委托,你也可以尝试先写一写把。

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

  • 相关阅读:
    多线程的创建方式
    ArrayList 初探
    java创建对象的几种方式
    select2动态查询及多选
    RabbitMQ 消息发送、消息监听
    tr命令
    集群,分布式,微服务概念和区别理解
    xargs命令
    shell中的EOF用法
    字段分隔符IFS
  • 原文地址:https://www.cnblogs.com/skysoft001/p/2489385.html
Copyright © 2011-2022 走看看