zoukankan      html  css  js  c++  java
  • 当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也就真正支持了嵌套,使实现和语义达成了一致。

  • 相关阅读:
    将PHP文件生成静态文件源码
    Entity Framework Code First 学习日记(6)一对多关系
    Entity Framework Code First 学习日记(5)
    Entity Framework Code First 学习日记(3)
    Entity Framework Code First 学习日记(7)多对多关系
    Entity Framework Code First学习日记(2)
    Entity Framework Code First 学习日记(8)一对一关系
    Entity Framework Code First 学习日记(9)映射继承关系
    Entity Framework Code First 学习日记(10)兼容遗留数据库
    Entity Framework Code First 学习日记(4)
  • 原文地址:https://www.cnblogs.com/vwxyzh/p/1895023.html
Copyright © 2011-2022 走看看