关于c ++:为什么可以使用多态来替换switch或else-if语句?

Why can polymorphism be used to replace switch or else-if statements?

我对我在文章中读到的内容有点困惑:案例与假设,如果哪个更有效率的话

很多时候建议使用多态性替换长case/if-else语句。我正努力让自己明白这真正的含义。如何替换:

1
2
3
4
5
6
7
8
9
10
11
case TASK_A:
    // do things for task A
    break;
case TASK_B:
    // do things for task B
    break;
        :
        :
case TASK_J:
    // do things for task J
    break;

有多态性吗?如果"do…"部分基本上是相同的重复,我可以理解它,但是如果部分或全部"cases"之间存在显著差异,那么这仍然适用吗?


在您链接到的示例中,switch超过了对象的类型,建议使用多态性来消除检查类型的需要。也就是说,在基类中声明一个虚拟函数,为每个具体的类重写它来做任何需要做的事情,并用对该函数的调用替换整个switch

在您的例子中,您测试的是变量的值,而不是对象的类型。但是,如果需要,可以将其转换为多态性解决方案:

1
2
3
4
5
struct Task         {virtual void do_things() = 0;};
struct TaskA : Task {virtual void do_things() {/*do things for task A*/}};
struct TaskB : Task {virtual void do_things() {/*do things for task B*/}};
//...
struct TaskJ : Task {virtual void do_things() {/*do things for task J*/}};

然后,您可以用一个指向task的(智能)指针替换要切换的变量,用task->do_things()替换该变量。这是否比switch更好,这是一个品味问题。


您创建一个父类/接口,例如task,它定义了子类重写的函数(可能是抽象的);让我们调用这个函数handle_task

然后为每种类型的任务(即上面的每个case语句)创建一个子类,并将// do things for task X放入该类的handle_task实现中。

由于多态性,这些子类中的每一个都可以作为父类的实例进行传递/处理,当您对它们调用handle_task时,将执行正确的代码。

一个快速工作的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <iostream>

class Task {
  public:
    virtual void handle_task()
    {
      std::cout <<"Parent task" << std::endl;
    }
};

class Task_A: public Task {
  public:
    void handle_task()
    {
      std::cout <<"task a" << std::endl;
    }
};

class Task_B: public Task {
  public:
    void handle_task()
    {
      std::cout <<"task b" << std::endl;
    }
};

int main( void )
{
  Task *task;
  Task_A a;
  Task_B b;

  task=&a;
  task->handle_task();
  task=&b;
  task->handle_task();

}

将打印

1
2
3
4
/tmp$ g++ test.cpp
/tmp$ ./a.out
task a
task b


主要的设计原因是多态性允许您在不接触公共代码的情况下分离和扩展代码。另一个提高效率的原因是,您不需要通过可能的代码路径进行线性搜索,而是无条件地跳到所需的操作(尽管这是一个实现细节)。

这是一个纯C版本的多态性,可能会令人不快:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Switch-based:

void do_something(int action, void * data)
{
    switch(action)
    {
        case 1: foo(data); break;
        case 2: bar(data); break;
        case 3: zip(data); break;
        default: break;
    }
}

// Polymorphic:

typedef void (*action_func)(void *);

void do_something(action_func f, void * data)
{
    f(data);
}

如您所见,第二个版本更易于阅读和维护,如果您想添加新的操作,则无需触摸。


一个重要的点是脱钩。在上面的代码中,您需要知道存在哪些情况。你必须每一次都列出来。如果将开关分支的逻辑放入虚拟方法中,则调用代码不再需要

  • 实际存在的案例以及
  • 每个案例的逻辑是什么?

相反,逻辑被放在它所属的位置——在类中。

现在考虑添加另一个案例。在您的代码中,您必须触摸程序中的每一个地方,在那里使用这样的switch语句。不仅你必须找到它们(不要忽略任何!)它们甚至可能不在您自己的代码中,因为您正在为其他人使用的某种libarry编写代码。使用虚拟方法,您只需根据需要在新类中重写一些方法,一切都将立即工作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
  BaseTask = class
  {
    virtual void Perform() = 0;
  }

  TaskOne = class(BaseTask)
  {
    void Perform() { cout <<"Formatting hard disk ..."; }
  }

  TaskTwo = class(BaseTask)
  {
    void Perform() { cout <<"Replacing files with random content ..."; }
  }

所以现在呼叫码只需要做

1
2
3
4
foreach( BaseTask task in Tasks)  // pseudo code    
{
  task.Perform();
}

现在假设您添加了另一个任务:

1
2
3
4
  TaskThree = class(BaseTask)
  {
    void Perform() { cout <<"Restoring everything form the backup..."; }
  }

你就完了。没有开关编辑,没有案例添加。这有多酷?


基本上,您有一个带有纯虚拟成员函数do_task()的基类Base。然后你继承了以东十一〔12〕、以东十一〔12〕。来自BaseDerivedJ,它们都定义了自己的do_task版本。然后你只需打电话:

1
2
3
4
5
std::shared_ptr<Base> obj = // points to a Derivedx

// ....

obj->do_task()

取一个类:Animal,作为2个子类:DogBird

您实现了feed()函数,如果您在dog或bird上调用它,则该函数是不同的。

不要这样做:

1
2
3
4
if object is dog
    object.dog.feed()
else
    object.bird.feed()

你只是这样做:

1
object.feed()