【问题标题】:Programming without if-statements? [closed]没有 if 语句的编程? [关闭]
【发布时间】:2013-04-12 13:42:23
【问题描述】:

我记得有一段时间(可能是几年)前,我在 Stackoverflow 上读到了关于使用尽可能少的 if 测试进行编程的魅力。 This question 有点相关,但我认为压力在于使用许多小函数,这些函数返回由测试确定的值,具体取决于它们收到的参数。一个非常简单的例子是使用这个:

int i = 5; 
bool iIsSmall = isSmall(i);

isSmall() 看起来像这样:

private bool isSmall(int number)
{
    return (i < 10);
}

而不仅仅是这样做:

int i = 5;
bool isSmall;
if (i < 10) {
    isSmall = true;
} else {
    isSmall = false;
}

(从逻辑上讲,这段代码只是示例代码。它不是我正在制作的程序的一部分。)

我相信这样做的原因是因为它看起来更好,并且使程序员更不容易出现逻辑错误。如果正确应用此编码约定,您将在任何地方几乎看不到 if 测试,除了在唯一目的是进行该测试的函数中

现在,我的问题是:有没有关于这个约定的文档?有没有地方可以看到这种风格的支持者和反对者之间的激烈争论?我尝试搜索向我介绍此内容的 Stackoverflow 帖子,但我再也找不到了。

最后,我希望这个问题不会被否决,因为我不是在寻求解决问题的方法。我只是希望能听到更多关于这种编码风格的信息,并且可能会提高我未来将要进行的所有编码的质量。

【问题讨论】:

标签: if-statement coding-style functional-programming conventions


【解决方案1】:

这整个“如果”与“不如果”的事情让我想起了Expression Problem1。基本上,观察到使用 if 语句或不使用 if 语句进行编程是封装和可扩展性的问题,有时使用 if 语句2 更好,有时使用方法/函数的动态调度更好指针。

当我们想要对某事物建模时,需要担心两个轴:

  1. 我们需要处理的输入的不同情况(或类型)。
  2. 我们要对这些输入执行的不同操作。

实现这种事情的一种方法是使用 if 语句/模式匹配/访问者模式:

data List = Nil | Cons Int List

length xs = case xs of
  Nil -> 0
  Cons a as -> 1 + length x

concat xs ys = case ii of
  Nil -> jj
  Cons a as -> Cons a (concat as ys)

另一种方法是使用面向对象:

data List = {
    length :: Int
    concat :: (List -> List)
}

nil = List {
    length = 0,
    concat = (\ys -> ys)
}

cons x xs = List {
    length = 1 + length xs,
    concat = (\ys -> cons x (concat xs ys))
}

不难看出,使用 if 语句的第一个版本可以轻松地对我们的数据类型添加新操作:只需创建一个新函数并在其中进行案例分析。另一方面,这使得向我们的数据类型添加新案例变得困难,因为这意味着要返回程序并修改所有分支语句。

第二个版本正好相反。向数据类型添加新案例非常容易:只需创建一个新“类”并告诉我们需要实现的每个方法做什么。但是,现在很难向接口添加新操作,因为这意味着为所有实现该接口的旧类添加新方法。

语言有许多不同的方法来尝试解决表达式问题,并使向模型中添加新案例和新操作变得容易。但是,这些解决方案各有利弊3,所以总的来说,我认为在 OO 和 if 语句之间进行选择是一个很好的经验法则,具体取决于您想要更容易扩展内容的轴。

不管怎样,回到你的问题我想指出几点:

第一个是我认为摆脱所有 if 语句并用方法分派替换它们的 OO“口头禅”与大多数 OO 语言没有类型安全的代数数据类型有关,而不是它必须做的“if statemsnts”不利于封装。由于类型安全的唯一方法是使用方法调用,因此鼓励您将使用 if 语句的程序转换为使用 Visitor Pattern4 或更糟的程序:将应该使用访问者模式的程序转换为程序使用简单的方法分派,因此容易在错误的方向上进行扩展。

第二件事是我不喜欢仅仅因为你可以将事物分解为函数。特别是,我发现所有函数只有 5 行并调用大量其他函数的风格很难阅读。

最后,我认为您的示例并没有真正摆脱 if 语句。本质上,您所做的是将函数从 Integers 转换为新数据类型(有两种情况,一种用于 Big,另一种用于 Small),然后在处理数据类型时仍然需要使用 if 语句:

data Size = Big | Small

toSize :: Int -> Size
toSize n = if n < 10 then Small else Big

someOp :: Size -> String
someOp Small = "Wow, its small"
someOp Big   = "Wow, its big"

回到表达式问题的角度,定义我们的toSize / isSmall函数的好处是我们把选择我们的数字适合什么情况的逻辑放在一个地方,我们的函数只能对之后的情况进行操作那。但是,这并不意味着我们已经从代码中删除了 if 语句!如果我们将 toSize 作为工厂函数,并且让 Big 和 Small 类共享一个接口,那么是的,我们将从代码中删除 if 语句。但是,如果我们的 isSmall 只返回一个布尔值或枚举,那么 if 语句的数量将与以前一样多。 (并且您应该选择使用哪种实现方式,具体取决于您是否希望将来更容易添加新方法或新案例——比如 Medium——)


1 - 问题的名称来自您拥有“表达式”数据类型(数字、变量、子表达式的加法/乘法等)并希望实现诸如评估函数和其他事物之类的问题。

2 - 或者在代数数据类型上进行模式匹配,如果你想要更安全的类型...

3 - 例如,您可能必须在“调度员”可以看到它们的“顶层”上定义所有多方法。与一般情况相比,这是一个限制,因为您可以使用深层嵌套在其他代码中的 if 语句(和 lambdas)。

4 - 本质上是代数数据类型的“教堂编码”

【讨论】:

    【解决方案2】:

    我从未听说过这样的对流。无论如何,我不明白它是如何工作的。当然,拥有iIsSmall 的唯一目的是稍后在其上进行分支(可能与其他值结合使用)?

    已经听说过一个论点,即避免使用像iIsSmall 这样的变量根本iIsSmall 只是存储您所做的测试结果,以便您以后可以使用该结果做出决定。那么为什么不在您需要做出决定的时候测试i 的值呢?即,而不是:

    int i = 5; 
    bool iIsSmall = isSmall(i);
    ...
    <code>
    ...
    if (iIsSmall) {
        <do something because i is small>
    } else {
        <do something different because i is not small>
    }
    

    只写:

    int i = 5
    ...
    <code>
    ...
    if (isSmall(i)) {
        <do something because i is small>
    } else {
        <do something different because i is not small>
    }
    

    这样你就可以在分支点上知道你实际分支的是什么,因为它就在那里。无论如何,这在这个例子中并不难,但如果测试很复杂,您可能无法将整个内容编码到变量名中。

    它也更安全。 iIsSmall 的名称不会有误导性,因为您更改了代码以便它正在测试其他东西,或者因为 i 在您调用 isSmall 之后实际上已被更改,因此它不再一定很小,或​​者因为有人刚刚选择了一个愚蠢的变量名等。

    显然这并不总是有效。如果isSmall 测试很昂贵并且您需要多次对其结果进行分支,那么您不希望多次执行它。您也可能不想多次复制该调用的代码,除非它是微不足道的。或者您可能希望返回标志以供不了解i 的调用者使用(尽管您可以只返回isSmall(i),而不是将其存储在变量中然后返回变量)。


    顺便说一句,在您的示例中,单独的函数不会保存任何内容。您可以像在bool 函数的return 语句中一样简单地将(i &lt; 10) 包含在对bool 变量的赋值中。即您可以轻松地编写bool isSmall = i &lt; 10; - 正是这样避免了 if 语句,而不是单独的函数。 if (test) { x = true; } else { x = false; }if (test) { return true; } else { return false; } 形式的代码总是很傻;只需使用x = testreturn test

    【讨论】:

      【解决方案3】:

      这真的是一个约定吗?是否应该仅仅因为可能会令人沮丧而杀死最少的 if 结构?

      好的,if 语句往往会失控,尤其是随着时间的推移添加了许多特殊情况。一个又一个的分支被添加,最后没有人能够理解每件事都做了什么而不花费数小时的时间和几杯咖啡进入这个成长的意大利面条代码实例。

      但是将所有内容放在单独的函数中真的是个好主意吗?代码应该是可重用的。代码应该是可读的。但是函数调用只是需要在源文件中进一步查找它。如果所有 if 都以这种方式存放,您只需一直在源文件中跳过。这是否支持可读性?

      或者考虑一个不在任何地方重用的 if 语句。只是为了惯例,它真的应该进入一个单独的函数吗?这里也涉及一些开销。在这种情况下,性能问题也可能是相关的。

      我想说的是:遵循编码约定是好的。风格很重要。但也有例外。只需尝试编写适合您项目的优质代码并牢记未来。归根结底,编码约定只是指导方针,它试图帮助我们在不强加任何东西的情况下编写好的代码。

      【讨论】:

        猜你喜欢
        • 2014-10-21
        • 2011-07-20
        • 1970-01-01
        • 2017-09-05
        • 2019-09-17
        • 2014-12-15
        • 1970-01-01
        • 1970-01-01
        • 2016-10-15
        相关资源
        最近更新 更多