什么时候可以使用instanceof?

我正在设计一款游戏。 在游戏中,各种游戏对象根据他们需要做的事情扩展不同的接口(和一个抽象类),并传递给处理程序,这些处理程序按照定义的时间间隔处理具有特定接口的项目(它们实际上将所有工作分散开来以一种简洁的方式确保始终处理输入/video/等)。

无论如何,其中一些对象扩展了抽象类Collider并传递给CollisionHandler。 Collider类和处理程序负责碰撞中涉及的所有技术,并且只要求对象实现collidesWith(Collider c)函数,并根据它碰撞的内容进行修改。

许多不同类的对象将彼此碰撞,并且将根据它们碰撞的对象的类型及其特定属性而以非常不同的方式起作用。

完美的解决方案似乎是像这样使用instanceof:

class SomeNPC extends Collider{ collidesWith(Collider c){ if(c instanceof enemy){ Fight it or run away depending on your attributes and theirs. } else if(c instanceof food){ Eat it, but only if it's yellow. } else if(c instanceof BeamOfLight){ Try to move towards its source. } } } 

这实际上似乎是一个合法的地方。 我感觉很糟糕。 就像goto在某些特定情况下是否有意义一样。 设计是否对任何人都有根本感觉? 如果是这样,你会建议做什么来实现相同的行为。

传统的答案是,使用访客模式。 你添加一个新的界面,

 interface Visitor { void visit(Enemy e); void visit(Food f); void visit(BeanOfLight bol); } 

和方法,

 public void visit(Visitor v) { visitor.visit(this); } 

游戏中的每个对象都实现了一种visit方法,您需要的每个操作都会实现一个Visitor界面。 因此,只要操作visits对象,就会强制执行与该对象关联的操作。

您当然可以更详细,而不是依赖于方法调度机制。

更新:返回问题标题,使用instanceof始终是可以接受的。 这是你的代码,它是你的语言。 问题是,如果你的代码中有很多地方你使用instanceof ,你迟早会不可避免地错过一个,所以你的代码会在没有编译器的情况下无声地失败来帮助你。 访问者会在编码过程中让您的生活更加痛苦,因为它会迫使您在每次更改界面时实施界面。 但从好的方面来说,你不会错过这种情况。

更新2:请阅读以下讨论。 访客当然会束缚你,只要你有十几种类型,你就会感到受到约束。 此外,如果您需要根据两个或更多对象的类型调度事件,例如碰撞,没有访问者会帮助您(也没有任何instanceof ):您将需要实现自己的碰撞后果表,这将映射您的类型组合一个对象(我说Strategy ,但恐怕讨论会增长十倍),这将知道如何处理这种特殊的碰撞。

强制性的Stroustrup引用:“没有替代品:智力;经验;品味;努力工作。”

通常建议访客类。 使用Visitor,您可以实现访问方法:

 interface Visitor { void visit(Enemy e); void visit(Food f); void visit(BeanOfLight bol); } 

但这实际上相当于:

 class SomeNPC extends Collider { public void collidesWith( Enemy enemy ) public void collidesWith( Food food ) public void collidesWith( Bullet bullet ) } 

这两者都有缺点。

  1. 您必须实现所有这些,即使您的对象的响应在每种情况下都相同
  2. 如果添加要碰撞的新类型的对象,则必须编写一个方法来实现与每个对象的碰撞。
  3. 如果系统中的一个对象对27种碰撞器的反应不同,但其他一切反应方式相同,那么你仍然需要为每个类编写27个访问者方法

有时最简单的方法是:

 collidesWith(Object o) { if (o instanceof Balloon) { // bounce } else { //splat } 

它的优势在于它可以保持对象如何对与该对象相关的事物做出反应的知识。 它还意味着如果Balloon有子类RedBalloon,BlueBalloon等,我们不必考虑这一点,就像访问模式一样。

不使用instanceof的传统观点是它不是OO,你应该使用多态。 但是你可能会对这篇文章感兴趣: 当Polymorphism失败时,Steve Yegge 解释了为什么instanceof有时候是正确的答案

奇怪的是,没有人发布过“未破坏”的访客模式实现。 并且没有破坏我的意思是不依赖于访客的副作用。 要做到这一点,我们需要访问者返回一些结果(让我们称之为R ):

 interface ColliderVisitor { R visit(Enemy e); R visit(Food f); R visit(BeanOfLight bol); R visit(SomeNpc npc); } 

接下来,我们修改accept以接受新访问者:

 interface Collider {  R accept(ColliderVisitor visitor); } 

对撞机的具体实现必须调用正确的visit方法,就像这样(我假设Food implements Collider ,但这不是必需的):

 class Food implements Collider { @Override  R accept(ColliderVisitor visitor) { return visitor.visit(this); } } 

现在要实现冲突,我们可以这样做:

 class SomeNpcCollisionVisitor implements ColliderVisitor { SomeNpcCollisionVisitor(SomeNpc me) { this.me = me; } SomeNpc me; @Override Action visit(Enemy they) { return fightItOrRunAway(me.attributes(), they.attributes()); } @Override Action visit(Food f) { return f.colour()==YELLOW ? eat(f) : doNothing; } @Override Action visit(BeamOfLight l) { return moveTowards(l.source()); } @Override Action visit(SomeNpc otherNpc) { // What to do here? You did not say! The compiler will catch this thankfully. } } class CollisionVisitor implements ColliderVisitor> { // currying anyone? @Override Action visit(Enemy they) { return new EnemyCollisionVisitor(they); // what to do here? } @Override Action visit(Food f) { return new FoodCollisionVisitor(f); // what to do here? } @Override Action visit(BeamOfLight l) { return new BeamOfLightCollisionVisitor(l); // what to do here? } @Override Action visit(SomeNpc otherNpc) { return new SomeNpcCollisionVisitor(otherNpc); } } Action collide(Collider a, Collider b) { return b.accept(a.accept(new CollisionVisitor())); } 

您可以看到编译器可以帮助您找到忘记指定行为的所有位置。 这不像一些人声称的那样是一种负担,但是因为你总是可以通过使用默认实现来禁用它,所以这是有利的:

 class ColliderVisitorWithDefault implements ColliderVisitor { final R def; ColliderVisitorWithDefault(R def) { this.def = def; } R visit(Enemy e) { return def; } R visit(Food f) { return def; } R visit(BeanOfLight bol) { return def; } R visit(SomeNpc npc) { return def; } } 

您还需要某种方法来重用(Food,SomeNpc)和(SomeNpc,Food)的碰撞代码,但这超出了本问题的范围。

如果你认为这太冗长了 – 那就是因为它。 在具有模式匹配的语言中,这可以在几行中完成(Haskell示例):

 data Collider = Enemy  | Food  | BeanOfLight  | SomeNpc  collide (SomeNpc npc) (Food f) = if colour f == YELLOW then eat npc f else doNothing collide (SomeNpc npc) (Enemy e) = fightOrRunAway npc (npcAttributes npc) (enemyAttributes e) collide (SomeNpc npc) (BeamOfLight bol) = moveTowards (bolSource bol) collide _ _ = undefined -- here you can put some default behaviour 

您可以在此处使用访问者模式,其中Collider的子类将为其可能遇到的每种类型的冲突实现单独的方法。 所以你的方法可能变成:

 class SomeNPC extends Collider { public void collidesWith( Enemy enemy ) {} public void collidesWith( Food food ) {} public void collidesWith( Bullet bullet ) {} public void doCollision( Collider c ) { if( c.overlaps( this ) ) { c.collidesWith( this ); } } } 

你明白了。 模型中的奇怪之处在于Collider基类必须知道所有潜在的子类才能为该类型定义方法。 部分原因与访客模式的问题有关,但也是因为Collider被合并为访客。 我建议寻找访客和对撞机的分离,这样您就可以定义碰撞发生时的行为方式。 这对你的对手来说意味着他们可以根据内部状态改变他们对碰撞的行为方式。 假设它们与正常模式无关,隐藏或死亡。 查看客户端代码可能是:

 collider1.getCollisionVisitor().doCollision( collider2 ); collider2.getCollisionVisitor().doCollision( collider1 ); 

在我看来,上面描述的内容是instanceof的合法使用,并且如果每个类仅与上面列出的其他几个类交互,则可能比使用Visitor系统更具可读性。

问题是它有可能变成else-if页面else-if是20个类型的敌人中的每一个。 但是使用instanceof,你可以通过一些标准的多态性来避免这种情况(检查一个Enemy类并对待所有的敌人,即使它们是Orc s或Dalek s或诸如此类的东西)。

访客模式使得这样做变得更加困难。 最可行的解决方案是拥有一个所有游戏对象派生的顶级类,并为该类的所有子类定义collideWith()方法 – 但是每个类的默认实现只需调用collideWith()超类型:

 class GameObject { void collideWith(Orc orc) { collideWith((Enemy)orc); } void collideWith(Enemy enemy) { collideWith((GameObject)enemy); } ... void collideWith(GameObject object) { } } class SomeNPC extends GameObject { void collideWith(Orc orc) { // Handle special case of colliding with an orc } // No need to implement all the other handlers, // since the default behavior works fine. } 

这是一个使大多数人都感到畏缩的例子。 这是一个有用的链接,可以为您提供一些见解: http : //www.javapractices.com/topic/TopicAction.do? Id =31

相反,Collider应该有一些每个子类都会覆盖的collide()方法。

这可能需要考虑。 使用instanceof可能很有用,但另一种考虑访问者想法的方法是构建一个Factory类,它检查一个相互字段以确定访问者的类型。

您仍然必须为每个实现构建新类,但它们在定义方法的抽象类中路由

  public abstract class Qualifier{ public abstract String type(); //... } /** * QualifierFactory method. */ public static Qualifier getQualifier(Item sp, Config cf) { String type = cf.type(); if (XQualifier.type().equals(type)) return new XQualifier(sp, cf); else if (NQualifier.type().equals(type)) return new NQualifier(sp, cf); else if (Tools.isNone(type) || NoneQualifier.type().equals(type)) return new NoneQualifier(sp); else if (CSQualifier.type().equals(type)) return new CSQualifier(sp, cf);//... } 

返回的对象可以是动作。

有几种方法可以解决这个问题:

  • 对撞机类型的枚举,略显丑陋但防故障
  • 使用处理程序进行类调度:即类似Map ,你从传递的对撞机类(或枚举类型)中选择CollisionHandler并调用processCollision(source) 。 每个Colllider类都有自己的处理程序映射。
  • 基于上述内容,您还可以创建碰撞器类型和写入处理程序的组合,例如Map, CollisionHandler>用于创建新处理程序所需的每种新碰撞类型。 好的一面是这样的声明可以是外部的(dependency injection),因此新的NPC / Object可以与Collision处理程序一起添加。

无论如何,首先你确保它以你感觉最舒服的方式工作,你可以在以后重构它。