型安全性

newtypeを使って静的に値を区別する (C-NEWTYPE)

実際の型は同じでも、newtypeを用いることでその異なる解釈の間を静的に区別することができます。

例えば、f64の値はマイルとキロメートルの何れかの量を示しているかもしれません。 newtypeを使うことで、どちらが正しい解釈なのかを示すことが可能です。


# #![allow(unused_variables)]
#fn main() {
struct Miles(pub f64);
struct Kilometers(pub f64);

impl Miles {
    fn to_kilometers(self) -> Kilometers { /* ... */ }
}
impl Kilometers {
    fn to_miles(self) -> Miles { /* ... */ }
}
#}

このように型を分けることで、混ざってしまわないことを静的に保証できます。 例えば、


# #![allow(unused_variables)]
#fn main() {
fn are_we_there_yet(distance_travelled: Miles) -> bool { /* ... */ }
#}

は間違ってKilometersの値で呼ばれることはありません。 コンパイラが適切な変換を施すことを思い出させてくれるため、恐ろしいバグも回避できます。

boolOptionの代わりに意味のある型を使っている (C-CUSTOM-TYPE)

こうして下さい。


# #![allow(unused_variables)]
#fn main() {
let w = Widget::new(Small, Round)
#}

次は駄目な例です。


# #![allow(unused_variables)]
#fn main() {
let w = Widget::new(true, false)
#}

boolu8Optionのような汎用的な型には様々な解釈が考えられます。

専用の型(列挙型、構造体、あるいはタプル)を用い、その意味と不変条件を伝えるようにしてください。 上記の駄目な例では、引数の名前を調べない限りtruefalseがどういった意味なのか分かりません。 SmallRoundといった型を使った場合ではその意味が明確になっています。

専用の型を使うことで今後の拡張も楽になります。上記の例で言うと、 ここにExtraLargeというバリアントを追加したりできますね。

既存の型にゼロコストで区別された名前を付ける方法についてはnewtypeパターン(C-NEWTYPE)を参照してください。

フラグの集合を列挙型ではなくbitflagsで表している (C-BITFLAG)

Rustではenumにおいて値を明示することが可能です。


# #![allow(unused_variables)]
#fn main() {
enum Color {
    Red = 0xff0000,
    Green = 0x00ff00,
    Blue = 0x0000ff,
}
#}

値の指定は他のシステムや言語向けに整数値としてシリアライゼーションしないといけない場合に便利です。 数値でなくColorを関数が取れば、数値への変換が可能であると同時に不正な値の入力が防げるため 「型安全性」に貢献します。

enumは複数の選択肢のうちの1つを要求するAPIに使われます。 しかし、APIの入力がフラグの集合である場合もあります。 C言語のコードでは各々のフラグを特定のビットに割当てることで、1つの整数値によって32あるいは64等のフラグを表せるようにします。 Rustではbitflagsクレートがこのパターンの型安全な実装を提供しています。

#[macro_use]
extern crate bitflags;

bitflags! {
    struct Flags: u32 {
        const FLAG_A = 0b00000001;
        const FLAG_B = 0b00000010;
        const FLAG_C = 0b00000100;
    }
}

fn f(settings: Flags) {
    if settings.contains(FLAG_A) {
        println!("doing thing A");
    }
    if settings.contains(FLAG_B) {
        println!("doing thing B");
    }
    if settings.contains(FLAG_C) {
        println!("doing thing C");
    }
}

fn main() {
    f(FLAG_A | FLAG_C);
}

複雑な値の生成にビルダーパターンを使っている (C-BUILDER)

以下のような理由によって、データ構造の生成に複雑な手順が必要なことがあります。

  • 多数の入力がある
  • 複合的なデータである (例えばスライス)
  • オプショナルな設定データがある
  • 選択肢が複数ある

こんなとき、異なった引数をもつ多数のコンストラクタを追加してしまいがちです。

そういったデータ構造Tがあるとき、Tビルダー を用意することを検討してください。

  1. インクリメンタルにTを設定するための別のデータ型、TBuilderを作ります。 可能であれば、より明瞭な名前を選んでください。例えば、子プロセスを作成するビルダーの CommandUrlを作成するビルダーのParseOptionsのようにです。
  2. ビルダー自体のコンストラクタは、Tを生成するために 必須 のパラメータのみ取るべきです。
  3. ビルダーは設定を行うための便利なメソッドを提供するべきです。 例えば、複合的なデータをインクリメンタルに設定できるようなものです。 メソッドはselfを返して、メソッドチェーンを行えるようにすべきです。
  4. ビルダーは実際にTの値を生成する「終端」メソッドを1つ以上提供すべきです。

ビルダーパターンは特に、Tの生成においてプロセスやタスクの立ち上げのような副作用が生じる場合にも適しています。

Rustでのビルダーパターンの作り方には、所有権の取り扱いによって以下で示す2通りの方法があります。

非消費ビルダー (推奨)

最終的なTの生成にビルダー自身を必要としない場合があります。 例としてstd::process::Commandを挙げます。


# #![allow(unused_variables)]
#fn main() {
// NOTE: 実際のCommand APIはStringを所有しません。
// これは単純化されたバージョンです

pub struct Command {
    program: String,
    args: Vec<String>,
    cwd: Option<String>,
    // etc
}

impl Command {
    pub fn new(program: String) -> Command {
        Command {
            program: program,
            args: Vec::new(),
            cwd: None,
        }
    }

    /// Add an argument to pass to the program.
    pub fn arg(&mut self, arg: String) -> &mut Command {
        self.args.push(arg);
        self
    }

    /// Add multiple arguments to pass to the program.
    pub fn args(&mut self, args: &[String]) -> &mut Command {
        self.args.extend_from_slice(args);
        self
    }

    /// Set the working directory for the child process.
    pub fn current_dir(&mut self, dir: String) -> &mut Command {
        self.cwd = Some(dir);
        self
    }

    /// Executes the command as a child process, which is returned.
    pub fn spawn(&self) -> io::Result<Child> {
        /* ... */
    }
}
#}

ビルダーの設定データを使って実際にプロセスを立ち上げるspawnメソッドが、 ビルダーを不変参照で受け取っていることに注目してください。 これはプロセスの立ち上げにビルダーの設定データの所有権が必要ないからです。

終端メソッドであるspawnが参照しか必要としないのですから、 設定メソッドはselfを参照で受け取り、返すべきです。

利点

借用を使うことで、Commandはワンライナーでも、もっと複雑なことをする場合でも どちらにでも使うことができます。


# #![allow(unused_variables)]
#fn main() {
// One-liners
Command::new("/bin/cat").arg("file.txt").spawn();

// Complex configuration
let mut cmd = Command::new("/bin/ls");
cmd.arg(".");
if size_sorted {
    cmd.arg("-S");
}
cmd.spawn();
#}

消費ビルダー

ビルダーがTを生成する際に、所有権を移動しなければならない場合があります。 つまり終端メソッドが&selfではなくselfを取るときです。


# #![allow(unused_variables)]
#fn main() {
impl TaskBuilder {
    /// Name the task-to-be.
    pub fn named(mut self, name: String) -> TaskBuilder {
        self.name = Some(name);
        self
    }

    /// Redirect task-local stdout.
    pub fn stdout(mut self, stdout: Box<io::Write + Send>) -> TaskBuilder {
        self.stdout = Some(stdout);
        self
    }

    /// Creates and executes a new child task.
    pub fn spawn<F>(self, f: F) where F: FnOnce() + Send {
        /* ... */
    }
}
#}

ここで、stdoutを設定するときにはio::Writeを実装する型の所有権が渡されるため、 タスクの生成を行う際にその所有権を移動する必要があります(spawn内)。

ビルダーの終端メソッドが所有権を要求するとき、大きなトレードオフが存在します。

  • もし他のビルダーメソッドが可変借用を受け渡すと、 前述した複雑な設定の場合は上手く動きますが、ワンライナーでの設定は不可能になります。

  • もし他のビルダーメソッドがselfの所有権を受け渡すと、 ワンライナーはそのまま動きますが、複雑な設定を行う際には不便です。

簡単なものは簡単なまま保つべきですから、 消費ビルダーのビルダーメソッドは全てselfを取り、返すべきです。 このビルダーを使用するコードは次のようになります。


# #![allow(unused_variables)]
#fn main() {
// One-liners
TaskBuilder::new("my_task").spawn(|| { /* ... */ });

// Complex configuration
let mut task = TaskBuilder::new();
task = task.named("my_task_2"); // must re-assign to retain ownership
if reroute {
    task = task.stdout(mywriter);
}
task.spawn(|| { /* ... */ });
#}

所有権がspawnに消費されるまで各メソッド間で次々と受け渡され、ワンライナーは今までどおり動きます。 一方で、複雑な設定は記述量が増えています。各メソッドを呼ぶ際に再束縛が必要になります。