关于双分派(Double Dispatch)的一点探讨

提纲

背景
我不认识你,就让你来处理
增加一个识别函数,避免无限循环
真正的增量式开发
非对称性处理
上升到设计模式层次
结论
 
背景
 
Scott Meyers More Effective C++ 的第 31 个条款中,提到了双分派( Double Dispatch )的问题,简述如下:
 
假设你想写一个游戏软件,场景发生在外层空间,涉及宇宙飞船、太空站、小行星等天体,这些天体在空中飞行时会发生碰撞,任何两种不同的天体碰撞的结果都不一样,比如:
 
如果宇宙飞船和太空站以低速碰撞,宇宙飞船会泊进太空站内。否则宇宙飞船和太空站收到的损害与其碰撞速度成正比。
 
至于其他的碰撞结果,本文不一一列出,这不是本文的重点。这里的重点是“任何两种不同的天体碰撞的结果都不一样”。
 
假设你设计的继承体系如下图所示:

 

 

class GameObject {…};
class SpaceShip: public GameObject {…};
class SpaceStation: public GameObject {…};
class Asteroid: public GameObject {…};
 
而处理碰撞的函数可能是这样的:
 
void processCollision(GameObject& object1, GameObject& object2)
{
            …
}
 
于是问题出来了。如果碰撞的结果只依赖于 object1 的动态类型,我们可以把 processCollision 作为 GameObject 的虚函数,然后调用 object1. processCollision(object2) 。如果碰撞的结果只依赖于 object2 的动态类型,则处理方法类似,只不过现在调用 object2. processCollision(object1) 。但是事实上,碰撞的结果视 object1 object2 两个参数,所以无法通过上述的单分派( Single Dispatch )来达成目标。这就是所谓的双分派( Double Dispatch )问题。
 
(如果您没有读过 Scott Meyers More Effective C++ ,建议您先读一遍,如果您曾经读过但现在已经忘得差不多了,也请您重温一遍,因为在下面的叙述中,我完全利用了原文中的例子,我不会给出所有的示例代码,只给出我做了修改的部分。)
 
接下来作者给出了很多种解决方案,但没有一种能够解决“当新型对象加入时,需要修改已有代码”的问题。其中,“使用非成员函数的碰撞处理函数”一法,虽然看上去当新型对象加入时,无需修改已有的类的申明与定义,但是仍然需要修改那些非成员的碰撞处理函数(至少必须添加新的碰撞处理函数),不能算是完全不用修改已有代码;何况把碰撞处理函数写成非成员函数,本身已经不符合封装的原则。本文尝试提供一个新的解决方案,当增加新型的对象时,完全不需要修改已有的代码。
 
首先说明一下,为什么我们要追求“当增加新型的对象时完全不修改已有的代码”这样一个目标。必须承认,如果旧有的系统是你自己设计的,或者你拥有原有系统的源码,那么增加新型的对象时,直接修改原来的代码,也许可读性更好,更易于理解。但是,实际情形中,原来的系统通常都是别人以二进制形式提供的,我们通常无法修改源代码,这时要想加进自己的东西,就不得不在不修改已有代码的基础上,无缝地加入自己的代码。
 
接下来我们来分析一下这种可能是否存在。当增加一个新的天体类型时,新的天体类型与已有的天体类型碰撞的规则不一样(这意味着碰撞处理函数不一样)。从一个方面看,这些碰撞处理函数牵涉到已有的天体类型,似乎一定要在已有的类中增加与新型天体碰撞的处理。但是,从另一个方面上看,这些新的碰撞处理,只有当新的天体类型存在时,它们才有意义,没有新的天体类型,这些碰撞处理就不会存在,换句话说,他们与新的天体类型是紧密相依的,所以没有理由一定要把这些碰撞处理写在已有的类中。所以,从感觉上看,当增加新类型的对象时,完全不需要修改已有的代码是有可能而且是应该的。
 
为了简化问题,我们假设碰撞是对称的( Scott Meyers 也做过这样的假设),也就是说 A 撞击 B 的规则与 B 撞击 A 的规则是一样的。这就为我们在处理碰撞函数时提供了一个契机:当一个对象面对不认识的对象时(这个对象可能就是将来新增加的类型),就调用这个不认识的对象的碰撞处理函数。我把它归纳成:
 
我不认识你,就让你来处理
 
我不认识你,就让你来处理,这看起来有点像推卸责任,但在这里却有着非常奇妙的作用。是的,既然碰撞是对称的,我跟你的碰撞处理就等于你跟我的碰撞处理,现在,我不认识你,我不知道我碰撞你之后,会是什么结果,于是我就让你来处理你与我的碰撞,而虚函数的特性恰恰可以保证做到这一点。
 
下面以 SpaceShip 为例给出示例代码。为了叙述方便,这里使用虚函数 + 基于运行期类型识别的 if-then-else 链,尽管我知道 Meyers 对这种方法并不赞成,但这不是本文的重点,改用其他的方法,一样可以采用本文叙述的方案。此时, GameObject 中已经申明了一个虚函数 collide 如下:
 
virtual void collide(GameObject& otherObject) = 0;
 
SpaceShip collide 函数实现如下:
 
void SpaceShip::collide(GameObject& otherObject)
{
            const type_info& objectType = typeid(otherObject);
 
            if(objectType == typeid(SpaceShip)){
                        SpaceShip& ss = static_cast<SpaceShip&>(otherObject);
 
                        //process a SpaceShip-SpaceShip collision;
            }
 
            else if(objectType == typeid(SpaceStation)){
                        SpaceStation& ss = static_cast<SpaceStation&>(otherObject);
 
                        //process a SpaceShip-SpaceStation collision;
            }
 
            else if(objectType == typeid(Asteroid)){
                        Asteroid& ss = static_cast<Asteroid&>(otherObject);
 
                        //process a SpaceShip-Asteroid collision;
            }
 
            else{
                        otherObject.collide(*this);    // 这里是关键
            }
}
 
注意上面最后一行代码。原文中这里是抛出一个异常,这里改成了调用 otherObject 的碰撞处理函数,虽然只是一行之差,结果却大不相同。
 
每一个类的写法都是这样,它只处理与它认识的天体类型的碰撞,不认识的交给对方处理(细心的读者可能已经发现,这里有一个隐患,我将在后面说明)。当增加一个新的天体类型时,新类型的写法也是这样,但它必须处理与所有已经存在的天体类型的碰撞,这是不难做到的,新增的类型总是认识已有的类型。
 
采用这种写法后,总的碰撞处理函数是非常简单的,它看起来是这样:
 
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值