【问题标题】:Untying Knuth's knots: how to restructure spaghetti code?解开 Knuth 的结:如何重构意大利面条代码?
【发布时间】:2016-05-06 18:39:57
【问题描述】:

这个问题的灵感来自How to transform a flow chart into an implementation?,它询问了从代码中通过算法消除goto 语句的方法。 answer 对一般问题的描述见this 科学论文。

我已经按照 Knuth 的计算机编程艺术中的算法 X 的高级草图实现了一些代码,描述了带有受限前缀的字典排列的生成(参见draft 的第 16 页)。

这是上面算法对应的flow chart

这可能是一个非常聪明且非常高效的算法,但是代码的结构似乎很难遵循。我最终使用了旧的 goto 风格的实现:

//Algorithm X;
1:
initialize();
2:
enter_level(k);
3:
set(a[k],q);
if(test() == ok) {
  if (k == n) {
    visit();
    goto 6;
  }
  goto 4;
}
goto 5;
4:
increase(k);
goto 2;
5:
increasev2(a[k]);
if (q != 0) {
  goto 3;
}
6:
decrease(k);
if (k==0) {
  goto 7;
}
set(p,u_k);
goto 5;
7:
return;

问题是:如何重构此代码以消除所有 goto 调用?

一个(虚假的)答案是建议“查找被引用的科学论文,并逐行跟踪” - 事实上,这当然是一种可能性。但是这个问题是关于有经验的程序员一看到这个spaghetti code就会立即看到什么。

我对如何逐步重构感兴趣,而不仅仅是代码。


注意:

  1. 根据其高级规范和goto 跳转,实际实现算法 X 很简单。实现黑盒函数initialize() 等只需要一些额外的指令,但这些指令与代码的结构 无关。函数调用期间发生了什么并不重要,因为现在的重点是程序的流程。
  2. “是GOTO still considered harmful?”的通常辩论与这个问题完全无关,并且不应该在答案和 cmets 中解决。

相关:how to work with or complete the spaghetti code?

【问题讨论】:

  • 您可能想了解重新措辞这个问题。我已经读了好几遍了,我认为你是说你有一些算法生成的代码,你想要一个算法来重构生成的gotos...对吗?或者只是如何在没有gotos 的情况下执行第一个算法?
  • 我不确定我是否理解您的评论。标题是你如何重构这个意大利面条代码?,然后运行文本详细说明它:“问题是如何重构这段代码,通过消除其中的所有 goto 调用”。我不知道如何使它更清楚。
  • 这个问题没有包含足够的信息来重构代码。至少有六个变量。哪些是全局和/或常量?函数是自包含的还是有任何函数会改变程序的状态?
  • @Matsmath 你似乎有很多东西描述了不相关的细节。起初,这似乎是一个反问(或者你打算自己回答?)。似乎它可能是题外话......具有讽刺意味的是,我认为你的问题需要重构而不是意大利面条。如果你愿意,我可以看看移动细节以使帖子更清晰。
  • 一个有经验的程序员可能会对自己说“虽然很丑,但这段代码可以工作,但是在没有 goto 的情况下重写后,它很有可能不再工作,在一些微妙的,难以找到方法。保持原样(除非后来发现它实际上不起作用,然后重写)。”

标签: c++ algorithm loops refactoring software-design


【解决方案1】:

无需太多努力(而且风险不大),您可以快速减少 goto 和标签的数量。

1) 删除未在任何地方引用的标签(这将是标签 1:)

2) 查找除了 goto 之外无法输入的代码块,这些代码块在少数地方被调用。这些通常可以简单地分解出来。 4:可以通过将代码移动到它被调用的地方来处理,并且安全地完成,因为它唯一的出口是一个goto。这也允许我们删除它上面的 goto 5,因为该代码将简单地落入 5:。 7:可以通过修改if语句来处理。此时我们有

initialize();
2:
enter_level(k);
3:
set(a[k],q);
if(test() == ok) {
  if (k == n) {
    visit();
    goto 6;
  }
  increase(k);
  goto 2;
}
5:
increasev2(a[k]);
if (q != 0) {
  goto 3;
}
6:
decrease(k);
if (k!=0) {
  set(p,u_k);
  goto 5;
}
return;

我倾向于在这里停下来。但是如果你继续,它就变成了一个识别循环和用循环结构替换 goto 的问题。但是,由于代码的结构方式,进行这些更改的风险似乎要大得多。此外,您可能最终会遇到中断和继续,这无论如何都是一种 goto。我最终得到的是这个(如果没有一些非常严格的测试,我不会保证它的正确性):

initialize();
enter_level(k);
while (true) {
  set(a[k],q);
  if(test() == ok) {
    if (k == n) {
      visit();
    } else {
      increase(k);
      enter_level(k);
      continue;
    }
  } else {
    increasev2(a[k]);
    if (q != 0) {
      continue; 
    }
  }
  while (true) {
    decrease(k);
    if (k!=0) {
      set(p,u_k);
      increasev2(a[k]);
      if (q != 0) {
        break; 
      }
    } else {
      return;
    }
  }
}

我做了 3:一个循环,和 6:一个内循环。我通过复制 5: 代码代替 goto 并用 break 替换 goto 3 来摆脱 goto 5。这使得制作更清晰的循环变得更容易一些。 goto 6 是通过使用 else 来固定的。 goto 3 继续。

在此之后(如果你有剩余的能量),你可以尝试将循环从 while(true) with continue 更改为具有实际条件的 while。

最好先开发测试,然后进行一两次更改并进行测试。进行另一个更改,然后再次测试。如果您不这样做,很容易在早期犯下结构性错误,然后使后续步骤无效并迫使您重新开始。

【讨论】:

  • 我非常喜欢你如何获得这个的阐述和半算法方式。有趣的是,您的代码在许多方面与@old_mountain 的答案不同。您的代码包含一定程度的“代码重复”,因为在多行中调用了相同的函数(例如increasev2(a[k]))。当我处理具有多个入口和出口点的讨厌的嵌套循环时,我也观察到了这种现象。
  • 我最喜欢这个使用breakcontinue
【解决方案2】:

我早先在https://stackoverflow.com/a/36661381/120163 为 OP 绘制了一个算法

找到一篇更好的论文,讨论如何在准确保留原始控制流图的同时生成结构化代码:

W.D Maurer, "Generalized structured programs and loop trees", Science of Computer Programming, 2007

我遵循了该程序(在纸面上,希望我做得对,凌晨 2:40 看起来还可以)。他的基本技巧是找到强连接区域(代码中的循环);这些将成为循环;然后他通过删除一条边来打破这个循环;这最终成为一个循环反向链接(当他完成时恢复)。重复该过程,直到找不到更多循环;剩下的本质上是一个带有已识别循环的结构化程序。正确地做到这一点很棘手。你真的需要一个自动化的程序。您的代码虽然很小,但仍然很讨厌:-}

我在一处作弊。 Maurer 坚持认为前向 goto 是可以的,即使是在循环的中间。如果您购买它,那么您可以完全保留 CFG。如果没有,您必须处理循环有两个或多个入口点的情况;你的算法有这样一个循环。我通过编写循环代码解决了这个问题,并编写了一个循环尾端片段等效项,其作用类似于跳转到中间的第一次迭代,然后是循环本身。

我的符号有点滑稽:大多数语言没有“block{...}”结构。 [我在(见生物)中编码的那个]。将其视为“执行一次迭代”循环:-} 我假设块/循环具有循环退出并且循环继续。如果你没有这些,你可以用足够数量的 just block{ ... } 和 exit_block@N 来模拟它们。

接受后编辑:在白天,我没有做对,我遗漏了while循环@3。我已经修补了它;现在不再需要块构造,因为我可以退出 while 循环@3 以实现相同的效果。实际上,代码读起来更好一些。

我把你的数字标签留在了,即使是在不需要的地方,以便于参考。

//Algorithm X;
1:
initialize();
2:
while (true) {
   enter_level(k);
   3: 
   while (true) {
      set(a[k],q);
      if (test() == ok) {
         if (k != n) exit_while@3;
         visit();
         decrease(k); // replicate logic at 6 to avoid jumping into middle of 5 loop
         if (k==0) return;
         set(p,u_k);
      }
      5:
      while (true) {
         increasev2(a[k]);
         if (q != 0) continue_while@3;
         6:
         decrease(k);
         if (k==0) return;
         set(p,u_k);
      } // while(true)@5
  } // while(true)@3
  4:
  increase(k);
} // while(true)@2

与迄今为止我看到的大多数其他答案不同,它的运行速度与原始答案相同(没有额外的标志或标志检查)。

@hatchet 的回答很有趣; a) 它同样快,b) 他选择用相同的技术处理两个入口循环,但他选择“其他入口”作为循环顶部。他对标签 2 处的“enter_level(k)”操作做了类似的事情。

有趣的是,所有这些结构化似乎并没有一点点帮助代码的可读性。让人对“结构化程序”的全部意义感到好奇。也许精心设计的意大利面还不错:-}

【讨论】:

  • 很难想象接受除此之外的任何其他答案,因为这种方法指向科学,并有可靠的推理支持。被引用的论文是开放获取的,并且在 p 的顶部。 224 -- 我在这里引用 -- “没有像意大利面条代码这样的东西”很好地总结了人们应该期待的经历。同样值得注意的是,这是一篇相当新的论文。谢谢你的回答。
  • 感谢您的出色回答。我正在努力跟上。 if (q != 0) continue_while@3; 怎么能用 C++ 实现?我想常规的“继续”会跳到错误的地方?
  • C 和 C++ 的 break 和 continue 语句设计不佳。你想要的语言是可以退出命名块/循环或继续命名循环的语句,无论嵌套如何。我认为你必须使用“goto 3”。我仍然认为包含 goto 到结构化位置(循环开始/结束)的程序是结构化的。
【解决方案3】:

在c++中,算法可以写成:

void initialize() {}
void enter_level(int k) {}
void set(int x,int y) {}
bool test() { return true; }
void visit() {}
void increase(int k) {}
void increasev2(int k) {}
void decrease(int k) {}

void algorithm_x()
{
    int k{0};
    int a[] ={1,2,3,4,5};
    int q{0};
    bool ok{true};
    int n{0};
    int p{0};
    int u_k{0};

        //Algorithm X;
    lbl1:
        initialize();
    lbl2:
        enter_level(k);
    lbl3:
        set(a[k],q);
        if (test() == ok) {
            if (k == n) {
                visit();
                goto lbl6;
            }
            goto lbl4;
        }
        goto lbl5;
    lbl4:
        increase(k);
        goto lbl2;
    lbl5:
        increasev2(a[k]);
        if (q != 0) {
            goto lbl3;
        }
    lbl6:
        decrease(k);
        if (k==0) {
            goto lbl7;
        }
        set(p,u_k);
        goto lbl5;
    lbl7:
        return;

}

int main()
{
    algorithm_x();
    return 0;
}

假设我们不使用 break 语句,那么程序可能是:

void initialize() {}
void enter_level(int k) {}
void set(int x,int y) {}
bool test() { return true; }
void visit() {}
void increase(int k) {}
void increasev2(int k) {}
void decrease(int k) {}

void algorithm_x()
{
    int k{0};
    int a[] ={1,2,3,4,5};
    int q{0};
    bool ok{true};
    int n{0};
    int p{0};
    int u_k{0};

    bool skiptail{false};

    //Algorithm X;
    initialize();
    enter_level(k);
    while (true) {

        skiptail = false;
        set(a[k],q);
        if (test() == ok) {
            if (k == n) {
                visit();
                decrease(k);
                if (k==0) {
                    return;
                }
                set(p,u_k);
                while (true) {
                    increasev2(a[k]);
                    if (q != 0) {
                        //goto lbl3;
                        skiptail = true;
                    }
                    if (!skiptail) decrease(k);
                    if (!skiptail) if (k==0) {
                        return;
                    }
                    if (!skiptail) set(p,u_k);
                }
            }
            if (!skiptail) increase(k);
            if (!skiptail) enter_level(k);
            //goto lbl3;
            skiptail = true;
        }
        if (!skiptail) while (true) {
            increasev2(a[k]);
            if (q != 0) {
                //goto lbl3;
                skiptail = true;
            }
            if (!skiptail) decrease(k);
            if (!skiptail) if (k==0) {
                return;
            }
            if (!skiptail) set(p,u_k);
        }
        if (!skiptail) increase(k);
        if (!skiptail) enter_level(k);
        //goto lbl3;
        skiptail = true;
        if (!skiptail) while (true) {
            increasev2(a[k]);
            if (q != 0) {
                //goto lbl3;
                skiptail = true;
            }
            if (!skiptail) decrease(k);
            if (!skiptail) if (k==0) {
                return;
            }
            if (!skiptail) set(p,u_k);
        }
    }

}

int main()
{
    algorithm_x();
    return 0;
}

更改使用了以下算法:

  1. 摆脱未使用的标签。删除lbl1

  2. 如果标签以 goto 结尾,则在使用它的地方替换该块。 删除lbl4lbl6lbl7

  3. 如果标签返回到自身,则在 while (true) 中放置块。 删除底部lbl5lbl5 现在是独立的,可以在使用的地方更换)

  4. 如果一个块是自包含的,则在使用它的地方替换它。 删除lbl5

  5. 如果一个标签跟随另一个标签,则在块的末尾放置一个 goto next 标签,以便可以按照规则 2 替换它。 去掉lbl2(可以goto lbl3

  6. 现在我们在整个代码中留下了最后一个标签的goto。将goto lbl3 替换为skiptail=true,将剩余块放入while (true) 块中,并设置剩余语句以检查是否为skiptail=false。 删除lbl3 并替换为skiptail = false

【讨论】:

  • 令人印象深刻的是,你放下了你的手套,尽管你早先有顾虑,但你还是设法重构了这头野兽。您现在看到,变量和函数调用确实 离题了。虽然您在此答案中的code 肯定几乎无法阅读,但据我所知,它是由您提到的六个规则完全生成的。这正是我要问的。我需要一两天时间来深入了解一般你在这里提出的建议。但它确实看起来像一个算法:-)。
  • skiptail 的目的是一个很小的细节。它为什么在那里,它来自哪里?
  • 一旦我们进入第 6 步,就无法继续使用替换标签的初始方法,因为我们现在需要递归。因此,另一种想法是通过引入一个变量来保留程序行为,该变量将“记住”我们现在已经跳转到标签 3。
【解决方案4】:

我从未使用过goto,但这似乎是一个有趣的挑战,所以我尝试了自己的重构。

首先,过一遍代码,看看每个标签有多少条语句gotoing;记住这一点很重要,以避免错误。在您的示例中,没有任何内容导致 1,因此我们可以忽略它。

有时,当控制流隐含 gotos 时,我发现添加它们很有用。当我在事物之间移动代码时,它有助于跟踪顺序。

重构gotos 的最佳方式是从内向上或自下而上。

  • 最后一条指令是7:return;,它可以简单地移动到调用goto 7 的任何地方。这很容易。

  • 接下来,我尝试查看哪些标签以goto 结尾(无条件),然后直接出现在不同的 goto 之后。在这种情况下为 4;它可以移动到 2 的前面,在一个由哨兵控制的 if 内(为循环做准备)。 (goto我的第一个观点是现在可以删除2。)

  • 接下来我将 5 和 6 放入一个循环中。如果我错了,无论如何我都可以原路返回。

  • 此时我看到6会在3或5之后执行。我也看到5可以执行3,所以我决定将3移到5之后。我添加一个变量以便我可以跳过5 第一次。我在 6 结束时将其设置为 true。

  • 为了保证 5 在需要的时候可以直接到 6,我可以将 3 包裹在 if 语句中,与 5 的执行条件相反。当我确实需要从 5 到 3 时,我可以在我在 5 内时更改条件,以便之后直接执行 3。

  • 此时我只有一个goto,它从3 变为4。如果我将其更改为break,我可以退出一个循环并到达最后。为了达到 4,我只是将所有内容(除了 1)包装在一个循环中。

您可以使用this trick 来跳出嵌套循环而不使用goto,如果您有任何这种情况,但在这种情况下没有必要这样做。

最后,我得到了这段代码(包括标签只是为了清楚起见):


1: initialize();
reached4=false;
do5 = false;
while(true){
    if (reached4){
      4: increase(k);
    }
    2: enter_level(k);
    while(true){
      if(do5){
        5:
        increasev2(a[k]);
        if (q != 0) {
          do5 = false;//goto 3
        }
      }
      if(!do5){
        3:
        set(a[k],q);
        if(test() == ok) {
          if (k == n) {
            visit();//goto 6;
          }else{
            reached4 = true;
            break;//goto 4
          }
        }
      }
      6:
      decrease(k);
      if (k==0) {
        7: return;
      }
      set(p,u_k);
      do5 = true;
    }
}

【讨论】:

  • 这与@old_mountain 的答案非常相似,但这里有一些中断语句。我将您的代码复制粘贴到code2flow.com,但流程图非常可怕。确实似乎摆脱gotos 弊大于利。关于 lambdas 的好点。我应该研究的另一件事。
  • @Matsmath 有趣的是,当您将这两个程序放入该工具时,它们看起来非常不同。在比较了这两个流程图之后,我觉得我更喜欢我的,因为它又长又细(我认为这很容易理解)。
【解决方案5】:

您可以使用大量变量来模拟 goto 的流程以使用 if'swhile's

initialize();

enterLevel = true;
executeWhile = true;

do 
{

    if (enterLevel)
    {
        enter_level(k);
    }

    enterLevel = false;

    goto4 = false;
    goto5 = false;
    goto6 = false;

    set(a[k],q);
    if(test() == ok) 
    {
        if (k == n) 
        {
            visit();
            goto6 = true;
        }
        else
        {
            goto4 = true;
        }
    }
    else
    {
        goto5 = true;
    }

    if (goto4) 
    {
        increase(k);
        enterLevel = true;
    }
    else
    {
        do
        {
            if(goto5)
            {
                increasev2(a[k]);
                goto6 = goto5 = !(q != 0); // if (q != 0) { goto6 = goto5 = false; } else { goto6 = goto5 = true; }
            }
            if(goto6)
            {
                decrease(k);
                executeWhile = !(k==0); // if (k == 0) { executeWhile = false; } else { executeWhile = true; }
                set(p,u_k);
                goto5 = true;
            }
        } while (goto5 && executeWhile);
    }
} while (executeWhile);

这个版本是否比goto's的版本更好我不能说。


首先,我将所有标签完全分开。

然后我发现这里有 2 个循环:

1 - 
    * label 4 -> goto 2
    * label 5 -> goto 3. 

两者都转到代码的顶部,但一个执行 enter_level(k) 而另一个不执行。 这就是 enterLevel 变量的原因。

2 - 
    * label 6 -> goto 5. This goes up a little in the code, and then executes again. 

在这个循环中,有两种情况会退出:

    * label 5 -> goto 3. The same as before, but now inside a nested loop
    * label 6 -> goto 7. The way out of the outer loop.

其他变量和 if 只是为了维持控制流。

是的,我可以使用一些中断(代码可以变得更短), 但由于问题是关于 goto 的,我个人更喜欢不使用它们。

【讨论】:

  • 我相信你这是一个正确的实现,或者如果不是,那么任何小细节都可以在以后修复。我看到程序的流程或多或少与goto 方法相同,除了一些仔细定位的 do-while 语句。绝对令人惊讶的是——对我来说——你设法在没有调用break 的情况下完成了所有这些工作(尽管一些控制变量可能只是起到相同的作用)。您能否详细说明是如何提出这个实现的?你看着意大利面……然后……做了什么,按照什么顺序,为什么?需要详细说明吗?
  • @Matsmath 添加了解释。
  • 所以不是“goto X”,而是“gotoX=true”,然后是条件“if (gotoX) { X: ... }”。是的,这显然有效。但这让我觉得 OP 对改进代码的兴趣失败了。您还没有摆脱任何 goto;你只是简单地一对一地模拟了它们,每一个都发生了。 (而且代码运行更慢,太启动了)。
  • @IraBaxter 是的,我同意你的看法。正如我在答案中所说,我不能说这比原来的更好。没有goto也是一样的
猜你喜欢
  • 1970-01-01
  • 2010-11-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-09-16
  • 2012-01-26
相关资源
最近更新 更多