Skip to content

Commit 51a5070

Browse files
committed
add get_single variant (#2793)
# Objective The vast majority of `.single()` usage I've seen is immediately followed by a `.unwrap()`. Since it seems most people use it without handling the error, I think making it easier to just get what you want fast while also having a more verbose alternative when you want to handle the error could help. ## Solution Instead of having a lot of `.unwrap()` everywhere, this PR introduces a `try_single()` variant that behaves like the current `.single()` and make the new `.single()` panic on error.
1 parent 5ff96b8 commit 51a5070

File tree

6 files changed

+114
-80
lines changed

6 files changed

+114
-80
lines changed

crates/bevy_ecs/src/system/mod.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -574,7 +574,7 @@ mod tests {
574574
let (a, query, _) = system_state.get(&world);
575575
assert_eq!(*a, A(42), "returned resource matches initial value");
576576
assert_eq!(
577-
*query.single().unwrap(),
577+
*query.single(),
578578
B(7),
579579
"returned component matches initial value"
580580
);
@@ -601,7 +601,7 @@ mod tests {
601601
let (a, mut query) = system_state.get_mut(&mut world);
602602
assert_eq!(*a, A(42), "returned resource matches initial value");
603603
assert_eq!(
604-
*query.single_mut().unwrap(),
604+
*query.single_mut(),
605605
B(7),
606606
"returned component matches initial value"
607607
);
@@ -618,18 +618,18 @@ mod tests {
618618
let mut system_state: SystemState<Query<&A, Changed<A>>> = SystemState::new(&mut world);
619619
{
620620
let query = system_state.get(&world);
621-
assert_eq!(*query.single().unwrap(), A(1));
621+
assert_eq!(*query.single(), A(1));
622622
}
623623

624624
{
625625
let query = system_state.get(&world);
626-
assert!(query.single().is_err());
626+
assert!(query.get_single().is_err());
627627
}
628628

629629
world.entity_mut(entity).get_mut::<A>().unwrap().0 = 2;
630630
{
631631
let query = system_state.get(&world);
632-
assert_eq!(*query.single().unwrap(), A(2));
632+
assert_eq!(*query.single(), A(2));
633633
}
634634
}
635635

crates/bevy_ecs/src/system/query.rs

Lines changed: 49 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -488,6 +488,33 @@ where
488488
}
489489
}
490490

491+
/// Gets the result of a single-result query.
492+
///
493+
/// Assumes this query has only one result and panics if there are no or multiple results.
494+
/// Use [`Self::get_single`] to handle the error cases explicitly
495+
///
496+
/// # Example
497+
///
498+
/// ```
499+
/// # use bevy_ecs::prelude::{IntoSystem, Query, With};
500+
/// struct Player;
501+
/// struct Position(f32, f32);
502+
/// fn player_system(query: Query<&Position, With<Player>>) {
503+
/// let player_position = query.single();
504+
/// // do something with player_position
505+
/// }
506+
/// # let _check_that_its_a_system = player_system.system();
507+
/// ```
508+
///
509+
/// This can only be called for read-only queries, see [`Self::single_mut`] for write-queries.
510+
#[track_caller]
511+
pub fn single(&'s self) -> <Q::Fetch as Fetch<'w, 's>>::Item
512+
where
513+
Q::Fetch: ReadOnlyFetch,
514+
{
515+
self.get_single().unwrap()
516+
}
517+
491518
/// Gets the result of a single-result query.
492519
///
493520
/// If the query has exactly one result, returns the result inside `Ok`
@@ -497,27 +524,28 @@ where
497524
/// # Examples
498525
///
499526
/// ```
527+
/// # use bevy_ecs::prelude::{IntoSystem, With};
500528
/// # use bevy_ecs::system::{Query, QuerySingleError};
501-
/// # use bevy_ecs::prelude::IntoSystem;
502-
/// struct PlayerScore(i32);
503-
/// fn player_scoring_system(query: Query<&PlayerScore>) {
504-
/// match query.single() {
505-
/// Ok(PlayerScore(score)) => {
506-
/// // do something with score
529+
/// struct Player;
530+
/// struct Position(f32, f32);
531+
/// fn player_system(query: Query<&Position, With<Player>>) {
532+
/// match query.get_single() {
533+
/// Ok(position) => {
534+
/// // do something with position
507535
/// }
508536
/// Err(QuerySingleError::NoEntities(_)) => {
509-
/// // no PlayerScore
537+
/// // no position with Player
510538
/// }
511539
/// Err(QuerySingleError::MultipleEntities(_)) => {
512-
/// // multiple PlayerScore
540+
/// // multiple position with Player
513541
/// }
514542
/// }
515543
/// }
516-
/// # let _check_that_its_a_system = player_scoring_system.system();
544+
/// # let _check_that_its_a_system = player_system.system();
517545
/// ```
518546
///
519-
/// This can only be called for read-only queries, see [`Self::single_mut`] for write-queries.
520-
pub fn single(&'s self) -> Result<<Q::Fetch as Fetch<'w, 's>>::Item, QuerySingleError>
547+
/// This can only be called for read-only queries, see [`Self::get_single_mut`] for write-queries.
548+
pub fn get_single(&'s self) -> Result<<Q::Fetch as Fetch<'w, 's>>::Item, QuerySingleError>
521549
where
522550
Q::Fetch: ReadOnlyFetch,
523551
{
@@ -534,9 +562,18 @@ where
534562
}
535563
}
536564

565+
/// Gets the query result if it is only a single result, otherwise panics
566+
/// If you want to handle the error case yourself you can use the [`Self::get_single_mut`] variant.
567+
#[track_caller]
568+
pub fn single_mut(&mut self) -> <Q::Fetch as Fetch<'_, '_>>::Item {
569+
self.get_single_mut().unwrap()
570+
}
571+
537572
/// Gets the query result if it is only a single result, otherwise returns a
538573
/// [`QuerySingleError`].
539-
pub fn single_mut(&mut self) -> Result<<Q::Fetch as Fetch<'_, '_>>::Item, QuerySingleError> {
574+
pub fn get_single_mut(
575+
&mut self,
576+
) -> Result<<Q::Fetch as Fetch<'_, '_>>::Item, QuerySingleError> {
540577
let mut query = self.iter_mut();
541578
let first = query.next();
542579
let extra = query.next().is_some();

examples/2d/many_sprites.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,15 +75,15 @@ fn setup(
7575

7676
// System for rotating and translating the camera
7777
fn move_camera_system(time: Res<Time>, mut camera_query: Query<&mut Transform, With<Camera>>) {
78-
let mut camera_transform = camera_query.single_mut().unwrap();
78+
let mut camera_transform = camera_query.single_mut();
7979
camera_transform.rotate(Quat::from_rotation_z(time.delta_seconds() * 0.5));
8080
*camera_transform = *camera_transform
8181
* Transform::from_translation(Vec3::X * CAMERA_SPEED * time.delta_seconds());
8282
}
8383

8484
// System for printing the number of sprites on every tick of the timer
8585
fn tick_system(time: Res<Time>, sprites_query: Query<&Sprite>, mut timer_query: Query<&mut Timer>) {
86-
let mut timer = timer_query.single_mut().unwrap();
86+
let mut timer = timer_query.single_mut();
8787
timer.tick(time.delta());
8888

8989
if timer.just_finished() {

examples/game/alien_cake_addict.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -352,7 +352,7 @@ fn rotate_bonus(game: Res<Game>, time: Res<Time>, mut transforms: Query<&mut Tra
352352

353353
// update the score displayed during the game
354354
fn scoreboard_system(game: Res<Game>, mut query: Query<&mut Text>) {
355-
let mut text = query.single_mut().unwrap();
355+
let mut text = query.single_mut();
356356
text.sections[0].value = format!("Sugar Rush: {}", game.score);
357357
}
358358

examples/game/breakout.rs

Lines changed: 56 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -185,32 +185,30 @@ fn paddle_movement_system(
185185
keyboard_input: Res<Input<KeyCode>>,
186186
mut query: Query<(&Paddle, &mut Transform)>,
187187
) {
188-
if let Ok((paddle, mut transform)) = query.single_mut() {
189-
let mut direction = 0.0;
190-
if keyboard_input.pressed(KeyCode::Left) {
191-
direction -= 1.0;
192-
}
193-
194-
if keyboard_input.pressed(KeyCode::Right) {
195-
direction += 1.0;
196-
}
188+
let (paddle, mut transform) = query.single_mut();
189+
let mut direction = 0.0;
190+
if keyboard_input.pressed(KeyCode::Left) {
191+
direction -= 1.0;
192+
}
197193

198-
let translation = &mut transform.translation;
199-
// move the paddle horizontally
200-
translation.x += direction * paddle.speed * TIME_STEP;
201-
// bound the paddle within the walls
202-
translation.x = translation.x.min(380.0).max(-380.0);
194+
if keyboard_input.pressed(KeyCode::Right) {
195+
direction += 1.0;
203196
}
197+
198+
let translation = &mut transform.translation;
199+
// move the paddle horizontally
200+
translation.x += direction * paddle.speed * TIME_STEP;
201+
// bound the paddle within the walls
202+
translation.x = translation.x.min(380.0).max(-380.0);
204203
}
205204

206205
fn ball_movement_system(mut ball_query: Query<(&Ball, &mut Transform)>) {
207-
if let Ok((ball, mut transform)) = ball_query.single_mut() {
208-
transform.translation += ball.velocity * TIME_STEP;
209-
}
206+
let (ball, mut transform) = ball_query.single_mut();
207+
transform.translation += ball.velocity * TIME_STEP;
210208
}
211209

212210
fn scoreboard_system(scoreboard: Res<Scoreboard>, mut query: Query<&mut Text>) {
213-
let mut text = query.single_mut().unwrap();
211+
let mut text = query.single_mut();
214212
text.sections[1].value = format!("{}", scoreboard.score);
215213
}
216214

@@ -220,53 +218,52 @@ fn ball_collision_system(
220218
mut ball_query: Query<(&mut Ball, &Transform, &Sprite)>,
221219
collider_query: Query<(Entity, &Collider, &Transform, &Sprite)>,
222220
) {
223-
if let Ok((mut ball, ball_transform, sprite)) = ball_query.single_mut() {
224-
let ball_size = sprite.size;
225-
let velocity = &mut ball.velocity;
221+
let (mut ball, ball_transform, sprite) = ball_query.single_mut();
222+
let ball_size = sprite.size;
223+
let velocity = &mut ball.velocity;
226224

227-
// check collision with walls
228-
for (collider_entity, collider, transform, sprite) in collider_query.iter() {
229-
let collision = collide(
230-
ball_transform.translation,
231-
ball_size,
232-
transform.translation,
233-
sprite.size,
234-
);
235-
if let Some(collision) = collision {
236-
// scorable colliders should be despawned and increment the scoreboard on collision
237-
if let Collider::Scorable = *collider {
238-
scoreboard.score += 1;
239-
commands.entity(collider_entity).despawn();
240-
}
225+
// check collision with walls
226+
for (collider_entity, collider, transform, sprite) in collider_query.iter() {
227+
let collision = collide(
228+
ball_transform.translation,
229+
ball_size,
230+
transform.translation,
231+
sprite.size,
232+
);
233+
if let Some(collision) = collision {
234+
// scorable colliders should be despawned and increment the scoreboard on collision
235+
if let Collider::Scorable = *collider {
236+
scoreboard.score += 1;
237+
commands.entity(collider_entity).despawn();
238+
}
241239

242-
// reflect the ball when it collides
243-
let mut reflect_x = false;
244-
let mut reflect_y = false;
240+
// reflect the ball when it collides
241+
let mut reflect_x = false;
242+
let mut reflect_y = false;
245243

246-
// only reflect if the ball's velocity is going in the opposite direction of the
247-
// collision
248-
match collision {
249-
Collision::Left => reflect_x = velocity.x > 0.0,
250-
Collision::Right => reflect_x = velocity.x < 0.0,
251-
Collision::Top => reflect_y = velocity.y < 0.0,
252-
Collision::Bottom => reflect_y = velocity.y > 0.0,
253-
}
244+
// only reflect if the ball's velocity is going in the opposite direction of the
245+
// collision
246+
match collision {
247+
Collision::Left => reflect_x = velocity.x > 0.0,
248+
Collision::Right => reflect_x = velocity.x < 0.0,
249+
Collision::Top => reflect_y = velocity.y < 0.0,
250+
Collision::Bottom => reflect_y = velocity.y > 0.0,
251+
}
254252

255-
// reflect velocity on the x-axis if we hit something on the x-axis
256-
if reflect_x {
257-
velocity.x = -velocity.x;
258-
}
253+
// reflect velocity on the x-axis if we hit something on the x-axis
254+
if reflect_x {
255+
velocity.x = -velocity.x;
256+
}
259257

260-
// reflect velocity on the y-axis if we hit something on the y-axis
261-
if reflect_y {
262-
velocity.y = -velocity.y;
263-
}
258+
// reflect velocity on the y-axis if we hit something on the y-axis
259+
if reflect_y {
260+
velocity.y = -velocity.y;
261+
}
264262

265-
// break if this collide is on a solid, otherwise continue check whether a solid is
266-
// also in collision
267-
if let Collider::Solid = *collider {
268-
break;
269-
}
263+
// break if this collide is on a solid, otherwise continue check whether a solid is
264+
// also in collision
265+
if let Collider::Solid = *collider {
266+
break;
270267
}
271268
}
272269
}

examples/shader/animate_shader.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,6 @@ fn setup(
120120
/// `time.seconds_since_startup()` as the `value` of the `TimeComponent`. This value will be
121121
/// accessed by the fragment shader and used to animate the shader.
122122
fn animate_shader(time: Res<Time>, mut query: Query<&mut TimeUniform>) {
123-
let mut time_uniform = query.single_mut().unwrap();
123+
let mut time_uniform = query.single_mut();
124124
time_uniform.value = time.seconds_since_startup() as f32;
125125
}

0 commit comments

Comments
 (0)