从 Rust 基础到掌握 — 第 10 集
通过安全边界和测试清理 Rust 设计
该博客上撰写的文章是出于教学目的而创建的。 另外,文章中的信息可能不正确,请谨慎。
很好地使用 Rust 并不涉及所有生命周期和智能指针的复杂使用。 它是为了保持所有权边界、错误、并发操作和不安全区域较小,并使行为可测试。
确定国家的所有者
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)
}
文件和网络实现细节不包含在规则代码中。 在测试中,您可以实现内存存储以根据需要创建失败和成功。
缩小不安全的界限
fn value_at(values: &[i32], index: usize) -> Option<i32> {
values.get(index).copied()
}
如果可以用安全的 API 来解决,那就没有理由使用 unsafe。 如果由于 FFI 或低级优化而需要不安全,则应将其隐藏在一个小函数中,并且应通过安全类型和检查来保证调用者必须满足的条件。
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 的优势并不在于它绝对没有错误,而是它在执行之前通过类型和编译检查减少了许多不正确的状态。