如何避免从超类进行强制转换?

huangapple go评论65阅读模式
英文:

How to avoid casting from superclass?

问题

我正在开发一个具有不同游戏实体的游戏。它运行得很好,但我想在我的代码中摆脱一些类型转换操作。例如,当我检查子弹是否击中敌人时,我需要将这两个对象都转换为相应类型,以便根据子弹的“伤害”属性来减少“敌人”的“生命值”属性。

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

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 bulletHit = (Bullet) bullet;
            Enemy enemyHit = (Enemy) enemy;
            
            enemyHit.health -= bulletHit.damage;
            if (enemyHit.health <= 0) {
                data.remove(enemyHit);
            }
            data.remove(bulletHit);
            
            break;
        }
    }
}

是否有任何方法可以避免对子弹和敌人进行这些转换操作?我考虑过的一个解决方案是摆脱地图,只是使用许多特定实体类型的列表,但这会使我的代码膨胀。

英文:

I am developing a game with different game entities. It works quiet well, but i would like to get rid of some casting operations in my code. For example, when i am checking if a Bullet hits an Enemy, i need to cast both objects to able to reduce the health (property of Enemy) based on the damage (property of Bullet).

I store the entities in a map together with their corresponding classes. It looks like this:

Map&lt;Class&lt;? extends Entity&gt;, List&lt;Entity&gt;&gt; entities;

Here are my methods to put, receive and remove entities from the map:

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

List&lt;Entity&gt; getAll(Class&lt;? extends Entity&gt; type) {
	return entities.getOrDefault(type, Collections.emptyList());
}

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

Finally here is my code (which runs in a game loop) to check if a Bullet hits an Enemy:

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 &lt;= 0) {
					data.remove(enemyHit);
				}
				data.remove(bulletHit);
				
				break;
			}
		}
	}

Is there any way to avoid these casting operations for the Bullet and the Enemy? One solution i was thinking about, is to get rid of the the map and just use many list of those specific entity types, but this would infalte my code.

答案1

得分: 3

> „&hellip;有没有办法避免为子弹和敌人进行这些转换操作?&hellip;

TL;DR: 有。有一个方法。考虑这种简单的方法



详细的回答

这里有一种方法来做到这一点(在我看来是最简单的方法)&hellip;

...
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( ) ) ) {
                // 子弹击中敌人
                Damagable bulletHit = bullet;
                Illable enemyHit = enemy;
            
                int health = enemyHit.getHealth( );
                int damage = bulletHit.getDamage( );
                enemyHit.setHealth( health -= damage  );
                if ( health &lt;= 0 ) {
                    data.remove(enemy);
                }
                data.remove(bullet);
            
                break;
            }
        }
    }       
}
...

这种方法涉及引入两个接口:DamagableIllable(当然,您可以将它们重命名为更能准确表达意图的任何内容)。

像每个设计选择一样,都存在权衡。但这是摆脱这些转换的简单方法。

点击链接顶部的绿色开始运行实验



以下简化的重构出现在我喜欢称之为“实验驱动开发”(EDD)的迭代过程中 &hellip;

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

EDD 过程包括 RPP(“远程对编程”)。在我“驾驶”时,@DavidL 做出了富有洞察力的观察(见评论),他意识到更简单的建议解决方案的一个副作用使他的代码比他最初的问题要求的代码更简单

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

这个提议的解决方案 如此灵活,以至于 OP 发现有机会将他的原始代码减少了 2 行加上 9 个不必要的字符(上面注释掉的内容)。

> „&hellip;但是我将不得不为所有实体实现这些接口&hellip;

在这个提议的解决方案中,您将不得不实现 Entity。您目前已经在做这个。不管您选择哪种设计,您都必须实现这些内容。对于任何额外的实现,自动生成的初始方法所需的工作不会超过在现代 IDE 中单击鼠标。除非您是在您操作系统的类似于 TextEdit 的等效工具中编写游戏?

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

英文:

> „&hellip;Is there any way to avoid these casting operations for the Bullet and the Enemy?&hellip;

TL;DR: Yes. There is. Consider this simple approach.



The long answer

Here's one way to do it (the simplest way, in my opinion)&hellip;

...
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 &lt;= 0 ) {
                    data.remove(enemy);
                }
                data.remove(bullet);
            
                break;
            }
        }
    }       
}
...

This way involves the introduction of the two intefaces: Damagable and Illable (of course, you can rename those to anything that expresses your intent more precisely).

Like every design choice, there are trade-offs. But this is a simple way to get rid of those casts.

Click the green Start at the top of the page in that link to run the experiment



The following simplifying refactors emerged from the iterative process that I like to call „Experiment-driven Development“ (EDD)&hellip;

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

The EDD process includes RPP („Remote Pair Programming“). From the insightful observations that @DavidL made while I „drove“ (see comments) he realized that a side effect of the simpler proposed solution makes his code even simpler than what his original question requested.

The original question being: „How to avoid casting from superclass?“ .

This proposed solution is so flexible that the OP spotted an opportunity to reduce his original code by 2 whole lines plus 9 unnecessary characters (the commented out stuff above).

> „&hellip;But then i would have to implement those interfaces for ALL entities&hellip;

In the proposed solution, you would have to implement Entity. You currently do that anyway. You'd have to implement it regardless of which design you go with.

Automatically-generated starter methods for any additional implementations would take no more effort than a single mouse click in a modern IDE. Unless you're writing your game in your OS's equivalent of TextEdit?

In your current code, you have to implement whatever behavior Bullet requires and whatever behavior Enemy calls for. Regardless of which design you opt for, you'd still have to implement those same behaviors.

答案2

得分: 1

如果我们能够保证我们只在entities中存储非通用对象,那么我们可以使访问器类型安全(尽管在某些情况下我们必须进行转换,但我们将证明这种转换是合理的)。我们要使用的概念与ArrayList使用Object[]作为内部数据结构的方式非常相似

警告:实现通过它们的类类型对对象进行分组。接口类型将被完全忽略。

我们将把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演示

现在我们需要证明我们在getListCasted(...)中所做的未经检查的转换。如果我们看一下add(...)方法,我们会发现类类型(键)是列表中包含对象的类型。因此,我们可以保证在某个Class<T extends Entity>的键下存储了一个List<T>。因此,这种转换是合理的。

我们甚至可以放弃对Map的边界限制,使用一个Map<Class<?>, List<?>>

Ideone演示

英文:

If we can guarantee that we only store non-generic objects within entities, then we can make the accessors type-safe (although we have to cast on one occasions but we will justify this cast). The concept we are going to use is very similar to what ArrayList does, using an Object[] as internal data structure.

Warning: the implementation groups objects by their class-type. Interface-types are completely ignored.

We are going to treat entities as internal data structure, i.e. we cannot allow to leak any references to entites to the outside. With this parameter set we introduce an internal method that gets a List from the map, casted to the correct type:

@SuppressWarnings(&quot;unchecked&quot;)
private &lt;T extends Entity&gt; List&lt;T&gt; getListCasted(Class&lt;? extends T&gt; type) {
    return (List&lt;T&gt;) entities.getOrDefault(type, Collections.emptyList());
}

Notice that this list is set private, i.e. only for class-internal usage.

Now, we rewrite getAll(...) and remove(...) to use this method:

private &lt;T extends Entity&gt; List&lt;T&gt; getAll(Class&lt;? extends T&gt; type) {
    return new ArrayList&lt;&gt;(getListCasted(type));
}

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

Notice that getAll(...) now returns a (mutable) copy of the internal List.

We do not need to modify method add(...).

<kbd>Ideone Demo</kbd>

Now we need to justify the unchecked cast we made within getListCasted(...). If we take a look at method add(...), we see that the class-type (key) is the type of the containee of the list. Thus, we can guarantee that under the key of some Class&lt;T extends Entity&gt; a List&lt;T&gt; is stored. Therefore, the cast is justified.

We can even forego the bounding on the Map, using a Map&lt;Class&lt;?&gt;, List&lt;?&gt;&gt;.

<kbd>Ideone Demo</kbd>

huangapple
  • 本文由 发表于 2020年8月16日 23:40:38
  • 转载请务必保留本文链接:https://go.coder-hub.com/63438801.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定