为什么“if"语句被认为是邪恶的? [英] Why is the 'if' statement considered evil?

查看:59
本文介绍了为什么“if"语句被认为是邪恶的?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我刚刚参加了简单设计和测试会议.在其中一场会议中,我们讨论了编程语言中的邪恶关键字.提出该主题的 Corey Haines 确信 if 语句绝对是邪恶的.他的替代方法是使用谓词创建函数.你能解释一下为什么 if 是邪恶的.

I just came from Simple Design and Testing Conference. In one of the session we were talking about evil keywords in programming languages. Corey Haines, who proposed the subject, was convinced that if statement is absolute evil. His alternative was to create functions with predicates. Can you please explain to me why if is evil.

我知道你可以写出非常丑陋的代码滥用 if.但我不相信有那么糟糕.

I understand that you can write very ugly code abusing if. But I don't believe that it's that bad.

推荐答案

if 语句很少被认为是邪恶的";作为 goto 或可变全局变量——甚至后者实际上也不是普遍和绝对邪恶的.我建议将这种说法视为有点夸张.

The if statement is rarely considered as "evil" as goto or mutable global variables -- and even the latter are actually not universally and absolutely evil. I would suggest taking the claim as a bit hyperbolic.

这也很大程度上取决于您的编程语言和环境.在支持模式匹配的语言中,您将有很好的工具来替换 if 供您使用.但是,如果您正在用 C 编写低级微控制器,用函数指针替换 if 将是朝错误方向迈出的一步.所以,我将主要考虑在 OOP 编程中替换 ifs,因为在函数式语言中,if 无论如何都不是惯用的,而在纯过程语言中你没有很多其他的选项开始.

It also largely depends on your programming language and environment. In languages which support pattern matching, you will have great tools for replacing if at your disposal. But if you're programming a low-level microcontroller in C, replacing ifs with function pointers will be a step in the wrong direction. So, I will mostly consider replacing ifs in OOP programming, because in functional languages, if is not idiomatic anyway, while in purely procedural languages you don't have many other options to begin with.

尽管如此,条件子句有时会导致代码更难管理.这不仅包括 if 语句,还包括更常见的 switch 语句,它通常比相应的 if 包含更多的分支.

Nevertheless, conditional clauses sometimes result in code which is harder to manage. This does not only include the if statement, but even more commonly the switch statement, which usually includes more branches than a corresponding if would.

当您编写实用程序方法、扩展或特定库函数时,您很可能无法避免 ifs(并且您不应该这样做).没有更好的方法来编写这个小函数,也没有比它更自我记录的方法:

When you are writing utility methods, extensions or specific library functions, it's likely that you won't be able to avoid ifs (and you shouldn't). There isn't a better way to code this little function, nor make it more self-documented than it is:

// 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;
}

分支到类型代码"是代码异味

另一方面,如果您遇到测试某种类型代码的代码,或者测试变量是否属于某种类型的代码,那么这很可能是重构的好候选,即 用多态替换条件.

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

The reason for this is that by allowing your callers to branch on a certain type code, you are creating a possibility to end up with numerous checks scattered all over your code, making extensions and maintenance much more complex. Polymorphism on the other hand allows you to bring this branching decision as closer to the root of your program as possible.

考虑:

// 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);
}

通过将通用但特定于类型(即特定于类)的功能放入单独的类中并通过虚拟方法(或接口)公开它,您允许程序的内部部分将此决定委托给更高层的人调用层次结构(可能在代码中的一个地方),允许更容易的测试(模拟)、可扩展性和维护:

By placing common but type-specific (i.e. class-specific) functionality into separate classes and exposing it through a virtual method (or an interface), you allow the internal parts of your program to delegate this decision to someone higher in the call hierarchy (potentially at a single place in code), allowing much easier testing (mocking), extensibility and maintenance:

// 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 方法是否正常工作:

And you can now easily test if your RunVehicle method works as it should:

// 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 可能想提到,有时您的代码中存在类似的模式,仅在条件表达式上有所不同.条件表达式确实与 if 一起出现,但整个想法是将重复模式提取到单独的方法中,将表达式作为参数.这就是 LINQ 已经做到的,通常与替代的 foreach 相比,会产生更清晰的代码:

Patterns which only differ in their if conditions can be reused

Regarding the argument about replacing if with a "predicate" in your question, Haines probably wanted to mention that sometimes similar patterns exist over your code, which differ only in their conditional expressions. Conditional expressions do emerge in conjunction with ifs, but the whole idea is to extract a repeating pattern into a separate method, leaving the expression as a parameter. This is what LINQ already does, usually resulting in cleaner code compared to an alternative foreach:

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

Consider these two very similar methods:

// 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;
}

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

This indicates that you can extract the condition into a predicate, leaving you with a single method for these two cases (and many other future cases):

// 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 已经有一堆这样方便的扩展方法,您实际上甚至不需要编写自己的方法:

And since LINQ already has a bunch of handy extension methods like this, you actually don't even need to write your own methods:

// 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 语句已经消失"了.完全,虽然:

In this last LINQ version the if statement has "disappeared" completely, although:

  1. 老实说,问题不在于 if 本身,而在于整个代码模式(只是因为它被复制了),并且
  2. if 仍然实际存在,但它写在 LINQ Where 扩展方法中,该方法已经过测试并关闭以进行修改.减少自己的代码总是一件好事:更少的测试、更少的错误,并且代码更易于跟踪、分析和维护.
  1. to be honest the problem wasn't in the if by itself, but in the entire code pattern (simply because it was duplicated), and
  2. the if still actually exists, but it's written inside the LINQ Where extension method, which has been tested and closed for modification. Having less of your own code is always a good thing: less things to test, less things to go wrong, and the code is simpler to follow, analyze and maintain.

大量嵌套的if/else 语句

当你看到一个跨越 1000 行并且有几十个嵌套的 if 块的函数时,它很有可能被重写为

Huge runs of nested if/else statements

When you see a function spanning 1000 lines and having dozens of nested if blocks, there is an enormous chance it can be rewritten to

  1. 使用更好的数据结构并以更合适的方式组织输入数据(例如哈希表,它将在一次调用中将一个输入值映射到另一个),
  2. 使用一个公式、一个循环,或者有时只是一个现有函数,在 10 行或更少行中执行相同的逻辑(例如 这个臭名昭著的例子出现在我的脑海中,但总体思路适用于其他情况),
  3. 使用保护子句来防止嵌套(保护子句使整个函数中的变量状态更加可信,因为它们会尽快摆脱异常情况),
  4. 至少在适当的情况下替换为 switch 语句.
  1. use a better data structure and organize the input data in a more appropriate manner (e.g. a hashtable, which will map one input value to another in a single call),
  2. use a formula, a loop, or sometimes just an existing function which performs the same logic in 10 lines or less (e.g. this notorious example comes to my mind, but the general idea applies to other cases),
  3. use guard clauses to prevent nesting (guard clauses give more confidence into the state of variables throughout the function, because they get rid of exceptional cases as soon as possible),
  4. at least replace with a switch statement where appropriate.

当你觉得它是代码异味时重构,但不要过度设计

说了这么多,你不应该因为现在有几个条件而度过不眠之夜在那里.虽然这些答案可以提供一些一般的经验法则,但能够检测需要重构的构造的最佳方法是通过经验.随着时间的推移,会出现一些模式,导致一遍又一遍地修改相同的子句.

Refactor when you feel it's a code smell, but don't over-engineer

Having said all this, you should not spend sleepless nights over having a couple of conditionals now and there. While these answers can provide some general rules of thumb, the best way to be able to detect constructs which need refactoring is through experience. Over time, some patterns emerge that result in modifying the same clauses over and over again.

这篇关于为什么“if"语句被认为是邪恶的?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆