• LINQ Provider进阶——“本地变量”处理


    问题

    请访问这里以获得一些说明以及完整的相关文章列表。

    所谓本地变量,其实就是一个变量。说他本地是为了和请求的服务器(例如SQL,DC)区分开来,说他变量又是为了和常量区分开来的。那么,在查询中引入变量会出现什么问题呢?在去年的文章中(听起来很久远的样子)我们构建了一个可以运行的Hello World,可以完成一些特定的任务(或者说提供了有限的一些功能),但是离“成型”还差得远,以至于一些功能根本无法使用。比如这么一个例子:

            static void translateDemo()
            {
                Context context = new Context("LDAP://searchAD", @"search\Ouwsearch", "OuweiSoft0123");
    
                //带本地变量的查询表达式
                string name = "sample";
    
                var lambda1 = context.Users.Where(u => u.Name.Contains(name));
                var lambda2 = context.Users.Where(u => u.Name.Contains("sample"));
                var lambda3 = context.Groups;
                ...
            }    

    在上面的例子中,lambda1和lambda2的语义是相同的,但是"sample"是常量,而name是变量,于是乎在分析的时候会出现一个问题。(在MSDN博客中,情况是返回了错误的表达式,但是由于我之前简化了处理方式,所以直接获得一个异常,见下图。)

    首先,第一怪异的是,变量的expressiontype竟然是memberaccess,其次可以看到中间那条红线框起来的值,是对一个莫名其妙的类型的对象访问成员。所以我们有以下结论(不是我得出的...):变量引用在表达式目录树中作为一个成员访问表达式存在,编译器为其生成了一个“匿名”类型。

    分析

    既然知道了问题的所在,我们就应该开始考虑如何解决这个问题。

    首先是,我们通过什么方式来处理?

    我们大可以在Translator类型中编写一定的逻辑,当碰到类似的表达式的时候,就对其进行一些处理,以使用它的值。但是这样可能使得原本就比较复杂的逻辑更加复杂。所以另一种方式是在Translator使用这个表达式之前对其进行“预加工”,使得其成为另外一个(注意表达式不可变)更“友好”的表达式。

    然后是,如何处理变量?

    这个不是很好解释,直接上代码吧。

    View Code
    else if (node.Method.Name == "Contains"
                    && node.Method.DeclaringType == typeof(string))
                {
                    var invoker = node.Object;
                    Visit(invoker);
                    //已经确定参数是字符串
                    string value = string.Empty;
                    if (node.Arguments[0].NodeType == ExpressionType.Constant)
                        value = string.Format("=*{0}*",
                            (node.Arguments[0] as ConstantExpression).Value);
                    else
                        value = string.Format("=*{0}*",
                            (Expression.Lambda(node.Arguments[0])).Compile().DynamicInvoke());
    
                    sb.Append(value);
                }

    这里修改了Contians方法的处理,添加了一个分支...但是需要注意,这里仅仅是为了突出如何获取变量的值,所以无法很好的处理其他的情况。一个比较完善的解决方式将在后面提供。

    如何预处理表达式

    我们总体的想法是将整棵表达式目录树中的“本地变量”引用作为子树处理(求值),从而得到一棵比较容易处理的树。那么我们1.需要分析这棵树的哪些部分是可以“直接求值”的;2.需要对已经确定的这些子树进行求值。其中,第一个过程是自底向上的,因为有一个规律很明显:如果子节点无法直接求值那么其父级也是无法直接求值的。第二个过程是自顶向下的,因为能够直接求值的子树越“深”那么,整棵树的简化程度越高,处理起来也就越方便。

    实现

    呃...这是别人的代码,已经很完备了,所以不做修改,只提供一些注释(注意访问本文中的链接以查看原文章,我怎么觉得自己走在翻译的路上了..我英语很差的..囧。)。

    分析者

    这个类型主要负责对表达式目录树进行分析,找出其中需要进行求值(简化)的子树,然后放入一个HashSet。

    class Nominator : ExpressionVisitor {
    //委托,可以由使用者提供一个方法来指定何种类型的表达式可以被求值 Func
    <Expression, bool> fnCanBeEvaluated; //哈希集,用来保存需要进行求值的表达式子树(表达式目录树的不可变性) HashSet<Expression> candidates; bool cannotBeEvaluated;
    //指定委托实例的构造函数
    internal Nominator(Func<Expression, bool> fnCanBeEvaluated) { this.fnCanBeEvaluated = fnCanBeEvaluated; }
    //返回哈希集
    internal HashSet<Expression> Nominate(Expression expression) { this.candidates = new HashSet<Expression>(); this.Visit(expression); return this.candidates; } //核心方法,分析表达式树 protected override Expression Visit(Expression expression) { if (expression != null) { bool saveCannotBeEvaluated = this.cannotBeEvaluated; this.cannotBeEvaluated = false;
     
    //重写了Visit方法,但是仍需要调用基类的Visit
    //自底向上
    base.Visit(expression); if (!this.cannotBeEvaluated) { if (this.fnCanBeEvaluated(expression)) { this.candidates.Add(expression); } else { this.cannotBeEvaluated = true; } } this.cannotBeEvaluated |= saveCannotBeEvaluated; } return expression; } } }

    由于该类型继承了ExpressionVisitor(处于System.Linq.Expressions名称空间),所以是深度优先的,一个实现样本可以参考这里。所以整体的流程应该是这样的,假设A-B-C。那么先判断C是否可求值,若C不可求值,由于cannotEval|=savecannotEval;导致cannotEval为true,B就被排除了,然后A也被排除。当然,如果ABC都可以求值,那么都会添加,但是由于另一个类型的逻辑,这点不成问题。

    求值者

    这个类型主要接受分析者的分析结果,然后对原表达式树进行处理。

     class SubtreeEvaluator: ExpressionVisitor {
    //子树列表 HashSet
    <Expression> candidates;
    //带参数的构造函数
    internal SubtreeEvaluator(HashSet<Expression> candidates) { this.candidates = candidates; }
    //求值
    internal Expression Eval(Expression exp) { return this.Visit(exp); } //需要修改为public,如果是继承System.Linq.Expressions里的ExpressionVisitor的话
    protected override Expression Visit(Expression exp) { if (exp == null) { return null; } if (this.candidates.Contains(exp)) { return this.Evaluate(exp); }
    //注意这里
    return base.Visit(exp); }
    //这里就是上面用到的求值方法
    private Expression Evaluate(Expression e) { //常量表达式不需要处理
    if (e.NodeType == ExpressionType.Constant) { return e; } LambdaExpression lambda = Expression.Lambda(e); Delegate fn = lambda.Compile(); return Expression.Constant(fn.DynamicInvoke(null), e.Type); } }

    这个类型的目标很明确,结构也很清楚。最主要的方法是Evaluate,负责转换子树。重写Visit方法是为了遍历表达式树。值得注意的是,Visit方法里面也执行了基类的Visit,但是这个流程却是自顶向下的,这是因为Evaluate方法(或者说自定义流程)调用的时机不同。对比下[分析者]类型的Visit实现就知道了。另外,由于是自顶向下的,当判定一个较深的树可求值之后会直接对其求值并返回一个常量表达式,所以不会继续分析这棵子树了。所以上个[分析者]类型将“有重叠”的子树加入哈希集的做法不会带来麻烦。

    调用入口

    View Code
    public static class Evaluator {
        public static Expression PartialEval(Expression expression, Func<Expression, bool> fnCanBeEvaluated) {
    
            return new SubtreeEvaluator(new Nominator(fnCanBeEvaluated).Nominate(expression)).Eval(expression);
    
        }
    
     
        public static Expression PartialEval(Expression expression) {
    
            return PartialEval(expression, Evaluator.CanBeEvaluatedLocally);
    
        }
    
     
    
        private static bool CanBeEvaluatedLocally(Expression expression) {
    
            return expression.NodeType != ExpressionType.Parameter;
    
        }

    这个就没什么好说了,实际上就一个静态方法,但是提供了一个重载,可以方便调用(采用默认的方式来确定一个子树是否可求值),而最后那个私有静态方法就是默认的委托实例。

    调用

    接下来只需要在Translator类型中使用这个处理就可以了。

    View Code
            public string Translate(Expression expression)
            {
                //在这里对本地变量进行处理
                expression = Evaluator.PartialEval(expression);
                //...执行转换
                Visit(expression);
                return string.Format("(&{0})", sb.ToString());
            }

    结果

  • 相关阅读:
    梯度下降法以及实现
    常见的端口号及其用途
    vue build报copy-webpack-plugin] unable to locate异常的解决方法
    vue build错误异常的解决方法
    Websocket-Sharp获取客户端IP地址和端口号
    理解SignalR
    城市经纬度 json
    FFmpeg部署及相关指令操作说明
    C#中Skip和Take的用法
    SQL Server 2008R2 :远程调用失败 的解决方法(全部方法)
  • 原文地址:https://www.cnblogs.com/lightluomeng/p/3042449.html
Copyright © 2020-2023  润新知