使用C#Lambda进行泛型参数的数值运算
做天写了一个矩阵运算类,我尝试了用泛型来实现,很明显泛型能在我给我的矩阵应用带来更多的选择.矩阵运算在游戏及其AI运用很广泛.不过后来我一想,还是没有必要运用泛型实现.不过昨天的经历让我有兴趣和大家讨论一下C#泛型参数的值运算问题:泛型参数带来的好处的同时也不能忽视其局限性.
泛型参数应用的一个很老的例子就是交换两个数的值了,比如下面的方法实现了两个任意值类型参数的值交换:
public void Swap(ref T a, ref T b)
{
T temp = a;
a = b;
b = temp;
}
当然此方法不能应用于引用类型参数,为了防止泛型编程过程中,不恰当的调入应用参数增加隐性bug,C#对泛型增加了参数约束.在C#中参数约束是通过where关键字实现的,关于更多的泛型参数约束请参考MSDN
上面的方法中参数a,b肯定只能是值类型,所以修正程序第一行如下:
public void Swap(ref T a, ref T b) where T:struct
现在我们讨论的不是泛型的概念,不妨考虑一下以下问题:
- 能否通过泛型实现一个方法来返回所有参数中最大的那个呢?
- 能否通过泛型实现一个方法来得到一个泛型参数的倒数呢?
- 能否计算方法中泛型参数的绝对值,或者取负呢?
- …
就拿第一个问题吧,很可能我们会写出下面的代码:
public T GetTheGreater(T a, T b) where T:struct
{
if(a>b)
return a;
else
return b;
}
世界上的事情总是有倒霉的一半,程序的第三行不会通过编译检查,已经很明了了,泛型参数是不能进行逻辑判断运算,至少不能进行大于的比较运算.实际上包括所有的逻辑运算和算术运算.由此可见上面的问题在C#里面几乎是不可解决了.
不过我们不要忘了倒霉的一半不是全部,C#3.0引入了表达式树,使得不同类型的值进行运算成为可能.表达式基类Expression提供了丰富的表达式运算方法来帮我们达到目的.我有点迫不及待的给出几行代码,下面是运用表达式树比较两个泛型参数的大小:
///
/// 判断a是否小于b
/// 注意参数顺序,判断的是前一个参数是否小于后一个参数
///
public static bool LessThan(T num1, T num2) where T:struct
{
//参数表达式树节点1
ParameterExpression para1 = Expression.Parameter(typeof(T), "left");
//参数表达式树节点2
ParameterExpression para2 = Expression.Parameter(typeof(T), "right");
//两个参数节点构成关系为比较大小的二叉树节点
BinaryExpression expsLessThan = Expression.LessThan(para1, para2);
//定义lambda表达式
Expression> exps = Expression.Lambda
>(expsLessThan, new ParameterExpression[] { para1, para2 });
//转化为匿名函数指针
Func myFunction = exps.Compile();
//返回执行结果
return myFunction(num1, num2);
}
关于表达式树的更多概念请参见System.Linq.Expressions 命名空间这里不再重复MSDN的内容
当然上面的是逻辑判断的例子,如果是算术运算呢?我们只要改变对Expression代理的封装就是.
///
/// 将两个泛型参数做加法运算
///
public static T Add(T num1, T num2) where T:struct
{
//参数表达式树节点1
ParameterExpression para1 = Expression.Parameter(typeof(T), "left");
//参数表达式树节点2
ParameterExpression para2 = Expression.Parameter(typeof(T), "right");
//两个参数节点构成关系为求和的二叉树节点
BinaryExpression expsAdd = Expression.LessThan(para1, para2);
//定义lambda表达式
Expression> exps = Expression.Lambda
>(expsLessThan, new ParameterExpression[] { para1, para2 });
//转化为匿名函数指针
Func myFunction = exps.Compile();
//返回执行结果
return myFunction(num1, num2);
}
上面的都是二元运算,如果是一元运算呢?有点点不同就是注意对lambda表达式的中的代理参数封装,还有就是二叉树节点为UnaryExpression(包含一元运算的表达式),而不是BinaryExpression(包含二元运算的表达式)
下面是一个求负的方法
///
/// 求给定泛型参数的负数
///
public static T Negate(T num) where T:struct
{
//创建一个参数表达式树节点
ParameterExpression para1 = Expression.Parameter(typeof(T), "left");
//构成一个求负的一元表达式
UnaryExpression expsNegate = Expression.Negate(p1);
//定义lambda表达式
Expression> exps = Expression.Lambda
>(expsLessThan, new ParameterExpression[] { para1});
//转化为匿名函数指针
Func myFunction = exps.Compile();
//返回执行结果
return myFunction(num);
}
有了上面三个例子,基本上所有的逻辑运算和算术运算都不是问题了,不过我们如果在定义的泛型类中一个个定义这些方法肯定很别扭,不仅破坏泛型类本身的聚合性,而且重复写也麻烦,所以我们可以将这些运算方法封装为另外一个泛型类,比如取个名字就叫 CalculateGeneric,实际上我就是这么叫的.
///
/// 泛型数值类型计算类
///
public static class CalculateGeneric where T:struct
{
/// 返回此数值泛型的0值
public static T Zero;
/// 实现两个泛型变量的相加算术运算
public static T Add(T num1, T num2);
/// 判断两个泛型变量值是否相等
public static bool Equal(T num1, T num2);
/// 取倒数
public static T Reciprocal(T num);
///更多丰富的计算类型....
}
你可能会注意到上面的类中有一个访问器是返回0值,另外你还可能注意到取倒数的方法.所以我还要补充一个问题就是怎样解决泛型参数常量1和0的表示,比如我要通过泛型方法得到不同类型数值的倒数,我就要得到数值1.Expression类也没有提供取倒数的表达式运算方法.
我还没有很好的办法来解决这个问题,不过我临时是这样来解决的:
- 通过泛型代码中的默认关键字default 来变相获取0值,这有泛型参数如果为值类型,则默认值为0
- 将一个不为0值的泛型参数和自身做除法运算则得到数值1
我不能保证上面的解决方法适合所有情况,欢迎大家提出错误或者分享你的解决方案.
另外说明,不能乱用这种方法来对泛型操作,毕竟这样修改了泛型的本意.(2010-4-4 15:57:01更新)