Prompt
デフォルト引数を扱ういい方法は?
普通の引数、RORO、そしてBuilder…
デフォルト引数の扱いだね!RustにはC++やPythonのような言語仕様としての「デフォルト引数」が存在しない。でも、その不自由さが逆に、**「明示的で型安全な設計」**を生んでいるんだ。
君が挙げた選択肢を含め、情報工学的に洗練された「Rust流のやり方」を整理して比較してみるぜ!
1. 🐣 Default トレイト + 構造体更新記法(RORO風)
JavaScriptで言うところの「RORO (Receive an Object, Return an Object)」パターンに近いのがこれだ。
Rustには Default トレイト があって、これを実装(または derive)した構造体は ..Default::default() という記法で「指定したフィールド以外をデフォルト値にする」ことができるんだ。
Rust
struct Config {
host: String,
port: u16,
timeout: u32,
}
impl Default for Config {
fn default() -> Self {
Self {
host: "127.0.0.1".to_string(),
port: 8080,
timeout: 30,
}
}
}
fn start_server(config: Config) { /* ... */ }
fn main() {
// portだけ変えて、あとはデフォルト!
start_server(Config {
port: 9000,
..Default::default()
});
}
-
メリット: 言語標準の機能だけで完結し、読みやすい。
-
落とし穴: 構造体のフィールドがすべて
pub(公開)である必要がある。カプセル化を重視したいときは少し困るね。
2. 🏗️ Builder パターン
もっとも「プロフェッショナル」で、カプセル化も守れる方法だ。
複雑な設定を持つライブラリなどでよく使われるよ。
Rust
let server = ServerBuilder::new()
.port(9000)
.build(); // timeoutなどはBuilderの内部でデフォルト値がセットされている
-
メリット: フィールドを非公開にできるし、引数のバリデーション(「ポート番号は0〜65535か?」など)を
build()時に行える。 -
デメリット: 自分で書くとコード量(ボイラープレート)が増える。
💡 豆知識: 実務では
typed-builderやderive_builderというクレートを使って、アトリビュート一行でこの機能を自動生成するのが一般的だぜ!
3. 🏺 Option<T> を引数に使う(簡易的な方法)
「特定の引数があるかないか」を表現する一番シンプルな方法だ。
Rust
fn connect(host: &str, port: Option<u16>) {
let port = port.unwrap_or(8080); // なければ8080
}
// 呼び出し側
connect("localhost", None);
connect("localhost", Some(9000));
-
メリット: 実装が楽。
-
デメリット:
NoneやSomeを書くのが地味に面倒。
⚖️ 結局どれがいいの?
君のプロジェクトの規模や目的に合わせて使い分けるのがベストだ!
| パターン | おすすめのシーン |
|---|---|
Default + 更新記法 | 構造体のデータをそのまま設定として扱う、シンプルな内部ツール。 |
| Builder パターン | 外部に公開するライブラリや、バリデーションが必要な複雑なオブジェクト。 |
Option<T> | 1〜2個だけ「あってもなくてもいい」引数がある小規模な関数。 |
Into<Option<T>> | (上級編) 呼び出し側で Some() を書く手間を省きたい時。 |
💡 見落とされがちな視点:Into の魔法
呼び出し側を楽にするテクニックとして、ジェネリクスと Into を組み合わせる方法がある。
Rust
fn set_timeout<T: Into<Option<u32>>>(timeout: T) {
let t = timeout.into().unwrap_or(30);
}
// こうすると、呼び出し側で Some(10) とも、None とも書けるし、
// 実装によっては 10 と直接書くことも可能になる!
どれも一長一短あるけれど、まずは Default トレイト を使いこなせるようになると、Rustのコードがぐっと「らしく」なるよ!