表达式树中的参数替换






4.95/5 (27投票s)
本文介绍如何通过用另一个表达式替换表达式树中的参数,类似于用一个函数替换数学函数中的一个自变量。
引言
本文介绍了一种转换 System.Linq.Expression
对象的方法,该方法通过用不同的表达式替换其中的某些参数来实现。此技术可应用于例如使用 lambda 表达式描述数学函数的情况,它将允许通过自变量替换技术生成新函数。相应地,它在创建图表、2D 和 3D 图形时非常有用。
本文仅展示如何将此技术应用于生成新函数以及创建这些函数的 Silverlight 图。未来的文章将描述将此技术应用于 2D 图形和动画。
背景
数学中有一个非常常见的问题。假设我们有一个函数 y = f(x)
。还有一个函数 x = g(t)
;我们可以组合这两个函数得到依赖关系 y = (f*g)(t)
,其中 f*g
是一个组合函数。
这个问题可以推广到多自变量函数:假设我们有一个函数 y = F(x1, x2, ..., xn)
。假设还有一个函数使得其一个自变量 xj 依赖于一组其他自变量:xj = G(t1, ..., tm)
。函数 F
和 G
可以组合成函数 (F*G)
,该函数操作与函数 F
相同的自变量,只是自变量 xj 被替换为 t1, ..., tm
。y = (F*G)(x1, ..., xj-1, t1, ..., tm, xj+1, ... ,xn)
.
.NET 中,数学函数可以用 lambda 表达式或相应的表达式树来描述。我们要解决的问题最好通过一个例子来展示:假设我们有一个表达式,它对应于例如某个多项式。
Expression<Func<double, double>> polynomialExpression =
x => x*x*x*x + 3*x*x*x + 5*x*x + 10*x + 20;
假设我们想创建并使用代表上述表达式沿变量 x
的任意平移的其他表达式。直接地,我们可以创建一个依赖于 2 个自变量的另一个表达式:x
和 shift
。
Expression<Func<double, double>> polynomialExpressionWithShifts =
(x, shift) =>
(x-shift)*(x-shift)*(x-shift)*(x-shift) +
3*(x-shift)*(x-shift)*(x-shift) +
5*(x-shift)*(x-shift) +
10*(x-shift) +
20;
然而,这并不是一个方便的表达式。此外,如果需要更多的自由度,例如,除了任意平移外,我们还需要任意的缩放(使函数沿 x
轴变窄或变宽),表达式将变得越来越复杂。
使用本文描述的方法,要获得 polynomialExpressionWithShifts
,只需编写以下代码。
Expression<Func<double, double>> shiftExpression = (x, shift) => x - shift
Expression<Func<double, double>> polynomialExpressionWithShifts =
(Expression<Func<double, double>>)polynomialExpression.Substitute
("x", shiftExpression);
扩展方法 Substitute
将用 ParameterExpressions x
和 shift
替换 polynomialExpression
中的 ParameterExpression x
,并修改表达式的代码,将变量 'x
' 的任何出现替换为 'x - shift
'(shiftExpression
的主体)。
为了添加缩放功能,我们只需编写以下代码。
Expression<Func<double, double, double>> dilationAndShiftExpression =
(x, dilationFactor, shift) = x * dilationFactor - shift
Expression<Func<double, double, double>> polynomialExpressionWithShifts =
(Expression<Func<double, double, double>>)polynomialExpression.Substitute
("x", dilationAndShiftExpression);
该方法足够通用,可以处理原始表达式中的多个变量。这就是为什么在扩展函数 Substitute
中指定变量名称的原因——我们只替换作为其参数之一传递给表达式的变量的名称。
Using the Code
附带的代码包含以下四个项目。
ExpressionModifier
- 包含核心功能 - 它生成 .NET 4.0 DLL。ExpressionModifierTester
- 包含ExpressionModifier
功能的 Microsoft 测试。SilverlightExpressionModifier
- 与ExpressionModifier
相同,但它是一个 Silverlight 4.0 项目。SimplePlots
- Silverlight 4.0 项目,用于绘制不同的表达式 - 其目的是提供一些参数替换的可视化示例。
使用示例可以从 SimplePlot
和 ExpressionModifierTester
项目中获取。
应将 SimplePlot
设置为解决方案的启动项目。
SimplePlot
项目中的替换发生在 MainPage.PlotMainAndModified
函数中。
public void PlotMainAndModified
(
Expression<Func<double, double>> mainExpression,
Expression<Func<double, double>> substExpression,
double delta = 0.1,
double pointSize = POINT_SIZE
)
{
AddPlot(Colors.Red, delta, mainExpression.Compile(), pointSize);
LambdaExpression modifiedExpression =
mainExpression.Substitute("x", substExpression);
AddPlot
(
Colors.Blue,
delta,
modifiedExpression.Compile() as Func<double, double>,
pointSize
);
}
此函数以红色绘制原始函数,以蓝色绘制结果函数。
请确保在 MainPage.MainPage
构造函数的末尾取消注释正弦示例行,而注释掉抛物线示例行,如下所示。
// uncomment the two lines below for a sinusoidal example
PlotMainAndModified(x => Math.Sin(-x), x => x * 2 + 3, 0.01, 10d);
AddPlot(Colors.Yellow, 0.01, x =>Math.Sin(-(x * 2 + 3)), 3d);
/*
// uncomment the two lines below for a parabola example
PlotMainAndModified(x => x * x, x => x * x - 2, 0.01, 10d);
AddPlot(Colors.Yellow, 0.01, x => (x * x - 2) * (x * x - 2), 3d);
*/
重新生成并运行 SimplePlot
项目将得到以下图。

原始函数 sin(-x)
以红色显示。通过将 2x + 3
替换原始函数中的 x
而得到的结果函数以蓝色显示。最后,我们用较细的黄色绘制组合函数 sin(-(2x + 3))
,以显示它与替换结果(较粗的蓝色)相匹配。
现在,如果我们注释掉与正弦示例相关的两行,并取消注释与抛物线示例相关的行,如下所示。
/*
// uncomment the two lines below for a sinusoidal example
PlotMainAndModified(x => Math.Sin(-x), x => x * 2 + 3, 0.01, 10d);
AddPlot(Colors.Yellow, 0.01, x =>Math.Sin(-(x * 2 + 3)), 3d);
*/
// uncomment the two lines below for a parabola example
PlotMainAndModified(x => x * x, x => x * x - 2, 0.01, 10d);
AddPlot(Colors.Yellow, 0.01, x => (x * x - 2) * (x * x - 2), 3d);
然后重新生成并重新运行项目,我们将看到以下图。

与第一个示例一样,红色显示的原始抛物线对应于 x2 函数。粗蓝色线是使用 x2 - 2
替换第一个函数中的 x
的结果的图。细黄色线是 (x2 - 2)2
函数的图,仅用于显示它与替换结果匹配。
ExpressionModifierTester
项目也可以用来学习如何使用替换功能。下面显示了 TestSimpleFunction
的代码。
[TestMethod]
public void TestSimpleFunction()
{
Expression<Func<double, double>>
originalExpression = t => t * t * t + 20;
Expression<Func<double, double>> substituteExpression = t => t * t;
LambdaExpression modifiedExpression =
originalExpression.Substitute("t", substituteExpression);
Func<double, double> modifiedFunction =
(Func<double, double>) modifiedExpression.Compile();
Assert.AreEqual((int) modifiedFunction(1), 21);
Assert.AreEqual((int) modifiedFunction(2), 2 * 2 * 2 * 2 * 2 * 2 + 20);
}
可以看到,我们的原始表达式是 t3 + 20
。我们将 t2
替换 t
。结果表达式对应于 t6 + 20
。然后我们测试结果表达式确实产生了正确的值。
TestComplexPolynomial
具有类似的功能,只是其原始函数要复杂得多。
核心替换功能回顾
核心功能位于两个文件中:ExpressionVariableSubstituteHelper.cs 和 SubstExpressionVisitor.cs,它们包含同名的类。这些文件位于 ExpressionModifier
项目下。SilverlightExpressionModifier
项目下也有指向相同文件的链接。
Static
类 ExpressionVariableSubstituteHelper
包含 Substitute 扩展函数。它在主函数中查找要替换的参数对应的 ParameterExpression
(如果按名称找不到该参数,则抛出异常)。它还执行其他一些检查,例如,它检查在替换后可能扩展的参数列表中不会发生名称冲突。然后,它调用 SubstExpressionVisitor
类的 Visit
函数来实际创建结果表达式树。
SubstExpressionVisitor
类派生自 ExpressionVisitor
,并重写了三个函数:VisitUnary
、VisitMethodCall
和 VisitBinary
。也许我忽略了其他可能包含 ParameterExpression
对象的子项的表达式,然后,代码可能会在某些复杂的表达式上失败,但上述三个函数涵盖了大多数表达式。上述三个函数中的每一个都实现了检查其子项是否为我们要替换的 ParameterExpression
,如果是,则返回一个相同类型的新表达式,该表达式将该 ParameterExpression
子项替换为替换表达式的主体。
关注点
这种操纵表达式树的新方法在数学密集型项目中可能非常有用,例如在 2D 和 3D 图形及动画中。本文描述了将此方法应用于不同函数的绘图。我的下一篇文章将使用此方法进行沿任意路径的 2D 动画。
历史
- 2011年1月6日:初始发布