dynamic_cast 与 避免 "向下转换" 继承层次

来源:互联网 发布:咋样做好一个淘宝店铺 编辑:程序博客网 时间:2024/05/18 16:55

在当今喧嚣的经济时代,关注一下我们的金融机构是个不错的主意。所以,看看下面这个有关银行帐户的协议类(Protocol class )。

class Person { ... };
class BankAccount {
public:
  BankAccount(const Person *primaryOwner,
              const Person *jointOwner);
  virtual ~BankAccount();
  virtual void makeDeposit(double amount) = 0;
  virtual void makeWithdrawal(double amount) = 0;
  virtual double balance() const = 0;
  ...
};
很多银行现在提供了多种令人眼花缭乱的帐户类型,但为简化起见,我们假设只有一种银行帐户,称为存款帐户:
class SavingsAccount: public BankAccount {
public:
  SavingsAccount(const Person *primaryOwner,
                 const Person *jointOwner);
  ~SavingsAccount();
  void creditInterest();                // 给帐户增加利息
  ...
};
银行想为它所有的帐户维持一个列表,这可能是通过标准库中的list类模板实现的。假设列表被叫做allAccounts:
list<BankAccount*> allAccounts;         // 银行中所有帐户
和所有的标准容器一样,list存储的是对象的拷贝,所以,为避免每个BankAccount存储多个拷贝,银行决定让allAccounts保存BankAccount的指针,而不是BankAccount本身。
假设现在准备写一段代码来遍历所有的帐户,为每个帐户计算利息。
for (list<BankAccount*>::iterator p = allAccounts.begin();
     p != allAccounts.end();
     ++p) {
  (*p)->creditInterest();      // 错误!
}
但是,编译器很快就会让你认识到:allAccounts包含的指针指向的是BankAccount对象,而非SavingsAccount对象,所以每次循环,p指向的是一个BankAccount。这使得对creditInterest的调用无效,因为creditInterest只是为SavingsAccount对象声明的,而不是BankAccount。
解决办法:
static_cast<SavingsAccount*>(*p)->creditInterest();
这种类型的转换 ---- 从一个基类指针到一个派生类指针 ---- 被称为 "向下转换",因为它向下转换了继承的层次结构。在刚看到的例子中,向下转换碰巧可以工作;但正如下面即将看到的,它将给今后的维护人员带来恶梦。
还是回到银行的话题上来。受到存款帐户业务大获成功的激励,银行决定再推出支票帐户业务。另外,假设支票帐户和存款帐户一样,也要负担利息:
class CheckingAccount: public BankAccount {
public:
  void creditInterest();    // 给帐户增加利息
  ...
};
void error(const string& msg);      // 出错处理函数;
不用说,allAccounts现在是一个包含存款和支票两种帐户指针的列表。于是,上面所写的计算利息的循环转瞬间有了大麻烦。
// 嗯,至少转换很安全
for (list<BankAccount*>::iterator p = allAccounts.begin();
     p != allAccounts.end();
     ++p) {
  // 尝试将*p安全转换为SavingsAccount*;
  // psa的定义信息见下文
  if (SavingsAccount *psa =
        dynamic_cast<SavingsAccount*>(*p)) {
    psa->creditInterest();
  }
  // 尝试将它安全转换为CheckingAccount
  else if (CheckingAccount *pca =
             dynamic_cast<CheckingAccount*>(*p)) {
    pca->creditInterest();
  }
  // 未知的帐户类型
  else {
    error("Unknown account type!");
  }
}
任何时候发现自己写出 "如果对象属于类型T1,做某事;但如果属于类型T2,做另外某事" 之类的代码,就要扇自己一个耳光。这不是C++的做法。是的,在C,Pascal,甚至Smalltalk中,它是很合理的做法,但在C++中不是。在C++中,要使用虚函数。
记得吗?对于一个虚函数,编译器可以根据所使用对象的类型来保证正确的函数调用。所以不要在代码中随处乱扔条件语句或开关语句;让编译器来为你效劳。
class BankAccount { ... };      // 同上
// 一个新类,表示要支付利息的帐户
class InterestBearingAccount: public BankAccount {
public:
  virtual void creditInterest() = 0;
  ...
};
class SavingsAccount: public InterestBearingAccount {
  ...                           // 同上
};
class CheckingAccount: public InterestBearingAccount {
  ...                           // 同上
};
用图形表示如下:
                         BankAccount
                                  ^
                                  |
                 InterestBearingAccount
                                 //
                                /  /
                               /    /
        CheckingAccount   SavingsAccount
 

因为存款和支票账户都要支付利息,所以很自然地想到把这一共同行为转移到一个公共的基类中。但是,如果假设不是所有的银行帐户都需要支付利息(以我的经验,这当然是个合理的假设),就不能把它转移到BankAccount类中。所以,要为BankAccount引入一个新的子类InterestBearingAccount,并使SavingsAccoun和CheckingAccount从它继承。
// 好一些,但还不完美
for (list<BankAccount*>::iterator p = allAccounts.begin();
     p != allAccounts.end();
     ++p) {
  static_cast<InterestBearingAccount*>(*p)->creditInterest();
}
为了完全消除转换,就必须对设计做一些改变。一种方法是限制帐户列表的类型。如果能得到一列InterestBearingAccount对象而不是BankAccount对象,那就太好了:
// 银行中所有要支付利息的帐户
list<InterestBearingAccount*> allIBAccounts;
// 可以通过编译且现在将来都可以工作的循环
for (list<InterestBearingAccount*>::iterator p =
        allIBAccounts.begin();
     p != allIBAccounts.end();
     ++p) {
  (*p)->creditInterest();
}
正如上面已经看到的,"向下转换" 可以通过几种方法来消除。最好的方法是将这种转换用虚函数调用来代替,同时,它可能对有些类不适用,所以要使这些类的每个虚函数成为一个空操作。第二个方法是加强类型约束,使得指针的声明类型和你所知道的真的指针类型之间没有出入。为了消除向下转换,无论费多大工夫都是值得的,因为向下转换难看、容易导致错误,而且使得代码难于理解、升级和维护。
用if-then-else风格的编程来进行向下转换比用虚函数要逊色得多,应该将这种方法保留到万不得已的情况下使用。运气好的话,你的程序世界里将永远看不到这样悲惨荒凉的景象。
原创粉丝点击