Rust von den Grundlagen bis zum Profi — Folge 10
Bereinigen Sie Ihr Rust-Design mit sicheren Grenzen und Tests
Die in diesem Blog verfassten Artikel wurden mit dem Ziel erstellt, zu unterrichten. Außerdem können die Informationen im Artikel falsch sein, also seien Sie bitte vorsichtig.
Eine gute Verwendung von Rust erfordert keine komplizierte Verwendung aller Lebensdauern und intelligenter Zeiger. Es geht darum, Eigentumsgrenzen, Fehler, gleichzeitige Vorgänge und unsichere Bereiche klein zu halten und das Verhalten testbar zu machen.
Bestimmung des Eigentümers eines Staates
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 besitzt den Kampfstatus und die Methode leiht sich nur die Felder aus, die sie kurzzeitig benötigt. Die Reduzierung des globalen veränderlichen Zustands vereinfacht auch die Lebensdauer und Parallelität von Referenzen.
Fügen Sie externe Funktionen nach dem Merkmal ein
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)
}
Details zur Datei- und Netzwerkimplementierung sind nicht im Regelcode enthalten. In Tests können Sie Speicherspeicher implementieren, um nach Wunsch Fehler und Erfolge zu erzeugen.
Die Grenzen des Unsicheren verengen
fn value_at(values: &[i32], index: usize) -> Option<i32> {
values.get(index).copied()
}
Es gibt keinen Grund, unsicher zu verwenden, wenn das Problem mit einer sicheren API gelöst werden kann. Wenn unsicher aufgrund von FFI oder Low-Level-Optimierung erforderlich ist, sollte es in einer kleinen Funktion ausgeblendet werden und die Bedingungen, die der Aufrufer erfüllen muss, sollten durch sichere Typen und Prüfungen garantiert werden.
unsicher ist keine Funktion, die alle Compilerprüfungen deaktiviert. Dies ist ein Hinweis darauf, dass der Entwickler einige Verantwortlichkeiten direkt nachweisen wird, z. B. die Dereferenzierung von Zeigern.
Den Vertrag testen
#[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);
}
Tests prüfen externe Verträge, z. B. ob der Status beibehalten wird, wenn sie fehlschlagen, und nicht die interne Implementierung. Die Stärke von Rust liegt nicht darin, dass es absolut fehlerfrei ist, sondern darin, dass es mit Typ- und Kompilierungsprüfungen viele fehlerhafte Zustände vor der Ausführung reduziert.