结构化编程:如何编写正确的if语句

2021-01-15 20:07:03

if语句(或if表达式)是每种现代编程语言的基石-它是如此普遍,以至于我们很少考虑应该如何准确地使用它,或者应该如何使用它。但是,尽管它很受欢迎,但如果它并不总是存在的话,并且它不像当今那样普遍存在,那么我认为它的作用还是有点被误解了。因此,在本文中,我将研究一些可以轻松避免的错误,以改进我们的代码。

这是一个自以为是的文本,即它包含了我的观点,尽管我有很多论点可以支持它们,但它们最终还是基于我对编程是什么以及应该是什么的核心信念。因此,此处是此建议所基于的一些内容,并按照假设进行了组织。

这种形式出现在骇客社区中称为“ SICP”的一本小书中,但我不知道他们是否是第一个陈述它的人。这意味着程序不仅仅是执行给定任务的指令-它们是文本,因此它们应与其他文本一样受相同的样式规则约束。

这看起来很不言自明,但是关键是我们必须不懈地努力,不仅要不断思考代码可能出错的方式,而且还要思考诊断潜在问题的方式。

我想我应该说些为什么我相信这些事情。很简单-因为所有其他方法都只是由于人为因素而失败,例如当支持该代码的人离开时,任何不考虑样式而编写的代码都必将成为传统。同样,维护缺少结构的代码也会导致错误(通常只能通过施加更多的结构来解决)。

最后,人们经常将良好的结构和可读性与速度进行对比。首先,这种比较有点不公平,因为结构良好的结构更易于优化。对于确实如此的情况,在99%的情况下,增益是如此之低,以至于没人在乎,因此即便如此,仍应将重点放在可读性和结构上。这是我抛出DEK这个非常著名的报价的时候:

程序员浪费大量时间来考虑或担心程序非关键部分的速度,而在考虑调试和维护时,这些对效率的尝试实际上会产生严重的负面影响。我们应该忘记效率低下的问题,大约有97%的时间是这样的:成熟前的优化是万恶之源。资源

请记住,他在计算机比现在慢一千倍的时候写了这篇文章,因此我们应该更加在意可维护性。

从前,所有的汇编语言都是人们没有方括号,而不得不使用跳转指令。对于那些不知道跳转是什么的人,如果您想象运行代码的虚拟机或解释器中的某个地方,有一个变量指示下一步应执行哪一行代码,那么跳转是一种功能基本上允许您将变量设置为所需的任何值-就像门户网站一样,它允许您从程序的任何位置转到任何其他位置。例如,典型的if语句在使用跳转编写时如下所示:

将输入与水位比较,如果少,则跳至Aif相等,跳至Bjump到CA:打开电机到CB:关闭电机C:...

您可以从此示例中看到,尽管功能强大,但跳转比if-s难得多,尽管它们有时可以使您的代码短一些,但它们会100%降低可读性,这几乎不是一件好事-如果重要的是代码无错误,所以您总是希望有一段更长的代码和可读性更高的代码,而不是较短但更难理解的代码。这种认识导致了当时所谓的“结构化编程”范式的兴起,该范式是基于这样的思想,即跳跃的力量不应暴露给程序员-这一点由Edsger W. Dijkstra提出。精读文章Go To语句被认为有害(goto等同于非汇编语言上下文中的跳转指令语句)。同时,块的意思是执行一个常见任务的集合语句(通常今天用大括号括起来表示)的想法被推进了,这两个想法催生了我们今天所知道的if语句:

if(输入小于水位){打开马达} else if(输入等于水位){关闭马达} else {...

采用结构化程序设计是一个漫长的过程,始于人们甚至质疑结构化程序是否甚至还能够编码所有可能的计算,但是今天,尽管有些人仍在捍卫goto的使用,但它(过程)在很大程度上已经完成- goto被放逐,结构化的编程风格在现代语言中如此普遍,以至于不再使用“结构化”一词,仅仅是因为没有什么可与之形成对比的。

重申一下,这样做的原因非常明显-使用方括号可使我们的代码更像我们通常相互交谈和编写的方式:我们经常说

“如果A然后转到上一页并从第12行开始阅读”。

(我想知道捍卫goto用法的程序员是否会像这样写和说话?)

goto驱动的编程风格的剩余部分是所谓的“提前退出”的可能性-能够从其主体的任何位置退出给定功能或方法的执行(通常是通过使用关键字return)而不是执行到最后。再一次,如果我们考虑普通的if语句:

为什么不这样写呢?我们已经确定goto不好,并且我认为在某些方面混合使用两种方法会更糟,因为这可能使您误以为代码实际上不是结构化的。此外,提前退出会使我们的代码在翻译成自然语言时像其他任何包含goto的代码一样令人困惑。想象有人说:

“如果您不早点回家,请忽略我在这句话之后要说的一切。给我喝杯咖啡。

因此,如果您的语言支持,则始终依靠隐式返回,而不是显式结束执行。如果没有,您可以遵循的简单规则是,您应该从if语句的所有分支返回,也可以不从任何分支返回。

我看到过早的返回模式最常见的情况是处理错误。例如,想象一个很长的函数,在开始时进行了一些验证。很多人会写:

注册(用户){如果(用户名有效){抛出无效的用户名}为用户注册用户创建帐户,初始化其他一些东西,在日志中发送操作,写电子邮件...}

这是我正在描述的问题的非常微妙的变体(由于使用了throw而不是return,这一事实变得更加微妙)。正确的方法是:

注册(用户){如果(用户名有效){抛出无效的用户名}其他{为用户注册用户创建帐户,初始化一些其他内容,在日志发送电子邮件中写入操作...}}

但是人们希望直接在主体中使用主要功能代码以避免缩进(特别是如果已经有很多缩进)。根据他们(他们有观点),错误处理在功能范围之外,我们可以说它们处于不同的抽象级别。在这种情况下,我们应该使用函数来构造代码并保持不同的抽象级别分离。

函数是语句的随机序列不是也不能永远是的所有东西-它们具有名称,具有独立的作用域(块也具有自己的作用域,但它们也继承父块的作用域),最重要的是,它们很容易进行调试(例如,您在堆栈跟踪的顶部上有出错的名称。因此,让我们看一下如何使上面的代码与函数一起使用。我认为这是一个非常酷且未充分利用的模式这样您就可以在不牺牲结构的情况下丢失缩进。

注册(用户){如果(用户名有效){抛出无效的用户名}否则{返回内部注册(用户)}}内部注册(用户){//或_signup为某些人命名,为用户注册创建帐户用户注册初始化其他内容在日志发送电子邮件中写入操作...}

我们可能会说,这是另一种不好的模式,与第一个模式有相同的根本原因-为了使事情简单明了,人们并没有将所有需要的结构都包含在健壮的代码中。

在if语句中,我们经常不得不处理多个相互依赖的条件。有时在这种情况下,人们尝试在一个带有多个分支的语句中处理所有这些语句,否则尝试使用if-s。我认为,最好是每个条件只有一个语句,然后嵌套语句以获得我们想要的控制流。

如果(a而不是b){这样做}否则(a){这样做} if(b){做另一件事}

这是典型的混乱,当我们只是在代码中添加内容而不进行重构时,我们最终会陷入困境,尽管它看上去并不那么糟糕,但几乎无法理解。我为该人必须调试此程序以解决毫无疑问会引起的问题而感到抱歉,我坚信他们唯一可以做的聪明事就是将处理a和the的代码分开。将b处理为两个单独的语句的代码,如下所示:

if(a){if(b){做其他事情} else {做此}} else {if(b){做其他事情} else {// ??? }}

您可以看到,此块现在看起来像是逻辑,而不是随机的条件堆。现在,我们可以对此进行推理,例如,我们可以指出,并非a和b值的所有组合都得到处理,但稍后会处理更多。简而言之,我们的代码不再像Rube Goldberg机器那样工作。

有人会辩称,必须在两个分支中重复执行另一事物块是一个缺陷,但我不同意-两个分支代表两个不同的案例,而我们以相同的方式处理这两个案例的事实几乎不是原因因为我们没有明确列举它们-我们失去了一些简洁性,但是在可读性和鲁棒性方面却获得了很多。

但是,如果我们必须重复多行,该怎么办?如我上面所说,将其包装为一个函数。

我们都知道,如果语句应该处理所有可能的情况,并且这样做很容易,尤其是如果我们在每个语句中保留一个条件,那么我们只需要确保每个if语句都具有else子句即可处理其他情况或其他情况。条件不满足。尽管很简单,但有时却会违反此规则。例如,当在给定情况下我们什么都不要做时,我们该怎么办?例如,假设我们有一个“ Sing Up”按钮,可以将其禁用,如果禁用,该按钮将不起作用。规范可能会说类似“如果按钮被禁用,则什么也不做。”以简单的方式将此要求转换为代码将导致以下结果:

我们的实现是正确的,但我认为该规范是错误的-作为用户,当我单击内容且没有任何反馈时,我会讨厌它(如果用户开始与您联系,您也会开始讨厌您)带着烦人的问题)。如果您在那里发出警报,您将不会失去任何东西,也不会赢得任何胜利。

在注册时单击click(){如果(启用了注册){signupuser}否则{show popup" signup被禁用,因为$ {reason}' }

除了使您的程序更好之外,此行可能对调试很有用-假设某些用户单击“注册”按钮而没有任何反应。这意味着在代码路径中的某个位置存在错误。但是,如果路径包含多个if语句,且没有其他-s,那么您将无法知道其中哪个出错了。无论发生的是用户看到的消息,前端代码,还是可以在日志中查找的异常(无论后端是后端),都可以对发生的事情进行某种标记所有这些问题都是微不足道的。

人们犯此错误的另一种情况是,您遇到许多情况以不同的方式处理,而您并不真正期望发生其他事情(如果您违反了将多个数据混为一谈的规则,这种模式将更加普遍一个陈述中的条件)。

if(a === 1){这样做} else if(a === 2){这样做} else {//在这里做什么? }

我在这里的建议是做某事-返回值,抛出异常,任何异常,留下面包屑,以备不时之需将帮助您解决问题。

无论我们使用if语句有多守时,我们程序的逻辑最终都取决于业务需求,而直接执行这些业务需求通常很难看。在这些情况下,您可以通过在数据结构甚至DSL中编码一些需求来帮助自己,并围绕这些数据结构编写代码。假设您具有以下要求。

用户注册后,向他们收取会员资格。会员费用为18岁以下的人10美元,年龄较大的人20美元。哦,专业会员也要额外支付2美元,除非订阅了5年以上,在这种情况下,他们需要支付1。

再次,从需求直接转到代码将导致混乱的块(请注意,随后有两个if-s-我认为这是反模式)。

如果(少于18){收费10}否则{收费20}如果(赞成){如果(订为5){收费1}否则{收费2}}

避免这种情况的一种方法是将该逻辑编码为数据。让我们回到需求并创建一个包含所有条件的表格。该表将如下所示:

之后,我们可以轻松地使用此表来获取给定用户的价格,例如,使用以下功能块:

相对费用=所有费用过滤器(年龄最小<用户。年龄&年龄最大)过滤器(针对最小<用户。针对<针对最大订阅)过滤器(promemberhup和plan.isPrototal =相对费用减少(费用对象1.price +费用。对象2.price)

我们不仅设法使代码变得非常漂亮,而且已经有了一种无需更改代码即可编辑规则的机制。

让我们再看一个示例,说明如何利用数据结构进行控制流。这与错误处理有关。我们经常会以多种方式出错,并且我们使用ifs对其进行编码,如下所示。

if(error1){返回错误:error1} else if(error2){返回错误:error2} else {doStuff()}

尽管这种方法与非合并规则(仅对彼此依赖的条件有效)并不矛盾,但在可读性方面有某些缺点,更不用说无法返回所有错误的列表了,这有时是首选。如果我们将错误检查视为接受输入的函数,并返回指示输入在给定条件下是否有效的结果,则可以将它们编码为表格,类似于上一个示例,只是这里的行将以自定义逻辑。

错误检查= [名称:18岁以下,检查:(用户)=>用户年龄< 18名称:不是pro,请检查:(用户)=> !user.isPro]

如前所述,这使得收集所有错误并以数组形式返回它们变得容易:

错误=错误检查过滤器(error.check(user))map(error.name)if(错误> 0){抛出错误}其他{做事情}