Rust 基礎からマスターまで — 第10回
安全な境界とテストでRust設計を整理する
本ブログ作成文は、講義を目指して製作されました。 また、作成された文章の情報も間違っている可能性がありますので、ご注意ください。
Rustをうまく使うことは、すべての寿命とスマートポインタを複雑に使うことではない。 所有権の境界、エラー、同時作業、unsafe 領域を小さく保ち、動作をテストできるようにすることだ。
状態の所有者を決定する
struct GameState {
players: Vec<Player>,
monsters: Vec<Monster>,
}
impl GameState {
fn attack(&mut self, player_index: usize, monster_index: usize) -> bool {
let Some(player) = self.players.get(player_index) else {
return false;
};
let Some(monster) = self.monsters.get_mut(monster_index) else {
return false;
};
monster.take_damage(player.attack);
true
}
}
GameStateは戦闘状態を所有し、メソッドはしばらく必要なフィールドのみを借ります。 グローバル変更状態を減らすと、参照寿命と並行性も簡単になります。
外部機能をトレートの後ろに置く
trait SaveStore {
fn save(&self, state: &GameState) -> Result<(), SaveError>;
fn load(&self) -> Result<Option<GameState>, SaveError>;
}
fn save_game(store: &impl SaveStore, state: &GameState) -> Result<(), SaveError> {
store.save(state)
}
ファイルとネットワークの詳細な実装はルールコードに入らない。 テストでは、メモリストアを実装して失敗と成功を望むようにすることができます。
unsafe 境界を絞り込む
fn value_at(values: &[i32], index: usize) -> Option<i32> {
values.get(index).copied()
}
安全なAPIで解決できる場合は、unsafeを使用する理由はありません。 FFIや低レベルの最適化でunsafeが必要な場合は、小さな関数内に隠し、呼び出し側が守るべき条件を安全なタイプとチェックで保証する必要があります。
unsafeはコンパイラのすべてのチェックをオフにする機能ではありません。 ポインタの逆参照など、一部の責任を開発者が直接証明するという表示だ。
契約をテストする
#[test]
fn invalid_target_does_not_change_state() {
let mut state = sample_state();
let before = state.monsters[0].health;
assert!(!state.attack(0, 99));
assert_eq!(state.monsters[0].health, before);
}
テストは、内部実装よりも失敗したときに状態が維持されるかどうかなどの外部契約を確認します。 Rustの強みは、エラーが絶対にないということではなく、多くの誤った状態をタイプとコンパイルチェックで実行する前に減らすことにある。