65.9K
CodeProject 正在变化。 阅读更多。
Home

表达式树中的参数替换

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.95/5 (27投票s)

2011年1月6日

CPOL

6分钟阅读

viewsIcon

56016

downloadIcon

427

本文介绍如何通过用另一个表达式替换表达式树中的参数,类似于用一个函数替换数学函数中的一个自变量。

引言

本文介绍了一种转换 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 个自变量的另一个表达式:xshift

  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 xshift 替换 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

附带的代码包含以下四个项目。

  1. ExpressionModifier - 包含核心功能 - 它生成 .NET 4.0 DLL。
  2. ExpressionModifierTester - 包含 ExpressionModifier 功能的 Microsoft 测试。
  3. SilverlightExpressionModifier - 与 ExpressionModifier 相同,但它是一个 Silverlight 4.0 项目。
  4. 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.csSubstExpressionVisitor.cs,它们包含同名的类。这些文件位于 ExpressionModifier 项目下。SilverlightExpressionModifier 项目下也有指向相同文件的链接。

Static ExpressionVariableSubstituteHelper 包含 Substitute 扩展函数。它在主函数中查找要替换的参数对应的 ParameterExpression (如果按名称找不到该参数,则抛出异常)。它还执行其他一些检查,例如,它检查在替换后可能扩展的参数列表中不会发生名称冲突。然后,它调用 SubstExpressionVisitor 类的 Visit 函数来实际创建结果表达式树。

SubstExpressionVisitor 类派生自 ExpressionVisitor,并重写了三个函数:VisitUnaryVisitMethodCallVisitBinary。也许我忽略了其他可能包含 ParameterExpression 对象的子项的表达式,然后,代码可能会在某些复杂的表达式上失败,但上述三个函数涵盖了大多数表达式。上述三个函数中的每一个都实现了检查其子项是否为我们要替换的 ParameterExpression ,如果是,则返回一个相同类型的新表达式,该表达式将该 ParameterExpression 子项替换为替换表达式的主体。

关注点

这种操纵表达式树的新方法在数学密集型项目中可能非常有用,例如在 2D 和 3D 图形及动画中。本文描述了将此方法应用于不同函数的绘图。我的下一篇文章将使用此方法进行沿任意路径的 2D 动画。

历史

  • 2011年1月6日:初始发布
© . All rights reserved.