【问题标题】:How to avoid casting from superclass?如何避免从超类转换?
【发布时间】:2020-12-05 21:04:53
【问题描述】:

我正在开发一款包含不同游戏实体的游戏。它运行良好,但我想在我的代码中去掉一些强制转换操作。例如,当我检查子弹是否击中敌人时,我需要将两个对象都投射到能够根据 damage(子弹的属性)减少 health(敌人的属性)。

我将实体及其对应的类一起存储在地图中。它看起来像这样:

Map<Class<? extends Entity>, List<Entity>> entities;

以下是我在地图中放置、接收和移除实体的方法:

void add(Entity entity) {
    Class<? extends Entity> type = entity.getClass();
            
    if (!entities.containsKey(type)) {
        entities.put(type, new CopyOnWriteArrayList<>());
    }
    
    entities.get(type).add(entity);
}

List<Entity> getAll(Class<? extends Entity> type) {
    return entities.getOrDefault(type, Collections.emptyList());
}

void remove(Entity entity) {
    getAll(entity.getClass()).remove(entity);
}

最后是我的代码(在游戏循环中运行)来检查子弹是否击中敌人:

for (Entity bullet : data.getAll(Bullet.class)) {
        for (Entity enemy : data.getAll(Enemy.class)) {
            if (bullet.box.overlaps(enemy.box)) {
                // Bullet hits Enemy
                Bullet bulletHit = (Bullet) bullet;
                Enemy enemyHit = (Enemy) enemy;
                
                enemyHit.health -= bulletHit.damage;
                if (enemyHit.health <= 0) {
                    data.remove(enemyHit);
                }
                data.remove(bulletHit);
                
                break;
            }
        }
    }

有没有办法避免子弹和敌人的这些施法操作?我正在考虑的一种解决方案是摆脱地图并仅使用这些特定实体类型的许多列表,但这会破坏我的代码。

【问题讨论】:

  • 您打算只存储非泛型Objects 的Lists 吗?
  • 任何适合我的问题。如果泛型是答案,我会使用它。在您输入后,我正在浏览 Stack Overflow 并找到了这个 (stackoverflow.com/questions/7354740/…)。你怎么看?
  • 如果您想give the lastest experiment a run,我已经编辑了您在我的回答的 cmets 中提到的第 45 和 46 行。那么这是否解决了您打算解决的原始问题:“有什么方法可以避免子弹和敌人的这些施法操作?”或者你是死心塌地必须用泛型完成的实现? 99% 的情况下,最简单的设计通常是最“正确”的。

标签: java generics inheritance design-patterns casting


【解决方案1】:

...有什么办法可以避免子弹和敌人的这些施法操作吗?...

TL;DR是的。有。 Consider this simple approach.



长答案

Here's one way 这样做(在我看来是最简单的方法)......

...
static void play(Gamer data){ 
     for ( Entity bullet : data.getAll(Bullet.class)) {
        for (Entity enemy : data.getAll(Enemy.class)) {
            if (bullet.getBox( ).overlaps( enemy.getBox( ) ) ) {
                // Bullet hits Enemy
                Damagable bulletHit = bullet;
                Illable enemyHit = enemy;
            
                int health = enemyHit.getHealth( );
                int damage = bulletHit.getDamage( );
                enemyHit.setHealth( health -= damage  );
                if ( health <= 0 ) {
                    data.remove(enemy);
                }
                data.remove(bullet);
            
                break;
            }
        }
    }       
}
...

这种方式涉及到两个接口的引入:DamagableIllable当然,你可以将它们重命名为anything 更准确地表达您的意图)。

就像每个设计选择一样,也需要权衡取舍。但这是摆脱这些演员表的简单方法。

单击该链接中页面顶部的绿色开始以运行实验



以下简化的重构来自我喜欢称之为“实验驱动开发”(EDD)的迭代过程......

...
/*Damagable bulletHit = bullet;
Illable enemyHit = enemy;*/
            
int health = enemy/*Hit*/.getHealth();
int damage = bullet/*Hit*/.getDamage();
enemy/*Hit*/.setHealth(health -= damage);
...

EDD 过程包括RPP(“远程配对编程”)。从 @DavidL 在我“开车”(参见 cmets)时所做的深刻观察中,他意识到更简单的建议解决方案的副作用是他的代码甚至比他最初的问题要求的更简单

最初的问题是:“如何避免从超类强制转换?”。

This proposed solution 非常灵活,以至于 OP 发现了一个机会,可以将他的原始代码减少 2 整行加上 9 个不必要的字符(上面注释掉的东西)。

...但是我必须为所有实体实现这些接口...

在建议的解决方案中,您必须实现 Entity。无论如何,您目前都这样做。无论您采用哪种设计,都必须实施它。

为任何其他实现自动生成的启动方法只需要在现代 IDE 中单击鼠标即可。除非你在你的操作系统中相当于 TextEdit 来编写你的游戏?

在您当前的代码中,您必须实现 Bullet 要求的任何行为以及 Enemy 要求的任何行为。无论您选择哪种设计,您仍然必须实现这些相同的行为。

【讨论】:

  • 但是我必须为所有实体实现这些接口。子弹不应是可损坏的实体。
  • 我刚刚通过 browxy 看到了您的代码。感谢您花时间写下所有这些。我不知道你可以在子类中再次实现一个接口,即使它已经被超类实现了。
  • ...子弹不应该是可损坏的实体...“-原始代码中的此表达式另有说明:„enemyHit.health -= bulletHit.damage ”。 .damage 属性表示 Bullet状态。从语义上讲,它说:“A Bullet 可以处于 damaged 状态”。用 OO 术语来说,这意味着 BulletDamagable。这有意义吗?
  • Bullet 的 damage 属性是一个整数。它定义了子弹有多“强大”,或者damage 它可以对敌人等其他事物造成多大影响。在这种情况下,也许我的代码不那么可读。
  • ...也许我的代码在这种情况下不那么可读...“ -​​ 不一定是这种情况。 就是这么读的。下一个人可能会完全按照您的想法看待它。您不需要这样做,因此您没有深入了解问题中属性的语义细节。但就是这样。在没有这些语义细节的情况下,您发布的代码本身是可以解释的。此外,你比我更了解你的游戏。因此,我提供的解决方案可能会让您更好地了解如何进行自己的设计。
【解决方案2】:

如果我们可以保证我们只在entities 中存储非泛型对象,那么我们可以使访问器类型安全(虽然我们必须在某些情况下进行转换,但我们会证明这种转换是合理的)。我们将要使用的概念与what ArrayList does, using an Object[] as internal data structure 非常相似。

警告:实现按对象的类类型对对象进行分组。接口类型被完全忽略。

我们将entities 视为内部数据结构,即我们不能允许将任何对entites 的引用泄漏到外部。通过这个参数集,我们引入了一个内部方法,该方法从映射中获取 List,并转换为正确的类型:

@SuppressWarnings("unchecked")
private <T extends Entity> List<T> getListCasted(Class<? extends T> type) {
    return (List<T>) entities.getOrDefault(type, Collections.emptyList());
}

请注意,此列表设置为私有,即仅供类内部使用。

现在,我们重写getAll(...)remove(...) 来使用这个方法:

private <T extends Entity> List<T> getAll(Class<? extends T> type) {
    return new ArrayList<>(getListCasted(type));
}

public void remove(Entity entity) {
    getListCasted(entity.getClass()).remove(entity);
}

请注意,getAll(...) 现在返回内部 List 的(可变)副本。

我们不需要修改方法add(...)

Ideone Demo

现在我们需要证明我们在getListCasted(...) 中所做的未经检查的演员表是合理的。如果我们看一下方法add(...),我们会看到class-type (key) 是列表的containee 的类型。因此,我们可以保证在某些Class&lt;T extends Entity&gt; 的key 下存储了一个List&lt;T&gt;。因此,演员阵容是合理的。

我们甚至可以使用Map&lt;Class&lt;?&gt;, List&lt;?&gt;&gt; 放弃Map 上的边界。

Ideone Demo

【讨论】:

  • 这看起来很棒。它并没有消除铸造问题,但它很好地外包了。
  • 由于type erasure的性质,无法避免强制转换。
  • ...它并没有消除选角问题,但它很好地外包了...“所以我认为这意味着尽管您要求:„有什么方法可以避免子弹和敌人的这些施法操作?“,没有施法不是你真正想要的吗?那么,您真正最优先考虑的是解决方案必须涉及泛型吗?
  • 对我来说最好的解决方案是消除所有铸造操作而不会使代码膨胀。但在这种情况下,这可能是不可能的。我不知道如何用泛型解决它。
  • ...对我来说最好的解决方案是消除所有铸造操作而不会使代码膨胀...“-My answer demonstrates that is possible。 — “...我不知道如何用泛型解决它...” — 没有人像我一样喜欢泛型。但有时泛型是矫枉过正的。每个人都经历过他们想用泛型解决所有问题的阶段。但我们最终都会学习。你也会:对于很多问题,最好的解决方案是抽象接口形式的普通、简单、良好的老式子类型多态
猜你喜欢
  • 2011-11-24
  • 2020-01-18
  • 1970-01-01
  • 2019-10-04
  • 2014-11-26
  • 2020-12-29
  • 2014-07-09
  • 1970-01-01
  • 2012-05-30
相关资源
最近更新 更多