ここから保存できるタスク管理CLIを作ります。前提は第01〜16回です。Java・Python編と同じadd、list、doneを、RustのResultと所有権で実装します。
契約を先に決める
IDは正のu64、題名は前後の空白を除いた1〜200 Unicodeスカラー値、完了状態はboolです。改行とタブは認めません。未完了を完了にし、すでに完了でも成功します。存在しないIDはエラー、破損ファイルは読み込み失敗とし、空として上書きしません。同時更新は対象外です。
cargo new study-tasks-rs --edition 2024 で作り、src/task.rsを追加します。Serdeなどの依存設定とmainは次回でそろいます。
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub(crate) struct Task {
pub(crate) id: u64,
pub(crate) title: String,
pub(crate) done: bool,
}
impl Task {
pub(crate) fn new(id: u64, title: &str) -> Result<Self, &'static str> {
let task = Self { id, title: title.trim().to_owned(), done: false };
task.validate()?;
Ok(task)
}
pub(crate) fn validate(&self) -> Result<(), &'static str> {
if self.id == 0 { return Err("IDは正の整数です"); }
if self.title.trim() != self.title || self.title.is_empty()
|| self.title.chars().count() > 200
|| self.title.contains(['\t', '\n', '\r']) {
return Err("題名は前後空白・改行・タブなしの1〜200文字です");
}
Ok(())
}
pub(crate) fn complete(&mut self) {
self.done = true;
}
}
newで正規化してから検査します。JSONから直接復元した場合はnewが呼ばれないため、復元後にvalidateを呼ぶ必要があります。「コンストラクタに検査を書いたからデシリアライズも安全」という思い込みを避けます。
公開範囲と所有権
この小さなアプリではクレート内の保存・表示処理へフィールドを公開しています。外部へ配布するライブラリなら、フィールドをprivateにして取得メソッドや専用の変換型を用意する設計もあります。
completeは可変参照を受け取ります。呼び出し側は対象タスクを独占して借用し、その範囲でだけ状態を変えます。Java・Python編の不変な値を返す設計との違いを比べてみてください。どちらも、変更箇所が読み取れることが大切です。
練習と解答
練習:IDの最大値へ1を足すとき、普通の加算で十分でしょうか。
解答:checked_addを使って範囲外をResultへ変換します。ビルド設定によって挙動が違う桁あふれに任せず、IDを発行できないことを明示します。
公式資料
Serdeのコンテナ属性とメソッドが参照先です。
Rust全20回の目次 | 前の回 | 次の回