为什么“如果”陈述被认为是邪恶的?


74

我只是来自简单设计和测试会议。在一个会议中,我们讨论了编程语言中的邪恶关键字。 提出该主题的科里·海恩斯Corey Haines)坚信,这种if说法绝对是邪恶的。他的替代方法是使用谓词创建函数。你能告诉我为什么if是邪恶的。

我了解您可以编写非常丑陋的代码if。但是我不相信那有那么糟糕。


127
if陈述就像锤子一样是邪恶的。有些人可能会滥用它们,但是它们是必不可少的工具。
Dominic Rodger

26
因此,有人抛出断言而没有备份或解释原因吗?
随机

11
我非常怀疑上面的陈述。我不介意偏好,也没有建议您为什么不希望使用特定工具/功能等的建议。但是教条化的“邪恶”标志使我无法接受(我不知道他是否完全符合条件,顺便说一句) )。
布赖恩·阿格纽

7
@echo-没有理由的断言永远不会有任何错误。;)
Dominic Rodger

9
“如果”语句不会杀死程序:不良的程序员会杀死程序!
杰伊,

Answers:


93

if语句很少被视为“邪恶的”goto或可变的全局变量-甚至后者实际上也并非普遍且绝对不是邪恶的。我建议把这个说法有点夸张。

它还在很大程度上取决于您的编程语言和环境。在支持模式匹配的语言中,您将拥有强大的替换工具供if您使用。但是,如果您正在用C编程低级微控制器,则用if函数指针替换s将是朝错误方向迈出的一步。因此,我将主要考虑if在OOP编程中替换s,因为在功能语言中,if反正不是习惯用法,而在纯过程语言中,您没有很多其他选择。

但是,条件子句有时会导致难以管理的代码。这不仅包括该if语句,而且更常见的是该switch语句,该语句通常包含比相应语句更多的分支if

在某些情况下,使用 if

当您编写实用程序方法,扩展或特定的库函数时,很可能将无法避免ifs(并且您不应该)。没有更好的方法来编写此小函数,也没有使其比它更能自我记录的代码:

// this is a good "if" use-case
int Min(int a, int b)
{
    if (a < b) 
       return a;
    else
       return b;
}

// or, if you prefer the ternary operator
int Min(int a, int b)
{
    return (a < b) ? a : b;
}

分支“类型代码”是一种代码味道

另一方面,如果遇到测试某种类型代码的代码,或者测试某个变量是否为某种类型的代码,则这很可能是重构的好选择,即用多态替换条件

这样做的原因是,通过允许调用者在某个特定类型的代码上分支,您将有可能最终在代码中散布大量检查,从而使扩展和维护变得更加复杂。另一方面,多态使您可以使此分支决策尽可能接近程序的根。

考虑:

// this is called branching on a "type code",
// and screams for refactoring
void RunVehicle(Vehicle vehicle)
{
    // how the hell do I even test this?
    if (vehicle.Type == CAR)
        Drive(vehicle);
    else if (vehicle.Type == PLANE)
        Fly(vehicle);
    else
        Sail(vehicle);
}

通过将常见但特定于类型的(即特定于类的)功能放在单独的类中,并通过虚拟方法(或接口)将其公开,您可以允许程序的内部将此决定委派给调用层次结构中较高的人员(可能位于代码的单个位置),从而使测试(模拟),可扩展性和维护更加容易:

// adding a new vehicle is gonna be a piece of cake
interface IVehicle
{
    void Run();
}

// your method now doesn't care about which vehicle 
// it got as a parameter
void RunVehicle(IVehicle vehicle)
{
    vehicle.Run();
}

现在,您可以轻松测试您的RunVehicle方法是否可以正常工作:

// you can now create test (mock) implementations
// since you're passing it as an interface
var mock = new Mock<IVehicle>();

// run the client method
something.RunVehicle(mock.Object);

// check if Run() was invoked
mock.Verify(m => m.Run(), Times.Once());

if条件不同的模式可以重复使用

关于if在您的问题中用“谓词”替换的说法,Haines可能想提到有时在您的代码上存在相似的模式,只是它们的条件表达式不同。条件表达式的确与ifs结合出现,但是整个思想是将重复模式提取到单独的方法中,而将表达式作为参数。这是LINQ已经做的,与替代方法相比,通常可以使代码更简洁foreach

考虑以下两种非常相似的方法:

// average male age
public double AverageMaleAge(List<Person> people)
{
    double sum = 0.0;
    int count = 0;
    foreach (var person in people)
    {
       if (person.Gender == Gender.Male)
       {
           sum += person.Age;
           count++;
       }
    }
    return sum / count; // not checking for zero div. for simplicity
}

// average female age
public double AverageFemaleAge(List<Person> people)
{
    double sum = 0.0;
    int count = 0;
    foreach (var person in people)
    {
       if (person.Gender == Gender.Female) // <-- only the expression
       {                                   //     is different
           sum += person.Age;
           count++;
       }
    }
    return sum / count;
}

这表明您可以将条件提取到谓词中,这两种情况(以及将来的许多其他情况)都只有一个方法:

// average age for all people matched by the predicate
public double AverageAge(List<Person> people, Predicate<Person> match)
{
    double sum = 0.0;
    int count = 0;
    foreach (var person in people)
    {
       if (match(person))       // <-- the decision to match
       {                        //     is now delegated to callers
           sum += person.Age;
           count++;
       }
    }
    return sum / count;
}

var males = AverageAge(people, p => p.Gender == Gender.Male);
var females = AverageAge(people, p => p.Gender == Gender.Female);

而且由于LINQ已经有很多方便的扩展方法,所以您实际上甚至不需要编写自己的方法:

// replace everything we've written above with these two lines
var males = list.Where(p => p.Gender == Gender.Male).Average(p => p.Age);
var females = list.Where(p => p.Gender == Gender.Female).Average(p => p.Age);

在最后的LINQ版本中,该if语句已完全“消失”,尽管:

  1. 老实说,问题不在if于其本身,而在于整个代码模式(仅仅是因为它是重复的),并且
  2. if仍实际存在,但它写在里面LINQWhere扩展方法,它已经过测试和修改关闭。减少自己的代码总是一件好事:减少测试的事情,减少出错的事情,并且使代码易于遵循,分析和维护。

大量的嵌套if/else语句

当您看到一个跨越1000行并包含数十个嵌套if块的函数时,很有可能将其重写为

  1. 使用更好的数据结构并以更适当的方式组织输入数据(例如,哈希表,该哈希表将在单个调用中将一个输入值映射到另一个输入值),
  2. 使用一个公式,一个循环,或者有时仅使用一个现有的函数,该函数在10行或更少的行内执行相同的逻辑(例如,我想到这个臭名昭著的示例,但一般的想法也适用于其他情况),
  3. 使用保护子句防止嵌套(保护子句使整个函数的变量状态更有信心,因为它们会尽快消除异常情况),
  4. 至少switch在适当情况下以声明代替。

当您感觉到它是代码的味道时进行重构,但不要过度设计

说了这么多,您不应该在现在和那里有条件的情况下度过不眠之夜。虽然这些答案可以提供一些一般的经验法则,但能够通过经验检测出需要重构的最佳方法。随着时间的流逝,出现了一些模式,导致一遍又一遍地修改相同的子句。


1
模式匹配的好例子,结果只有一个小问题:sum / people.Count是不正确的,因为它将部分和除以所有人的总数。15个程序员的团队中只考虑一个女性。女性的实际年龄是45岁,但是如果除以15,则女性的平均年龄是3。这不是正确的平均年龄...
朱罗

87

还有另一种if可能是邪恶的感觉:当它出现而不是多态时。

例如

 if (animal.isFrog()) croak(animal)
 else if (animal.isDog()) bark(animal)
 else if (animal.isLion()) roar(animal)

代替

 animal.emitSound()

但基本上,对于它的工作而言,if是否是一个完全可以接受的工具。当然,它可能会被滥用和滥用,但是goto的地位远远不及它。


6
我同意这一点。我对switch语句有同样的问题。在这种情况下,我们可以制定策略模式。
Vadim

1
我有一位教授确切地说过-多态性应该用来隐藏类型ifs。他甚至警告不要使用is(obj是Foo),因为这种反映暗示了设计缺陷。
Kobi

1
+1替代是否使用更通用的继承解决方案,但我认为Corey在谈论FP时比其他任何事情都多。
李B

3
在动态语言中,这if(obj is Foo)是非常明智的,因为您没有键入的参数。在大多数情况下,您确实想使用鸭子类型,但有时使用内置类型,除了真实的东西之外什么都没有,在这种情况下,除了之外Foo,您想对其他任何东西都引发错误。if(obj is Foo)
鲍勃·阿曼

2
最初,当书本/人们说“如果”不好时,正是在这种多态性的背景下。我完全同意。后来在某些时候,有些人在不知道原始上下文或原因的情况下重复了这一点,这很可惜。
b.roth,

34

Code Complete的一个好报价:

就像维护程序的人一样,编写代码时会知道自己住的地方是一个暴力的精神病患者。
-匿名

IOW,保持简单。如果通过在特定区域中使用谓词来增强应用程序的可读性,请使用它。否则,请使用“ if”继续。


23
嗯,但是Code Complete中的引号包含if语句,因此是邪恶的。:-)
杰森·贝克

6
@JasonBaker您的评论也有“那个”声明,因此您的评论也很邪恶。让我们在这里停止递归:)
nawfal

@JordanParmer我同意你的看法。我已经看到人们ifs在完全损害KISS原则的情况下回避。
fabriciorissetto

17

我认为这取决于您的诚实行为。

如果您有一个简单的if..else陈述,为什么要使用predicate

如果可以的话,请使用aswitch代替较大的if替换项,然后再选择对大型操作使用谓词(在这种情况下有意义,否则您的代码将成为维护的噩梦),请使用它。

这个家伙似乎有点像我喜欢的书呆子。if用谓词代替所有只是疯狂的谈话。


9
我不喜欢switch语句。如果可以的话,我更喜欢使用策略模式。
Vadim

7
我和Kyle在一起,这只是疯狂的谈话。没有条件就不能编程,就像没有GOTO一样可以编程。开关,谓词,调度表等只是IF的语法糖。
高性能Mark

除此之外,条件跳转的性能要优于v表查找和方法调用
Matthew Whited 2009年

1
@高效能标记:我不同意,谓词不是IF的语法糖。它们允许对代码进行解耦,否则这些代码将放置在单个if / switch块中,并且可能散布在整个程序中。
Groo

1
@Groo:为什么不知道系统是如何工作的。它可以帮助您在遇到瓶颈之前先找到瓶颈。我发现了解整个系统的工作原理是如何帮助我构建更干净,更快的代码的。
马修·怀特

15

今年早些时候开始反假冒运动。主要前提是,许多嵌套的if语句经常可以用多态替换。

我希望看到一个使用谓词的示例。这更像是函数式编程吗?


1
甚至都没有听说过反击战。似乎有些极端,但是我同意他们所说的基本租户。
杰森·贝克

6
看起来和听起来像是另一个以营利为目的的次宗教活动,希望让编码人员购买有关他们可以通过实践更好,更全面地学习的书籍。
迈克·伯顿

1
看起来它们不是反信号,而是反开关写的。
Alex Feinman

1
我还没有听说过这个活动,但是最近我撕了一些充满了if块的非常复杂的代码,并用OOP代替了它。代码从难以理解的越野车垃圾变成了非常优雅,干净,易于理解和易于扩展的解决方案。绝对推荐。if(possible);)
Lee B

3
叹息因此,基本上,我必须从根本上使我的代码复杂化,以摆脱“ If”语句?这等于采用代码混淆来满足代码纯粹主义者。谢谢,但是不。
Mike Hofer,


10

就像圣经中关于金钱的经文一样,if陈述不是邪恶的-if陈述是邪恶的。没有if语句的程序是一个荒谬的想法,并且在必要时使用它们是必不可少的。但是一个连续有100个if-else if块的程序(不幸的是,我已经看到)绝对是邪恶的。


8

我不得不说,我最近开始将语句是否视为代码气味:尤其是当您发现自己多次重复相同的条件时。但是您需要了解一些有关代码气味的知识:它们不一定意味着代码不好。它们只是意味着代码很可能是错误的。

例如,马丁·福勒(Martin Fowler)将注释列为代码气味,但是我不会认真对待任何说“注释是邪恶的;不要使用注释”的人。

通常,尽管如此,我更倾向于使用多态性而不是if语句。这样只会减少错误的余地。我倾向于发现很多时候,使用条件也会导致很多流氓参数(因为必须将形成条件的数据传递给适当的方法)。


3
马丁•福勒(Martin Fowler)说评论是甜味而不是难闻的气味。让我引用“重构”中“难闻的气味”一章。“ ...注释不是难闻的气味:确实它们是一种甜味... [但是]它们经常被用作除臭剂。令人惊讶的是,您经常查看稠密注释的代码并注意到注释在那里,因为评论不好。”
MarkJ

1
@MarkJ终于!实际上有人正确地引用了福勒。+1。
09年

7

我会同意你的。他错了。这样的事情可能会让您走得太远,为自己的利益太聪明了。

用谓词而不是ifs创建的代码非常难以维护和测试。


4

谓词来自逻辑/声明性编程语言,例如PROLOG。对于某些类型的问题,例如约束解决方案,可以说,它们比许多分步提出的“先做然后再做”废话要好。用命令语言解决的问题很长而且很复杂,可以在PROLOG中仅用几行就可以解决。

还存在可伸缩编程的问题(由于转向多核,Web等)。一般而言,if语句和命令式编程往往是逐步的,并且不可扩展。逻辑声明和lambda演算描述了如何解决问题以及将问题分解为哪些部分。结果,执行该代码的解释器/处理器可以有效地将代码分解为多个片段,并将其分布在多个CPU /内核/线程/服务器之间。

绝对不是到处有用;我不想尝试用谓词而不是if语句编写设备驱动程序。但是,是的,我认为重点可能是合理的,如果不是一直使用,至少值得熟悉。


4

也许对于量子计算,不使用IF语句而是让计算的每一步继续进行,并且仅在终止时具有函数“折叠”以得到有用的结果将是明智的策略。


3

谓词的唯一问题(就替换if语句而言)是您仍然需要测试它们:

function void Test(Predicate<int> pr, int num) 
{
    if (pr(num))
    { /* do something */ }
    else
    { /* do something else */ }
}

您当然可以使用三级运算符(?:),但这只是if变相的声明...


1
借助惰性计算或表达式没有副作用,您还可以使用谓词返回古老的Lambda布尔值,其中true = (a,b) => afalse = (a,b) => b,但这也只是变相的“ if”。
2009年

3

可能是希望降低代码的循环复杂性,并减少函数中分支点的数量。如果一个函数易于分解为许多较小的函数,每个函数都可以进行测试,则可以降低复杂性并使代码更易于测试。


2

IMO:我怀疑他是在试图引发一场辩论,并让人们思考对“ if”的滥用。没有人会认真建议完全避免这种编程语法的基本结构吗?


一方面,我同意您的观点,另一方面,我确定他们说的是关于10-15年前的
gotos

2

有时有必要采取极端立场来表达自己的观点。我确定这个人会使用if-但是每次您使用if,都值得考虑一下是否使用其他模式可以使代码更清晰。

优先考虑多态if是其核心。而不是:

if(animaltype = bird) {
     squawk();
} else if(animaltype = dog) {
     bark();
}

... 采用:

animal.makeSound();

但这假设您有Animal类/接口-那么实际上 if告诉您的是您需要创建该接口。

因此,在现实世界中,if我们看到什么样的s导致我们找到了多态性解决方案?

if(logging) {
      log.write("Did something");
}

在您的整个代码中看到这确实很令人讨厌。取而代之的是,有两个(或更多)Logger实现?

this.logger = new NullLogger();    // logger.log() does nothing
this.logger = new StdOutLogger();  // logger.log() writes to stdout

这导致我们进入策略模式

代替:

if(user.getCreditRisk() > 50) {
     decision = thoroughCreditCheck();
} else if(user.getCreditRisk() > 20) {
     decision = mediumCreditCheck();
} else {
     decision = cursoryCreditCheck();
}

... 你可以有 ...

decision = getCreditCheckStrategy(user.getCreditRisk()).decide();

当然getCreditCheckStrategy()可能包含if-可能很合适。您已将其推入其所属的整洁地方。


1

很好,在红宝石中,除非;)

但严重的可能,如果是下一个跳转,即使大多数人认为这是邪恶在某些情况下简化/加速的东西(在某些情况下,如低的水平高度优化的代码,这是一个必须)。


1

我认为If语句是邪恶的,但是If表达式不是。在这种情况下,if表达式的含义可以类似于C#三元运算符(条件?trueExpression:falseExpression)。这不是邪恶的,因为它是一个纯函数(在数学意义上)。它评估为一个新值,但对其他没有任何影响。因此,它适用于替代模型。

命令式If陈述式是邪恶的,因为它们在不需要时会迫使您产生副作用。为了使If语句有意义,您必须根据条件表达式产生不同的“效果”。这些影响可能是诸如IO,图形渲染或数据库事务之类的东西,它们会在程序外部进行更改。或者,可能是赋值语句改变了现有变量的状态。通常最好将这些影响最小化,并将其与实际逻辑分开。但是,由于有了If语句,我们可以在代码中的任意位置自由添加这些“有条件执行的效果”。我认为这很糟糕。


这是不正确的...再加上三元运算符在极端情况下对于即使大小合适的表达式也不切实际,它们也不可读。基于副作用的项目中的if语句不是一件坏事。if(socket) socket.connect else say "Socket done goofed"例如
Daniel Gratzer 2012年

-2

如果不是邪恶的!考虑...

int sum(int a, int b) {
    return a + b;
}

无聊吗?现在添加一个如果...

int sum(int a, int b) {
    if (a == 0 && b == 0) {
        return 0;
    }
    return a + b;
}

...您的代码创建效率(以LOC衡量)提高了一倍。

代码的可读性也有了很大的提高,现在您可以眨眼看到两个参数均为零时的结果。您无法在上面的代码中这样做,对吗?

此外,您还支持了测试团队,因为他们现在可以推动其代码覆盖率测试工具的使用达到极限。

此外,现在为将来的增强功能更好地准备了代码。例如,让我们猜测,如果参数之一为零,则总和应为零(不要笑,不要怪我,愚蠢的客户要求,您知道,客户永远是对的)。因为首先是if,所以只需要稍作更改即可。

int sum(int a, int b) {
    if (a == 0 || b == 0) {
        return 0;
    }
    return a + b;
}

如果您从一开始就没有发明if,那么还需要多少代码更改。

感恩将在所有方面都属于您。

结论:如果是,那就永远不够。

妳去 至。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.