考不上三本也能给自己心爱的语言加上Coroutine(二)

考不上三本也能给自己心爱的语言加上Coroutine(二)

你现在所阅读的并不是第一篇文章,你可能想看目录和前言

上一篇提到了一些考上了三本的人必然会明白的知识,毕竟只是改写代码而已,让你把代码从一个样子翻译成另一个样子,这种事情就像把主动(push)模型的代码和被动(pull)模型的代码来回改一样容易——毕竟说的就是同一件事情嘛。写个for循环输出1到10,是push。写个接口人家调用你10次依次返回1到10,是pull。push转pull无非就是把本来函数里面的变量放到了成员变量里面去,然后再搞点事情。很多架构上的问题,都可以通过在push和pull之间来回穿插来解决,后来就衍生出了很多概念。如果不熟悉push和pull的做法的话,这些概念你可以学很久。

不过这毕竟是人在做,人在做总是可以不断的试错最后得到一个方法。但是要写程序去做怎么办呢?其实这用的知识更简单,是高中上数学课就讲的(年纪大一点的可能没有,广东是比我小两届的人开始学的)画流程图的问题。以前学习的都是把流程图表达成程序,现在要反过来,把程序表达成流程图。如果你做到了这一点,那么这个实现Coroutine的算法基本上就完成了90%了。

今天先讲简单的那部分,不涉及对try-catch语句的处理。处理try-catch一直都是一件麻烦的事情,早期C#的await的实现甚至禁止你把await写在try里面,最大的原因其实是想要不出bug,因为细节实在太多(逃

那现在先来普及一下流程图的概念。这里用的流程图跟高中的流程图有一点区别。高中的流程图每一个结构都很简单,但是功能很多。现在我这个流程图的一个节点比较复杂一点,但是就只有这一个节点。节点长这样:

每一个节点都会包含要执行的一些语句,执行完之后,会按照(有顺序的)条件跳转,如果都不满足,或者干脆没有条件,就跳转到“其他情况”的方向。

现在让我们来回顾一下之前的那个函数(首先要解语法糖,包括SHIT_CALL!语句):

int Fuck(IEnumerable<int> xs)
{
    int sum = 0;
    var _xs = xs.CreateEnumerator();
    while (_xs.Next())
    {
        var x = _xs.Current;
        SHIT!
        sum += x;
    }
    return sum;
}

现在我们要做两件事情,第一个是要把所有的变量都声明在函数的最前面(于是就不会被包括在代码里),然后变量声明就变成了普通的赋值,第二个就是画流程图了。其中要注意的一点事,我们也要把SHIT!变成一个单独的语句,而且SHIT!后面的语句必须开启一个新的节点。在这里我们并没有要求说一个流程图的节点只能包含一条语句,你当然想塞多少就塞多少。节点做少了你难免需要在不同的地方重复相同的语句,节点做多了你浪费在while (true)里面那个switch的时间就多,该怎样适可而止,是一个权衡的问题,我就不深入讲了。现在大家可以尝试一下在纸上画出这个函数的流程图,记住SHIT!后面的语句必须开启一个新的节点,被跳转的地方当然也只能新开节点,画完应该全部都是长下面这个样子:


注意到这里的_xs.Next()被重复了两次。通常直接用程序生成的流程图,可能直接就给个菱形——也就是没有语句只有条件的节点,然后在sum+=x;后面指过去。这当然也是可以的,最后要不要变成这个图的画法也是大家的选择。现在我在Workflow的实现就没有输出这么紧凑的流程图,以后发现有性能问题了再改(逃

那有了这个流程图之后怎么办呢?之前说搞定流程图就搞定了90%,剩下的10%自然是直接翻译成代码啦。在翻译的时候要注意下面几点:

  • 所有在不同的节点里面都用到的变量都要搬进成员变量里(其实你偷懒全部搬进去也可以)。
  • 每一个节点都是一个相应的case语句。
  • 生成正确的节点间跳转的代码(在下面)。

在这里软SHIT!都没有画出来。其实只有SHIT!结尾的节点的跳转才是硬的,其他的都是软的。还记得软硬SHIT!出来的代码的区别吗?遇到return也要生成相应的代码。其实死代码处理的方法也是类似的,你从函数的开头开始,不断的做深度优先搜索,遇到return就停下来。搜完之后所有没碰到过的代码都是死代码。

现在我们给流程图里面的四个节点分别编号为0、1、2、3,然后先给出一个大的框架。这个框架除了case的数目和变量的数目有变化以外,其他的都是死的。

class Fuck_IShitCallable : IShitCallable<int>
{
    public int state = 0;
    public int Result {get; set;}

    public IEnumerable<int> xs;
    public int sum;
    public IEnumerator<int> _xs;
    public int x;

    void ShitCall()
    {
        while (true)
        {
            switch (state)
            {
            case 0:
                // 待填
                break;
            case 1:
                // 待填
                break;
            case 2:
                // 待填
                break;
            case 3:
                // 待填
                break;
            default:
                throw EatShitException();
            }
        }
    }
}

IShitCallable<int> Fuck(IEnumerable<int> xs)
{
    return new Fuck_IShitCallable{ xs=xs };
}

函数进来的第一个节点永远编号为0,把state初始化成0也就是这个意思。函数的返回值就给存到Result里面。然后执行下面几步:

  • 遇到跳转就编译为{ state = ?; continue; }
  • 遇到SHIT!就编译为{ state = ?; return false; }
  • 遇到return就编译为{ Result = ?; state = -1; return true; }
  • 其他的照搬

然后我们就可以分别把4个待填的地方分别填入:

case 0:

sum = 0;
_xs = xs.CreateEnumerator();
if (_xs.Next()) { state = 1; continue; }
{state = 3; continue; }

case 1:

x = _xs.Current;
{ state = 2; return false; } // SHIT!

case 2:

sum += x;
if (_xs.Next()) { state = 1; continue; }
{state = 3; continue; }

case 3:

{ Result = sum; state = -1; return true; }

大功告成!

对比一下上一篇文章的代码,一眼看上去结构差很远,但实际上表达的是同一个意思。push转pull带来的一个问题就是代码会不是很好懂,所以语言才需要提供这样的一个机制,好让我们写出好懂的代码。

同样是把普通函数转成Coroutine,是不是今天感觉就简单好多了,很多昨天还觉得云里雾里的,是不是今天就看得比较清楚了?你这么感觉就对了!本来这就是考不上三本也会的内容嘛,要是看起来觉得难才应该检讨一下自己是不是真的毕业了要做程序员。

今天就写到这里了。到了这里,在实现Coroutine的道路上,我们已经从昨天的:

函数 --(人肉处理)--> Coroutine

变成了今天的:

函数 --(人肉处理)--> 流程图 --(机械化处理)--> Coroutine

了,我们经过了一篇文章的时间,就已经把问题简化了。那么从函数弄出来流程图是不是也有机械化的方法呢?答案当然是有的。但是我先想让大家消化一下,毕竟考不上三本的人也不会是什么天才,不要浮躁,慢慢学才是硬道理。

下一篇将会讨论如何做出一个函数到流程图的算法,顺便把try-catch的事情也考虑进去。

流程图 Powered By Microsoft Powerpoint 2016
发布于 2017-03-25

文章被以下专栏收录