【问题标题】:Creating an expression that can be calculated at run time创建一个可以在运行时计算的表达式
【发布时间】:2013-12-29 19:45:49
【问题描述】:

我有一个产品列表,我需要根据这些产品创建表达式树,这些表达式树可以被持久化,以后可以检索和执行。这适用于客户端计算生成器。

我是 Expressions 的新手,虽然我已经阅读了相当多的文档,但这里的学习曲线有点陡峭。我想要的是能够积累 PropertyExpression 和 Operand 对开始。这是我到目前为止的内容,不确定我的结构是否正确。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;

namespace ConsoleApplication1
{
    public enum ProductType { Perishable, Fixed, Miscellaneous }    
    public enum OperandType { Addition, Sunbtraction, Multiplication, Division }

    public class Product
    {
        public string Name { get; set; }
        public ProductType Type { get; set; }
        public float Price { get; set; }
    }

    public class Configuration
    {
        public Dictionary<string, float> Dictionary { get; set; }
    }

    public class Operand
    {
        public OperandType Type { get; set; }
    }

    public class CalculationPair<TEntityType, TProperty>
        where TEntityType: class
        where TProperty: struct
    {
        public ValueTypeProperty<TEntityType, TProperty> Left { get; set; }
        public Operand Operand { get; set; }
        public ValueTypeProperty<TEntityType, TProperty> Right { get; set; }

        // How to specify TResult as an [out] parameter?
        public TResult Calculate<TResult> ()
            where TResult: struct
        {
            TResult result = default(TResult);

            if (this.Operand.Type == OperandType.Multiplication)
            {
                // How to execute the expression?
                //result = this.Left * this.Right;
            }

            return (result);
        }
    }

    public class ValueTypeProperty<TEntityType, TProperty>
        where TEntityType: class
        where TProperty: struct
    {
        public string Name { get; set; }

        public Expression<Func<TEntityType, TProperty>> PropertyExpression { get; set; }
    }

    public class ProductPriceProperty:
        ValueTypeProperty<Product, float>
    {
    }

    public static class Program
    {
        public static void Main ()
        {
            Configuration config = new Configuration();
            List<Product> products = new List<Product>();

            config.Dictionary.Add("ExportFactor", 80);
            config.Dictionary.Add("ChannelMargin", 100);

            products.Add(new Product() { Name = "1", Type = ProductType.Fixed, Price = 10 });
            products.Add(new Product() { Name = "2", Type = ProductType.Miscellaneous, Price = 20 });
            products.Add(new Product() { Name = "3", Type = ProductType.Perishable, Price = 30 });

            foreach (var product in products)
            {
                if (product.Type == ProductType.Fixed)
                {
                    CalculationPair<Product, float> calculation = new CalculationPair<Product, float>()
                    {
                        Left = new ProductPriceProperty() { Name = "Product Price", PropertyExpression = (entity => entity.Price) },
                        Operand = new Operand() { Type = OperandType.Multiplication },
                        Right = new ProductPriceProperty() { Name = "ExportFactor", PropertyExpression = (entity => config.Dictionary ["ExportFactor"]) },
                    };

                    // Calculation needs to be persisted to be retrieved later.
                    // ???!

                    // Once calculation has been reconstruction from the persistence layer, it needs to be executed.
                    product.Price = calculation.Calculate<float>();
                }
            }
        }
    }
}

更新:以下是我正在努力解决的优先级顺序:

  • CalculationPair.Calculate&lt;TReult&gt;()函数中的表达式如何执行?
  • 如何在CalculationPair.Calculate&lt;TReult&gt;()函数中将TResult指定为[out]参数?
  • 如何将计算表达式持久化并稍后检索?

【问题讨论】:

  • 为什么不使用内置的表达式树而不是从头开始重写所有内容?您只需要提供序列化逻辑,其他一切(包括稍后执行表达式)都是完全免费的。
  • @Jon:尽管我已尝试阅读它,但我不清楚我正在尝试的内容与您的建议之间的区别。这将用于表达式/计算构建器,因此具有更多控制权是可取的。如果我没记错的话,网络上的大多数表达式构建器示例都使用内置树并将它们用于过滤目的,而不是基于用户指定的公式进行值操作。
  • 您正试图从头开始编写内置表达式树的设计欠佳且(编译器)支持极差的子集。我建议使用内置插件(适当的子集)——只要你需要,你的代码不会正确处理所有理论案例,它也不需要。的确,大多数示例都面向过滤,但在任何地方都没有硬性限制。您可以使用 expr 树来表达任何不需要变异状态的东西,以及许多需要改变状态的东西。
  • @Jon:谢谢你的解释,我同意。请您指出其中一个示例作为起点。考虑到我基于这个问题的经验。我想我一开始很难掌握如何开始。

标签: c# .net linq reflection expression


【解决方案1】:

正如乔恩所说,您可以使用通常的表达式树,或者您可以像下面的代码一样制作闭包匿名方法:

public class CalculationPair<TEntityType, TProperty>
    where TEntityType : class
    where TProperty : struct
{
    // not sure that first three properties are needed here
    public ValueTypeProperty<TEntityType, TProperty> Left { get; set; }
    public Operand Operand { get; set; }
    public ValueTypeProperty<TEntityType, TProperty> Right { get; set; }

    // closure method
    public Func<TEntityType, TProperty> Calculator { get; set; }
}

这是使用它的 Main 方法的一部分:

foreach (var product in products)
{
    if (product.Type == ProductType.Fixed)
    {
        CalculationPair<Product, float> calculation = new CalculationPair<Product, float>()
        {
            Left = new ProductPriceProperty() { Name = "Product Price", PropertyExpression = (entity => entity.Price) },
            Operand = new Operand() { Type = OperandType.Multiplication },
            Right = new ProductPriceProperty() { Name = "ExportFactor", PropertyExpression = (entity => config.Dictionary["ExportFactor"]) },

            // only this property is needed, and it will handle reference to config object in the closure
            Calculator = (entity) => entity.Price * config.Dictionary["ExportFactor"]
        };

        // Once calculation has been reconstruction from the persistence layer, it needs to be executed.
        product.Price = calculation.Calculator(product);
    }
}

在该示例中,没有表达式树,只有常用的闭包方法。

更新1

Left 和 Right 节点表达式的问题在于,每个 this 表达式都链接到自己的 entity 参数,而不是我们创建的 ParameterExpression 并将指向真实的实体对象,所以我们需要用 ExpressionVisitor 将旧的重写为新的。它用于解析和重写需求。

这是该重写器的代码:

public class ParameterRewriter : ExpressionVisitor
{
    private readonly ParameterExpression _expToRewrite;

    public ParameterRewriter(ParameterExpression expToRewrite)
    {
        this._expToRewrite = expToRewrite;
    }

    protected override Expression VisitParameter(ParameterExpression node)
    {
        // we just use type checking to understand that it's our parameter, and we replace it with new one
        if (node.Type == this._expToRewrite.Type) return this._expToRewrite;
        return base.VisitParameter(node);
    }
}

这里是 CalculationPair 类:

public class CalculationPair<TEntityType, TProperty>
    where TEntityType : class
    where TProperty : struct
{
    public ValueTypeProperty<TEntityType, TProperty> Left { get; set; }
    public Operand Operand { get; set; }
    public ValueTypeProperty<TEntityType, TProperty> Right { get; set; }

    public TResult Calculate<TResult>(TEntityType entity)
        where TResult : struct
    {
        TResult result = default(TResult);

        var prop = Expression.Parameter(typeof(TEntityType), "param");
        var visitor = new ParameterRewriter(prop);
        var leftExp = visitor.Visit(Left.PropertyExpression.Body);
        var rightExp = visitor.Visit(Right.PropertyExpression.Body);

        Expression body;

        switch (this.Operand.Type)
        {
            case OperandType.Multiplication:
                body = Expression.Multiply(leftExp, rightExp);
                break;
            case OperandType.Addition:
                body = Expression.Add(leftExp, rightExp);
                break;
            case OperandType.Division:
                body = Expression.Divide(leftExp, rightExp);
                break;
            case OperandType.Sunbtraction:
                body = Expression.Subtract(leftExp, rightExp);
                break;
            default:
                throw new Exception("Unknown operand type");
        }

        var lambda = Expression.Lambda<Func<TEntityType, TResult>>(body, prop);

        // compilation is long operation, so you might need to store this Func as property and don't compile it each time
        var func = lambda.Compile();
        result = func(entity);

        return (result);
    }
}

和用法一样

product.Price = calculation.Calculate<float>(product);

【讨论】:

  • 谢谢。有没有办法让CalculationPair 类在内部自动填充Calculator,而不是从Main 中的外部初始化程序设置它?
  • 换句话说,我想了解如何在CalculationPair 类中使用ValueTypeProperty&lt;TEntityType, TProperty&gt; Left 和 Right 成员。您的解决方案当然有效,但我的要求可能会给我一个更好地理解 Expression> 类型的起点。
  • 此代码是混合代码,因为它使用您的 ValueTypeProperties 和 CalculationPair,并且还使用表达式进行计算逻辑。如果您需要左右节点,则此代码不能仅使用表达式。基本上,表达式只是一些操作\对象的树,可以在运行时解析,也可以编译为通常的委托。而闭包是当一个匿名方法使用来自参数、内部范围的变量时,它会锁定它们。
  • 您的评论(This code is hybrid...) 真的很到位。理解从编译时委托开始,转到仍然需要编译时声明的Func&lt;&gt;,然后转到可以在编译时表示或在运行时使用expression trees API 创建的Expression&lt;Func&lt;&gt;&gt;。为了开发一个适度的计算构建器,我必须分别使用最后一个组合编译和运行时声明/创建。再次感谢您提供超出预期的帮助。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-12-19
  • 2014-03-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-05-21
相关资源
最近更新 更多