代码改变世界

“表达式树”配合“泛型参数字典”定义通用操作

2009-11-13 13:53 by Jeffrey Zhao, ... 阅读, ... 评论, 收藏, 编辑

上午有朋友提出了这么一个问题:如何定义一个通用的相加操作。这样的话,我们就可以定义如下的扩展方法了:

public static class EnumerableExtensions
{
    public static T Sum<T>(this IEnumerable<T> source)
    {
        T sum = default(T);
        bool first = true;

        foreach (var item in source)
        {
            if (first)
            {
                sum = item;
                first = false;
            }
            else
            {
                sum += item;
            }
        }

        return sum;
    }
}

这个扩展方法的作用便是计算出source中所有的T类型元素之和——显然,上面标红的那行代码是无法编译通过的,因为并非所有的类型都有“相加”操作。因此,我们要设法实现这个功能。这又是“泛型参数字典”的用武之地了,当然更关键的其实是“表达式树”的“编译”功能:

public static class AddOperation<T>
{
    static AddOperation()
    {
        var x = Expression.Parameter(typeof(T), "x");
        var y = Expression.Parameter(typeof(T), "y");
        var add = Expression.Add(x, y);
        var lambda = Expression.Lambda<Func<T, T, T>>(add, x, y);
        s_add = lambda.Compile();
    }

    private static Func<T, T, T> s_add;

    public static T Add(T x, T y)
    {
        return s_add(x, y);
    }
}

无论是什么类型,“相加”操作的表达式都是“Add”,因此从表达式树的角度来说它们是完全相同的。因此,我们只要使用表达式树进行Compile得到的结果,自然可以应对各种情况。很显然,性能也是非常高的——这是“泛型参数字典”和“表达式树”共同努力的结果。那么来尝试一下:

public struct Complex
{
    public int m_real;
    public int m_imaginary;

    public Complex(int real, int imaginary)
    {
        this.m_real = real;
        this.m_imaginary = imaginary;
    }

    public static Complex operator +(Complex c1, Complex c2)
    {
        return new Complex(c1.m_real + c2.m_real, c1.m_imaginary + c2.m_imaginary);
    }
    
    public override string ToString()
    {
        return (String.Format("{0} + {1}i", m_real, m_imaginary));
    }
}

// 调用
AddOperation<int?>.Add(3, 5);
AddOperation<double>.Add(3.5, 6.8);
AddOperation<Complex>.Add(new Complex(1, 2), new Complex(2, 3));

可见,无论是Nullable Int32,Double还是使用自定义“+”操作符的Complex类型,AddOperation类都能正常工作。只可惜字符串不行——因为编译器其实是将字符串相加编译为String.Concat方法的调用,而String类型本没有“相加”操作。因此,我们AddOperation需要进行修改,补充一个特殊情况:

static AddOperation()
{
    if (typeof(T) == typeof(string))
    {
        Func<string, string, string> strAdd = (x, y) => x + y;
        s_add = (Func<T, T, T>)(object)strAdd;
    }
    else
    {
        var x = Expression.Parameter(typeof(T), "x");
        var y = Expression.Parameter(typeof(T), "y");
        var add = Expression.Add(x, y);
        var lambda = Expression.Lambda<Func<T, T, T>>(add, x, y);
        s_add = lambda.Compile();
    }
}

如果T是字符串,那么我们“强行”指定一个加法的实现,至于其他情况,那还是最最普通的Add操作了。那么,还有哪些类型是需要特殊处理的呢?嗯……我还想到了“委托”……还有吗?

于是乎,我们的通用Sum方法便可以这样实现了:

public static T Sum<T>(this IEnumerable<T> source)
{
    T sum = default(T);
    bool first = true;

    foreach (var item in source)
    {
        if (first)
        {
            sum = item;
            first = false;
        }
        else
        {
            sum = AddOperation<T>.Add(sum, item);
        }
    }

    return sum;
}

感觉如何?是不是比.NET框架中定义的Enumerable.Sum扩展方法要强大许多呢?

使用Live Messenger联系我