`Entity::PLACEHOLDER` should not be used as a null value
- Dominant language
- Rust
- Stars
- 48.2k
- Forks
- 4.8k
- Avg merge
- 3d 22h
- Merged PRs (30d)
- 161
Description
The documentation for [`Entity::PLACEHOLDER`](https://docs.rs/bevy_ecs/0.19.0/bevy_ecs/entity/struct.Entity.html#associatedconstant.PLACEHOLDER) says:
```rust
/// An entity ID with a placeholder value. This may or may not correspond to an actual entity,
/// and should be overwritten by a new value before being used.
```
This implies that `Entity::PLACEHOLDER` should not be used as a null value - i.e. nothing should be checking `if entity == Entity::PLACEHOLDER`, or relying on `world.get(Entity::PLACEHOLDER)` to return not found. So the only purpose of `Entity::PLACEHOLDER` is to temporarily work around `Entity` not supporting `Default` (e.g. initializing a `[Entity; N]`).
But there are a number of places in the engine that use `Entity::PLACEHOLDER` as a null value (e.g. `ComputedUiTargetCamera::get`). There's also some code that can pass it to `World::get` (e.g. `propagate_ui_target_cameras`), or doesn't have guard rails to prevent other code doing that by accident. These cases should try to avoid `Entity::PLACEHOLDER` by using `Option` or more enum variants.
## Is This Really A Bug?
Reference types that require supporting a null value are generally seen as a bad thing - they make it harder to understand the intent and scope of code (see [The Billion Dollar Mistake](https://www.infoq.com/presentations/Null-References-The-Billion-Dollar-Mistake-Tony-Hoare/)). And there have been previous decisions to avoid `Entity::PLACEHOLDER` (#16029).
But there are some counter-arguments. An `Entity` on its own still has ambiguous intent - it's not like a `Box` that's guaranteed to point to something valid. The entity might be despawned, or might not have an expected component. So there's an argument that `Option` just adds complexity and ambiguity, and that `Entity::PLACEHOLDER` should become an official null value (#16204).
Personally, I think `Option` is useful *if used consistently*. That means an `Entity` on its own is a strong hint - although not a guarantee - that the referenced entity was valid when the variable was initialized. In contrast, an `Option` clearly says that there might never have been a referenced entity.
For example, take this well-dressed player component:
```rust
#[derive(Component)]
struct Player {
monocle: Option
top_hat: Entity,
}
```
This communicates that the player's monocle is optional, but they should at least start with a top hat.
## How Difficult Is The Fix?
Most cases that I could see are straightforward, but there's at least a few awkward ones. I didn't do a thorough investigation so I might have missed other cases.
The most fiddly case is where the renderer stores a `MainEntity`/`RenderEntity` pair, and one of the pair can in some cases be null (e.g. [`BinnedRenderPhaseBatch::representative_entity`](https://docs.rs/bevy_render/0.19.0/bevy_render/render_phase/struct.BinnedRenderPhaseBatch.html#structfield.representative_entity)). There's a lot of code to fix up and it's not always clear where null is allowed. And I suspect there could be issues where entities are stored in `EntityHashMap`, which would mean a new type is needed to to support `Option`.
Another awkward case is one-to-one relationship targets (see `impl RelationshipSourceCollection for Entity`), which rely on `Entity::PLACEHOLDER` to represent a missing source entity. `Option` was considered but rejected (https://github.com/bevyengine/bevy/pull/18087#issuecomment-2696545889).
## Performance Of `Entity::PLACEHOLDER` Versus `Option`
`Entity` and `Option` are both 8 bytes (since #18704), so there's no memory concerns in most cases. There is a difference in the throughput of entity queries - this favors `Option` in most but not all situations.
For background, queries that take an `Entity` usually start by finding the location of the entity via `Entities::get_spawned`. That starts by checking if `Entity::index()` is within the bounds of the `Entities::meta` array. `Entity::PLACEHOLDER` will always be out of bounds, so it's sort of a free null check. Switching to `Option adds a branch to check for `None`, but on the other hand this branch is simpler and earlier than the bounds check.
The table below shows cycle counts for calling `Entities::get_spawned` in a loop.
| | `PLACEHOLDER` | `Option` | Throughput delta |
|-|-|-|-|
| 0% null | 4.14 | 4.52 | x0.92 |
| 50% null (unpredictable) | 17.09 | 11.73 | x1.46 |
| 50% null (predictable) | 3.92 | 2.67 | x1.47 |
| 100% null | 3.88 | 1.00 | x3.88 |
So `Entity::PLACEHOLDER` has a small advantage if all values are non-null. But `Option` will start to win as the number of nulls increases, and is much faster when all values are null.
Note that `Entity::get_spawned` is a very simple query - the `Entity::PLACEHOLDER` advantage in the non-null case will be proportionally lower in more typical queries. For example, `World::get` is roughly 19 cycles, so I'd estimate that `Option` would be ~2% slower.
(Tested on desktop Zen 4. I'll make a PR with the benchmark.)
## Can `Entity::PLACEHOLDER` Be Removed?
The case for `Option` would be stronger if `Entity::PLACEHOLDER` was removed entirely. That would make it harder (although still possible) to create a "null" entity, so `Entity` would be slightly less ambiguous. But there's a few problems.
First, there's a legitimate need for `Entity::PLACEHOLDER` when it's difficult to allocate and initialize an `Entity` variable in one step. This doesn't happen often, but there are a couple of cases in the engine:
1. Relationship sources (like `ChildOf`) want `Default` or `FromWorld` to support reflection.
2. [`PropagateEntityTrigger::original_event_target`](https://docs.rs/bevy_ecs/0.19.0/bevy_ecs/event/struct.PropagateEntityTrigger.html#structfield.original_event_target) wants `Default`.
I don't know enough about the relationship and event systems to say if these cases are solvable without major downsides. And of course there will be other situations outside of the engine - particularly components that want `Reflect`.
Second, there are cases where converting null values to `Option` will be awkward. One-to-one relationship targets are probably the main blocker (see earlier note).
Third, there are a bunch of tests that use `Entity::PLACEHOLDER` when something has an `Entity` variable that isn't relevant to the test. These are easy to fix but will add some boilerplate.
If `Entity::PLACEHOLDER` stays then its use as a null value could be discouraged by making the name more obnoxious (e.g. `Entity::TEMPORARY_PLACEHOLDER`). Another possibility is changing `World::get` and other entity queries to `debug_assert!(entity != Entity::PLACEHOLDER)`, making it harder to use as an implicit null.
## Summary
- There's a decent case to be made for avoiding `Entity::PLACEHOLDER` as a null value, but it's not a slam-dunk.
- The performance of `Option` is usually better, but there are situations where `Entity::PLACEHOLDER` has an edge.
- Removing `Entity::PLACEHOLDER` entirely seems technically possible but unlikely.
Contributor guide
Research direction
Start by auditing the uses of Entity::PLACEHOLDER named in the issue, including ComputedUiTargetCamera::get, propagate_ui_target_cameras, BinnedRenderPhaseBatch::representative_entity, and relationship targets. Read the linked decisions around #16029, #16204, and pull request #18087 before choosing a direction. Done means null semantics are made explicit where appropriate without breaking legitimate placeholder uses or relationship behavior.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- game-dev
- Issue type
- Refactor
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Needs clarification
- Newbie friendliness
- 35/100