• 当foreach遇到yield和上下文切换时


        说到c#里面foreach应该是尽人皆知的了,不过,各位是不是了解foreach是怎么工作的哪?

        大多数情况下,即使不了解foreach是如何工作的,照样可以把代码写的很正确。不过,前两天我在写一段代码时,却不得不把foreach大卸八块,原因就是遇到了yield和上下文切换,详细情况听我慢慢道来。

    情景介绍

        首先说说,整个应用程序的场景。这个应用程序是用来对账的,因此涉及很多数据的读取和比对,同时由于业务的快速发展,减少对账相对于各应用的发布的滞后时间,采用了Xml定义对账,并支持sql和内嵌c#等代码片断的方式,以及添加了独立的脚本支持。

        同时为了最大限度的增强Xml的表现能力,添加了一个Xql的书写方式(抄袭Linq的思想,只不过是xml表达的),例如:

          <value xsi:type="Xql">
            <from as="x">
              <value xsi:type="Eval" expression="call Split ($a)"/>
            </from>
            <where>
              <value xsi:type="Eval" expression="$x.Length==1"/>
            </where>
            <join as="y" on="$x" equals="$y" method="LeftJoin">
              <value xsi:type="Eval" expression="call Split ($b)"/>
            </join>
            <let as="y" on="$y==null">
              <value xsi:type="Eval" expression="$x+'1'"/>
            </let>
            <select>
              <prop as="X">$x</prop>
              <prop as="Y">$y</prop>
              <prop as="Length">$y.Length</prop>
            </select>
          </value>

        其中$a,$b为上下文中的变量,而$x和$y为Xql中查询的当前值。

        其中Xql的执行被转换成下面的代码:

            private IEnumerable<ScriptObject> SelectResult(IEnumerable<XqlItem> items)
            {
                foreach (var item in items)
                {
                    ScriptObject so = new ScriptObject();
                    foreach (var prop in this.select)
                        so[prop.@as] = Eval(item, prop.Value);
                    yield return so;
                }
            }

        在最初的测试中,程序非常完美的做出了正确的结果。但是,这里有一个非常隐蔽的问题,过了好久才被发现。

    发现问题

        发现问题的时候总是充满着以外,Xql虽然可以提供相对较清晰的逻辑,设计得非常灵活,所以Xql允许嵌套,例如:

          <value xsi:type="Xql">
            <from as="xy">
              <value xsi:type="Xql">
                <from as="x">
                  <value xsi:type="Eval" expression="call GetSource1()"/>
                </from>
                <join as="y" on="$x.Key" equals="y.Key" method="InnerJoin">
                  <value xsi:type="Eval" expression="call GetSource2()"/>
                </join>
                <select>
                  <prop as="Key">$x.Key</prop>
                  <prop as="Prop1">$x.Prop1</prop>
                  <prop as="Prop2">$y.Prop2</prop>
                </select>
              </value>
            </from>
            <join as="z" on="$xy.Key" equals="$z.Key" method="FullJoin">
              <value xsi:type="Eval" expression="call GetSource3()"/>
            </join>
            <select>
              <prop as="Key">$xy.Key</prop>
              <prop as="Prop1">$xy.Prop1</prop>
              <prop as="Prop2">$xy.Prop2</prop>
              <prop as="Prop3">$z.Prop3</prop>
            </select>
          </value>

        这里,先把x和y两个数据源Join成一个xy的数据源,再把xy这个数据源与z做Join,这个查询非常合理,而且也可以充分体现Xql在处理复杂情况时的表达能力,但是一个不幸的bug就发生了。

        Xql在设计的时候,定义为总是延迟执行,因此,在外层Xql真正执行前,内层的Xql是不会执行的,表面上看很合理,但是,如果内层的Xql依赖一个函数的参数,而且,在这个函数内,并未对外层Xql做任何查询,仅仅是将Xql的结果返回(类似一个c#方法返回一个IQueryable对象),那么在外层Xql开始迭代时,才会创建出内层Xql,但此时,内层Xql已经无法知道当初的函数的参数的值。

        此时,杯具就产生了,一个完全符合Xql语义的Xql实例,却无法做出一个正确的结果。

    思考方案

        首先,方案应该是积极的,而不是消极的。也就是应该去支持嵌套,而不是想尽办法去阻止嵌套。

        其次,参考c#里面的Linq。可以发现c#用的是闭包来解决这个问题,使Linq在执行时的上下文和创建时的一样。

        那么,我们也可以创建出一个上下文对象,并且在执行时,让延迟的那部分代码总是在这个被抓去下来的上下文中执行。

    执行上下文

        因此,很容易想到的ExecutionContext,其实也很容易实现,在轻松搞定后,这时的SelectResult方法就需要修改方法签名了,因为它需要传递当时的上下文:

    private IEnumerable<ScriptObject> SelectResult(IEnumerable<XqlItem> items, ExecutionContext context)

        当然,上下文提供一个基本的方法:

    void Execute(Action action)

        这个方法保证action的执行一定是在之前被抓去下来的上下文中执行的,而不是当前的上下文。

        那么,这个SelectResult怎么实现哪?

    foreach+yield+上下文切换

        首先,你可能会想到的是这么写:

                context.Execute(() =>
                {
                    foreach (var item in items)
                    {
                        ScriptObject so = new ScriptObject();
                        foreach (var prop in this.select)
                            so[prop.@as] = Eval(item, prop.Value);
                        yield return so;
                    }
                });

        然后,很遗憾的发现vs会很“聪明”的提示你:不能在匿名方法或 lambda 表达式内使用 yield 语句。

        其次,你可能会想到:

                return items.Select(item =>
                {
                    ScriptObject so = new ScriptObject();
                    context.Execute(() =>
                    {
                        foreach (var s in this.select)
                            so[s.@as] = Eval(item, s.EvalValue);
                    });
                    return so;
                });

        很好,你掌握了linq,但是搞错了方向。要切换上下文的不是Select的过程,而是迭代的时候。

        当外层Xql执行时,内层Xql是作为一个数据源存在的,也就是在执行MoveNext操作时,内层Xql才会被执行,换句话说,就是只有MoveNext时,才需要切换掉上下文。

    正解

        经过上面的分析就浮出水面了,需要把foreach大卸八块,也就是还原到可以被抓取到真正执行MoveNext方法的时候:

                IEnumerator<XqlItem> x = null;
                try
                {
                    x = items.GetEnumerator();
                    bool moved = false;
                    context.Execute(() => moved = x.MoveNext());
                    while (moved)
                    {
                        ScriptObject so = new ScriptObject();
                        foreach (var prop in this.select)
                            so[prop.@as] = Eval(x.Current, prop.Value);
                        yield return so;
                        context.Execute(() => moved = x.MoveNext());
                    }
                }
                finally
                {
                    if (x != null)
                        x.Dispose(); 
                }

        这样,Xql也就真正支持了嵌套,使实现和语义达成了一致。

  • 相关阅读:
    win10下python环境变量设置
    c++ primer第15章这几个例子中的构造函数形式不太理解
    ++与*
    C++符号优先级
    56-Remove Linked List Elements
    55. Binary Tree Preorder Traversal
    54. Flatten Binary Tree to Linked List
    野指针--内存泄漏--缓存区溢出--栈溢出
    数组指针和指针数组的区别
    53-Linked List Cycle II
  • 原文地址:https://www.cnblogs.com/vwxyzh/p/1895023.html
Copyright © 2020-2023  润新知