15

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

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-tests,除了在唯一目的是执行该 test的函数中。

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

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

4

3 回答 3

10

这整个“如果”与“否”的事情让我想到了表达式问题1。基本上,观察到使用 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 语言没有类型安全的代数数据类型有关,而不是与“如果statemsnts”不利于封装。由于类型安全的唯一方法是使用方法调用,因此鼓励您将使用 if 语句的程序转换为使用访问者模式4或更糟的程序:将应该使用访问者模式的程序转换为使用简单方法分派的程序,因此可扩展性容易朝错误的方向发展。

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

最后,我认为您的示例并没有真正摆脱 if 语句。本质上,您正在做的是使用从整数到新数据类型的函数(有两种情况,一种用于大,一种用于小),然后在使用数据类型时仍然需要使用 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 - 本质上是代数数据类型的“教堂编码”

于 2013-04-21T07:11:36.833 回答
1

真的是约定俗成吗?是否应该仅仅因为可能会感到沮丧而杀死最少的 if 结构?

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

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

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

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

于 2013-04-19T08:31:40.100 回答
1

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

听到的是一个论点,以避免像iIsSmall at all一样的变量。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),而不是将其存储在变量中然后返回变量)。


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

于 2013-04-20T02:07:51.357 回答