我记得有些时候(可能是几年前)我在Stackoverflow上阅读了尽可能少的if-tests编程的魅力。 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-tests,除了唯一目的是进行那个测试的函数。
现在,我的问题是:有关于此约定的文档吗?在这种风格的支持者和反对者之间是否有任何地方可以看到疯狂的争论?我试着搜索引入我的Stackoverflow帖子,但我再也找不到了。
最后,我希望这个问题不会被打倒,因为我不是要求解决问题。我只是希望能够更多地了解这种编码风格,并且可能会提高我将来会做的所有编码的质量。
答案 0 :(得分:7)
这整个“if”vs“no if”的事情让我想起了Expression Problem 1 。基本上,它使用if语句或没有if语句进行编程的观察是封装和可扩展性的问题,并且有时使用语句 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 Pattern 4 的程序或更糟:转换应该使用访问者的程序使用简单的方法调度将模式转换为程序,从而使可扩展性在错误的方向上变得容易。
第二件事是,我不是因为你可以把功能制成功能的忠实粉丝。特别是,我发现所有函数只有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语句。 (并且您应该选择要使用的实现,具体取决于您是否希望更容易添加新方法或新案例 - 比如将来介绍中等)
1 - 问题的名称来自于你有“表达式”数据类型(数字,变量,子表达式的加法/乘法等)并想要实现评估函数和其他东西的问题。
2 - 或代数数据类型的模式匹配,如果你想更加类型安全......
3 - 例如,您可能必须在“调度员”可以看到的“顶级”上定义所有多方法。与一般情况相比,这是一个限制,因为您可以将if语句(和lambdas)嵌套在其他代码中。
4 - 基本上是代数数据类型的“教会编码”
答案 1 :(得分:1)
This可能是一个很好的起点。我是从this question得到的。
答案 2 :(得分:1)
真的是会议吗?是否应该仅仅因为可能会对它感到沮丧而杀死最小的if-constructs?
好的,如果陈述过于失控,特别是如果随着时间的推移添加了许多特殊情况。添加分支后的分支,最后没有人能够理解所有事情,而不花费数小时的时间和一些杯咖啡进入意大利面条代码的这个grwon实例。
但将所有内容放在单独的功能中真的是个好主意吗?代码应该是可重用的。代码应该是可读的。但是函数调用只是需要在源文件中进一步查找它。如果以这种方式放置所有ifs,您只需在源文件中一直跳过。这是否支持可读性?
或者考虑一个不在任何地方重复使用的if语句。它是否应该真正进入一个单独的功能,只是为了惯例?这里也涉及一些开销。在这种情况下,绩效问题也可能是相关的。
我想说的是:遵循编码惯例是好的。风格很重要。但也有例外。试着编写适合您项目的优秀代码,并牢记未来。最后,编码约定只是一些指导原则,它试图帮助我们生成优秀的代码而不对我们强制执行任何操作。
答案 3 :(得分:1)
我从来没有听说过这种对流。无论如何,我不知道它是如何工作的。当然,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)
,而不是将它存储在变量中然后返回变量)。
顺便说一句,单独的函数在您的示例中不会保存任何内容。您可以将(i < 10)
包含在bool
变量的赋值中,就像bool
函数中的return语句一样容易。即你可以轻松地写bool isSmall = i < 10;
- 这就是避免if语句,而不是单独的函数。表格if (test) { x = true; } else { x = false; }
或if (test) { return true; } else { return false; }
的代码总是愚蠢的;只需使用x = test
或return test
。