156
152

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

『Rust Design Patterns』を翻訳してみました(Idiom 編)

Last updated at Posted at 2021-02-18

はじめに

rust-unofficialというところの出しているRust Design Patternsの日本語訳が見つからなかったため、理解のために翻訳してみました(分からないところは DeepL に頼りました)。

今回は Introduction と Idioms の部分です(デザインパターン・アンチパターン編の翻訳はこちらにあります)。

  • FFI の部分はよく分からなかったためスキップしています。
  • 不慣れなため翻訳間違いなどある可能性が高いです(教えていただきたいです)。

以下から本文です。

Introduction

デザインパターン

プログラムを開発するとき、私たちは多くの問題を解決しなければなりません。プログラムは問題の解決方法と見ることができます。また、プログラムは多くの異なった問題の解決方法の集まりと見ることもできます。これらの解決方法の全てが一緒に大きな問題の解決へと働きかけるのです。

Rust におけるデザインパターン

同じ形を共有した問題はたくさん存在します。Rust はオブジェクト指向ではないため、デザインパターンは他のオブジェクト指向プログラミング言語のものとは異なります。問題の詳細は異なっていますが、同じ形を持っているため同じ基本的な方法を使って解決することができます。

  • デザインパターンはソフトウェアを書く上で一般的な問題を解決する方法です。
  • アンチパターンは同じ一般的な問題を解決する方法です。
    しかし、デザインパターンは利点を教えてくれるのに対し、アンチパターンはより多くの問題を生み出してしまいます。
  • イディオムはコーディングするときに従うガイドラインです。
    それらはコミュニティの社会的規範です。
    それに従わないこともできますが、その場合はそうする理由があるべきです。

イディオム

イディオムはコミュニティによって広く合意のとられた一般的に使われるスタイルとパターンのことです。イディオムとはガイドラインです。慣用句的なコードを書くと、他の開発者はその形に慣れ親しんでいるために理解しやすいです。

コンピュータはコンパイラによって生成された機械語を読み取ります。そのためプログラミング言語は多くの場合開発者にとって有益です。このような抽象化されたレイヤーを扱うことができるので、それを使いやすくシンプルにしていきませんか?

KISS の原則「Keep It Simple, Stupid(シンプルにしておけ、この間抜け)」を思い出してください。この言葉が主張するのは「多くの場合、システムは複雑に作られているときよりもシンプルに保たれているときに最善に振る舞う。ゆえに簡潔さはデザインにおける重要な目標であり、必要のない複雑さは避けるべきである」ということです。

引数には借用型を使用する

解説

関数の引数の型を決めるとき、参照外しの型強制を使用するとコードの柔軟性が向上します。この方法を使うと、関数はより多くの入力型を受け付けられるようになります。

これはスライスや fat ポインタに限られた話ではありません。実は、所有型の借用よりも、借用型を常に使うべきです。
&String よりも &str&Vec<T> よりも &[T]&Box<T> よりも &T といった感じです。

借用型を使用すれば、所有型がすでに間接参照のレイヤーを持っている場合にインスタンスの間接参照のレイヤーを避けることができます。例えば、String は間接参照のレイヤーを持っているため、&String は 2 つの間接参照のレイヤーを所持しています。代わりに &str を使用すれば関数が呼び出される場所で &String&str に型強制されます。

この例として、関数の引数に &String を使ったときと &str を使ったときの違いを示します。しかし考え方は &Vec<T>&[T] を使ったときの違いや &T&Box<T> を使ったときの違いにも当てはめられます。

単語に連続して母音が 3 つ含まれるかどうかを判定したいとします。この判定に文字列を所有する必要はないため、参照を受け取ります。

コードはこのような感じになります。

fn three_vowels(word: &String) -> bool {
    let mut vowel_count = 0;
    for c in word.chars() {
        match c {
            'a' | 'e' | 'i' | 'o' | 'u' => {
                vowel_count += 1;
                if vowel_count >= 3 {
                    return true
                }
            }
            _ => vowel_count = 0
        }
    }
    false
}

fn main() {
    let ferris = "Ferris".to_string();
    let curious = "Curious".to_string();
    println!("{}: {}", ferris, three_vowels(&ferris));
    println!("{}: {}", curious, three_vowels(&curious));

    // 上は動くが、下の 2 行は動かないだろう:
    // println!("Ferris: {}", three_vowels("Ferris"));
    // println!("Curious: {}", three_vowels("Curious"));
}

これは &String 型を引数として渡しているため動作します。
最後の 2 行は、&String 型が &str 型に型強制できないため動作しません。
この問題は単に引数の型を変更することで修正できます。

例えば、関数定義を次のように変更します:

fn three_vowels(word: &str) -> bool {

そうすれば、この 2 つのバージョンはコンパイルに通り同じ出力を行います。

Ferris: false
Curious: true

しかし、これで終わりではありません!この話には続きがあります。
「そんなの問題ない、どちらにせよ &'static str は入力として使わない("Ferris" を使ったときのように)」とあなたは思うかもしれません。この特殊な例を除いてもまだ、&String よりも &str を使ったときの方が柔軟性があるとわかります。

ここで、文章が与えられたとき、その単語のいずれかに 3 つの連続した母音が含まれるかどうかを判定する例を考えましょう。おそらくすでに定義した関数を文章の各単語に単に適用することになるでしょう。

この例はこのような感じになると思われます:

fn three_vowels(word: &str) -> bool {
    let mut vowel_count = 0;
    for c in word.chars() {
        match c {
            'a' | 'e' | 'i' | 'o' | 'u' => {
                vowel_count += 1;
                if vowel_count >= 3 {
                    return true
                }
            }
            _ => vowel_count = 0
        }
    }
    false
}

fn main() {
    let sentence_string =
        "Once upon a time, there was a friendly curious crab named Ferris".to_string();
    for word in sentence_string.split(' ') {
        if three_vowels(word) {
            println!("{} has three consecutive vowels!", word);
        }
    }
}

この &str 型を使って関数を定義した例を実行すると以下が得られます。

curious has three consecutive vowels!

しかし、この例は &String 型の引数で定義された関数では動かないでしょう。
なぜなら、文字列スライスは &String ではなく &str であり、&String への変換には割り当てが必要で、暗黙的に行われないからです。一方で、String から &str への変換は低コストで暗黙的に行われます。

参考

文字列を format! を使って連結する

解説

文字列は、ミュータブルの String に対して pushpush_str メソッドを用いるか、+ 演算子を使って作成することができます。しかし、format! を使う方が多くの場合便利で、特にリテラルと非リテラル文字列が混じっている場合に活躍します。

fn say_hello(name: &str) -> String {
    // result 文字列を地道に作成することもできる。
    // let mut result = "Hello ".to_owned();
    // result.push_str(name);
    // result.push('!');
    // result

    // しかし format! を使う方がよりよい。
    format!("Hello {}!", name)
}

利点

format! を使用すると簡潔に可読性高く文字列を組み合わせられます。

欠点

format! を使う方法は通常は最適な方法ではありません。
ミュータブルな文字列に対し push 操作を行うのが通常最適な方法です。
(特に文字列があらかじめ期待されるサイズにアロケートされている場合)

コンストラクタ

解説

Rust は言語機能としてコンストラクタを持っていません。
代わりに、static な new メソッドを使ってオブジェクトを作成する慣例があります。

// Rust のベクトル。liballoc/vec.rs を参照。
pub struct Vec<T> {
    buf: RawVec<T>,
    len: usize,
}

impl<T> Vec<T> {
    // 新たな空の `Vec<T>` を作成する
    // これは static メソッドであり self がないことに注意する
    // このコンストラクタは引数を受け取らないが、オブジェクトを初期化するために持たせることもできる
    pub fn new() -> Vec<T> {
        // 正しく初期化されたフィールドを持つ新たな Vec を作成する
        Vec {
            // ここでは RawVec のコンストラクタを呼んでいることに注意する
            buf: RawVec::new(),
            len: 0,
        }
    }
}

参考

オブジェクトを作成するビルダーパターンには多くの設定が存在します。

Default トレイト

解説

Rust の多くの型にはコンストラクタがあります。
しかし、これは型に固有のものです。Rust は「すべてが new() メソッドを持っている」という抽象化ができません。
これを行うため Default トレイトが考案されました。Default トレイトはコンテナや他のジェネリック型と一緒に使うことができます(例えば Option::unwrap_or_default() を参照)。とりわけ、一部のコンテナは適用可能な場合すでにそれを実装しています。

CowBoxArc など 1 つの要素を持つコンテナが Default を実装するだけでなく、
フィールドがすべて Default を実装している構造体も #[derive(Default)] をつけることで自動的に Default を実装することができます。よって多くの型が Default を実装するほど便利になっていきます。

一方で、コンストラクタは複数の引数を受け取れるのに対し、default() メソッドは受け取れません。また、異なる名前を持つ複数の構造体を定義できるのに対し、Default の実装は 1 つの型につき 1 つしか行えません。

use std::{path::PathBuf, time::Duration};

// ここで Default を自動的に derive できます
#[derive(Default, Debug)]
struct MyConfiguration {
    // Option のデフォルトは None
    output: Option<PathBuf>,
    // Vecs のデフォルトは空のベクトル
    search_path: Vec<PathBuf>,
    // Duration のデフォルトはゼロ時間
    timeout: Duration,
    // bool のデフォルトは false
    check: bool,
}

impl MyConfiguration {
    // ここにセッターを追加する
}

fn main() {
    // デフォルト値をもつ新たなインスタンスを作成
    let mut conf = MyConfiguration::default();
    // ここで conf を使って何かする
    conf.check = true;
    println!("conf = {:#?}", conf);
}

参考

コレクションはスマートポインタ

解説

Deref トレイトを使って、データの所有や借用を提供するスマートポインタのようなコレクションを扱います。

use std::ops::Deref;

struct Vec<T> {
    data: T,
    //..
}

impl<T> Deref for Vec<T> {
    type Target = [T];

    fn deref(&self) -> &[T] {
        //..
    }
}

Vec<T>T の集まりを所有しており、スライス(&[T])は T の集まりを借用しています。
VecDeref を実装することで &Vec<T> から &[T] への暗黙的な参照外しを行うことができ、自動的に型強制できるか調べてくれるようになります。Vec に実装されていて欲しいメソッドのほとんどが代わりにスライスに実装されていることでしょう。

String&str についても確認してみてください。

動機

所有と借用は Rust 言語の重要な側面です。
データ構造では、よいユーザエクスペリエンスを与えるためにこれらのセマンティクスを適切に扱わなくてはなりません。データを所持するデータ構造を実装するとき、データの参照を提供するとより柔軟な API になります。

利点

ほとんどのメソッドは参照に対してのみ実装されているはずで、暗にそれは所有した値にも実装することができます。

クライアントにデータを借用するか所有権をとるかの選択肢を与えることができるのです。

欠点

間接参照を使って利用できるメソッドやトレイトは境界チェックのとき考慮されないので、
このパターンを使ったデータ構造を持つ generic プログラミングが複雑になりやすいです
BorrowAsRef トレイトなどを参照してください)。

議論

スマートポインタとコレクションは類似しています。スマートポインタは 1 つのオブジェクトを指し示すのに対し、コレクションはたくさんのオブジェクトを指し示します。型システムの観点ではこの 2 つにはほとんど差がありません。コレクションはコレクションを介してでしか各データにアクセスできないときはデータを所有し、データの削除を担当します(所有権が共有されている場合でさえも、ある種の借用が適切と思われます)。コレクションがデータを所有するとき、通常は複数参照できるようにデータの借用を提供するのが便利です。

ほとんどのスマートポインタ(例えば Foo<T>)は Deref<Target=T> を実装します。
しかし、コレクションは通常は好みの型に参照外しをします。[T]str は言語のサポートがありますが、一般的には、これは必須ではありません。Foo<T> は、Bar が動的にサイズの決まる型で &Bar<T>Foo<T> におけるデータの借用である場合に Deref<Target=Bar<T>> を実装できます。

一般的に、順序づけられたコレクションはスライス構文を提供するために RangeIndex を実装します。ターゲットは借用です。

参考

デストラクタでのファイナライズ処理

解説

Rust は finally ブロック(関数がどのように exit しても実行されるコード)に相当するものを提供しません。代わりに、オブジェクトのデストラクタを使って exit する前に実行しなければならないコードを実行することができます。

fn bar() -> Result<(), ()> {
    // これらは関数の中で定義される必要はない
    struct Foo;

    // Foo のデストラクタを実装する
    impl Drop for Foo {
        fn drop(&mut self) {
            println!("exit");
        }
    }

    // _exit のデストラクタは関数 `bar` がどのように終了しても実行される
    let _exit = Foo;
    // 暗黙的に `?` 演算子を使って return する
    baz()?;
    // 通常の return
    Ok(())
}

動機

関数が複数の return ポイントを持っている場合、exit 時にコードを実行するのは難しく繰り返しを生みやすい(バグを発生させやすい)です。これは特に return がマクロによって暗黙的に行われる場合に起こります。一般的なケースでは結果が Err である場合に return するが Ok のときは処理を続ける ? 演算子です。
? は例外処理メカニズムとして使用されますが、finally を持つ Java と違い、正常系と異常系の両方でコードが実行されるようにスケジュールする方法はありません。パニックした場合にも関数は早期に終了します。

利点

デストラクタのコードはパニックやアーリーリターンなどに対応していて(ほぼ)常に実行されます。

欠点

デストラクタが実行されることは保証されません。
例えば、関数に無限ループがあったり終了する前に関数がクラッシュした場合です。
また、デストラクタはすでにパニックしたスレッドでパニックした場合に実行されません。
よって、デストラクタはファイナライズ処理が必須なときのファイナライザとして頼ることができません。

このパターンは気付きにくく暗黙的なコードになってしまいます。
関数を読んでも終了時にデストラクタが実行されることが明白ではありません。
このせいでデバッグがトリッキーになりえます。

ファイナライズ処理のためにオブジェクトと Drop の実装を必要とすることはボイラープレートとしては重たいです。

議論

ファイナライザとして使われるオブジェクトを実際にどのように保持するかには微妙なところがあります。オブジェクトは関数の終了まで保持しておく必要があり、それから destroy させなければいけません。オブジェクトは常に値であるかただ 1 つの所有されたポインタ(例えば Box<Foo>) でなくてはなりません。もし(Rc のような)シェアードポインタが使われるとき、ファイナライザは関数のライフタイムの上で維持されるはずです。似たような理由で、ファイナライザは move されたり return されたりしてはいけません。

ファイナライザは変数にアサインされなければなりません。
そうしなければスコープから抜けるよりも早く直ちに destroy されてしまいます。変数がファイナライザとしてのみ使われる場合には、変数名は _ から始めないといけません。さもないとコンパイラがファイナライザが一度も使われていないと警告してしまいます。しかし、その変数に suffix なしの _ という名前をつけてはいけません。その場合は直ちに destroy されてしまいます。

Rust ではデストラクタはオブジェクトがスコープから抜けた時に実行されます。
これはブロックの終わりか、アーリー return があるときか、プログラムがパニックしたときに起こります。パニックしたとき、Rust はスタックを展開し各スタックフレームの各オブジェクトのデストラクタを実行します。そのため、デストラクタは関数が呼ばれてパニックが起こったときにも呼ばれます。

デストラクタがパニックしたとき、実行するとよくないことがあるため、Rust は直ちにスレッドを中断し、デストラクタの実行を中断します。つまりデストラクタは実行されることが必ずしも保証されません。またデストラクタはパニックしないしないように注意しなくてはなりません。なぜならリソースが予期しない状態のままになってしまう可能性があるからです。

参考

mem::{take(_), replace(_)} で変更された enum で所有した値を保持する

解説

A { name: String, x: u8 }B { name: String } の(少なくとも)2 つの変数を持つ &mut MyEnum があるとします。ここで MyEnum::Ax が 0 の場合には B に変更し、そうでなければ MyEnum::B をそのままにしたいとします。

これは name をクローンせずに行うことができます。

use std::mem;

enum MyEnum {
    A { name: String, x: u8 },
    B { name: String }
}

fn a_to_b(e: &mut MyEnum) {

    // ここで `e` を可変に借用する。このようにすると、借用チェッカーが許可しないために
    // `*e = ...` のように値を直接書き換えることができない。
    // よって、`e` への代入は `if let` 節の外側にこなくてはならない。
    *e = if let MyEnum::A { ref mut name, x: 0 } = *e {

        // これは `name` を受け取り代わりに空文字列を詰めている。
        // (空文字列は領域を占有しない)
        // そして、新しい enum 変数を作成している
        // (`if let` の結果であるため、`*e` にアサインされている)
         MyEnum::B { name: mem::take(name) }

    // 他のすべてのケースではすぐに return する。そうすると代入をスキップできる。
    } else { return }
}

これはもっとの多くの変数がある場合でも動作します:

use std::mem;

enum MultiVariateEnum {
    A { name: String },
    B { name: String },
    C,
    D
}

fn swizzle(e: &mut MultiVariateEnum) {
    use MultiVariateEnum::*;
    *e = match *e {
        // 所有権ルールは値から `name` を受け取ることを許可しない。
        // しかし、可変参照から値を取り出すことはできない。置き換えない限りは:
        A { ref mut name } => B { name: mem::take(name) },
        B { ref mut name } => A { name: mem::take(name) },
        C => D,
        D => C
    }
}

動機

enum を使うとき、enum の値を、おそらくもう一つの変数に、書き換えたいはずです。
これは通常借用チェッカーを満足させるために 2 つのフェーズで行われます。
最初のフェーズでは、既存の値を観察し部分を見て次に何をするか決めます。
2 つ目のフェーズでは値を条件にしたがって(上の例のように)変更します。

借用チェッカーは name を enum の外に取り出すことを許可ません(何かがそこにあるため)。
もちろん name を .clone() してクローンしたものを MyEnum::B に入れることもできますが、
それは借用チェッカーを満足させるためにクローンするアンチパターンの例です。
どちらにしても e を可変の参照だけを使って変更するだけで余剰なアロケーションを避けることができます。

mem::take を使うと値を取り出し、デフォルト値と書き換え、以前の値を返すことができます。
String の場合デフォルト値は空文字列になり、アロケートする必要はありません。
結果として元々の name所有された値として得ることができます。
そしてもう一つの enum でこれを包むことができます。

NOTE: mem::replace はとても似ていますが、書き換える値を指定することができます。
mem::take の行は mem::replace(name, String::new()) と等価です。

しかし、Option を使っていて値を None と置き換えたい場合には、
Optiontake() メソッドでより短く慣用句的に書くことができます。

利点

見てください、アロケーションがありません!
また、このようにしているときはインディ・ジョーンズのような感じがします。

欠点

少しくどくなってしまいます。繰り返し間違えると借用チェッカーが嫌いになります。
コンパイラは二重のストアの最適化に失敗し、結果として unsafe な言語でするのとは対照的にパフォーマンスが低下してしまいます。

さらに、受け取る型は Default トレイトを実装している必要があります。
しかし、もし使用している型が Default を実装していない場合は、代わりに mem::replace を使用できます。

議論

このパターンの関心は Rust のみです。GC がある言語では、デフォルトで値への参照を受け取り(そして GC が参照を追い続け)、その他 C のような低レベルの言語では単にポインタのエイリアスを作成して、後で物事を解決します。

しかし、Rust ではもう少しやらなければならないことがあります。所有された値は所有者が一人のみ必要なので、それを取り出すには代わりに何かを渡す必要があります。インディ・ジョーンズのようにアーティファクトの代わりに砂袋に置き換えます。

参考

これにより、特定のケースでの借用チェッカーを満足させるためにクローンするアンチパターンが解消されます。

スタック上で動的ディスパッチ

解説

複数の値に対して動的ディスパッチを行うことができますが、そうするためには、異なる型のオブジェクトをバインドするために複数の変数を定義する必要があります。ライフタイムを必要に応じて伸ばすために、以下のように遅延された条件つきの初期化を使用できます。

use std::io;
use std::fs;

# fn main() -> Result<(), Box<dyn std::error::Error>> {
# let arg = "-";

// `readable` よりも長く存在しないといけないため、最初に宣言する :
let (mut stdin_read, mut file_read);

// 動的ディスパッチを得るために型を割り当てる必要がある。
let readable: &mut dyn io::Read = if arg == "-" {
    stdin_read = io::stdin();
    &mut stdin_read
} else {
    file_read = fs::File::open(arg)?;
    &mut file_read
};

// ここで `readable` から読み込む

# Ok(())
# }

動機

Rust はデフォルトでコードを単相にします。
これはコードのコピーが使用される型ごとに生成され個々に最適化されることを意味しています。これはホットパスにおいてコードをとても速くしてくれるのに対し、パフォーマンスが求められない場所でコードを肥大化させてしまい、多くのコンパイルの時間やキャッシュを使用してしまいます。

幸いにも、Rust では動的ディスパッチを使うことができますが、明示的にそれを使うことを示す必要があります。

利点

ヒープ領域に何もアロケートする必要がありません。後から使わないものを初期化する必要もなければ、FileStdin の両方で動作するようにコード全体を単相にする必要がありません。

欠点

Box ベースのものよりも記述量が増えます。

// 動的ディスパッチのために依然として型を記述する必要がある。
let readable: Box<dyn io::Read> = if arg == "-" {
    Box::new(io::stdin())
} else {
    Box::new(fs::File::open(arg)?)
};
// ここで `readable` から読み込む。

議論

Rust 初心者は通常 Rust はすべての変数が使用される前に初期化される必要があることを学びます。そのため使用されない変数が初期化されていないという事実を見落としやすいです。
Rust は懸命に動作して、これがうまくいき、初期化された値のみがスコープの終わりで drop されることを保証します。

この例は Rust が課しているあらゆる制約を満たしています。

  • すべての変数は使われる前に(このケースでは借用される前に)初期化される必要があります
  • 各変数は 1 つの型の値を持つことのみが必要です。この例では stdinStdin 型であり、fileFile 型であり、readable&mut dyn Read 型です。
  • それぞれの借用された値はそれを借用している参照のすべてよりも生存期間が長いです。

参考

  • デストラクタでのファイナライズ処理RAII ガードを使うとライフタイムを厳しく制限することで利益が得られます。
  • 条件つきで満たされた Option<&T> の(ミュータブルな)参照に対しては、Option<T> を直接初期化して .as_ref() メソッドを使うことでオプションの参照を取得できます。

Option 上でイテレートする

解説

Option は 0 個か 1 個の要素を持つコンテナとして見ることができます。
特に、IntoIterator トレイトを実装しているため、そのような型を必要とする汎用的なコードで使用することができます。

OptionIntoIterator を実装しているため.extend() の引数として使用できます。

let turing = Some("Turing");
let mut logicians = vec!["Curry", "Kleene", "Markov"];

logicians.extend(turing);

// 以下と等しい
if let Some(turing_inner) = turing {
    logicians.push(turing_inner);
}

既存のイテレータの最後に Option をつける必要がある場合は.chain() に渡します。

let turing = Some("Turing");
let logicians = vec!["Curry", "Kleene", "Markov"];

for logician in logicians.iter().chain(turing.iter()) {
    println!("{} is a logician", logician);
}

Option がいつも Some の場合には、要素の代わりにstd::iter::onceを使うのがより慣用句的です。

また、OptionIntoIterator を実装しているため、for ループを使ってイテレートすることも可能です。これは if let Some(..) を使うのと同じであり、ほとんどの場合で後者の方がよいでしょう。

参考

変数をクロージャに渡す

解説

デフォルトで、クロージャは環境を借用によってキャプチャします。
もしくは move クロージャを使って環境全体を move します。
しかし、いくつかの変数だけをクロージャに移動させたり、いくつかのデータのコピーを与えたり、参照で渡したり、他の変換を行いたい場合がよくあります。

そのためには、別のスコープで変数のリバインディングを使用します。

以下の代わりに

use std::rc::Rc;

let num1 = Rc::new(1);
let num2 = Rc::new(2);
let num3 = Rc::new(3);

let num2_cloned = num2.clone();
let num3_borrowed = num3.as_ref();
let closure = move || {
    *num1 + *num2_cloned + *num3_borrowed;
};

次のようにします。

use std::rc::Rc;

let num1 = Rc::new(1);
let num2 = Rc::new(2);
let num3 = Rc::new(3);
let closure = {
    // `num1` is moved
    let num2 = num2.clone();  // `num2` is cloned
    let num3 = num3.as_ref();  // `num3` is borrowed
    move || {
        *num1 + *num2 + *num3;
    }
};

利点

コピーされたデータは、クロージャの定義と一緒にグループ化されているので、目的がより明確になり、クロージャで消費されなくてもすぐに drop されます。

クロージャでは、データをコピーするか移動するか、周囲のコードと同じ変数名を使用しています。

欠点

クロージャー本体に追加でインデントが必要になります。

拡張性のために private にする

解説

プライベートフィールドを使用して、安定性の保証を壊すことなく構造体を拡張できるようにします。

mod a {
    // public な構造体
    pub struct S {
        pub foo: i32,
        // プライベートフィールド
        bar: i32,
    }
}

fn main(s: a::S) {
    // S::bar はプライベートなので、ここでは名前をつけることができずパターン内で `..` を使わなければならない
    let a::S { foo: _, ..} = s;
}

議論

構造体にフィールドを追加することは、ほとんどの場合、下位互換性のある変更です。
しかし、クライアントがパターンを使用して構造体インスタンスをデコンストラクトする場合、構造体内のすべてのフィールドに名前を付け、新しいフィールドを追加するとそのパターンが壊れてしまいます。クライアントはいくつかのフィールドに名前を付けて、パターン内で .. を使用することができます。この場合、別のフィールドを追加することは下位互換性があります。少なくとも一つの構造体のフィールドをプライベートにすると、クライアントは後者の形式のパターンを使用するようになります。構造体の将来性を確保します。

この方法の欠点は、不要なフィールドを構造体に追加する必要があるかもしれないことです。実行時のオーバーヘッドがないように () 型を使用し、フィールド名の前に _ を付加することで未使用フィールドの警告を回避することができます。

もし Rust が列挙型のプライベート変数を許可しているならば、列挙型にバリアントを追加して下位互換性を持たせるために同じトリックを使うことができます。ここで問題となるのは、網羅的なマッチ式です。プライベート変数はクライアントに _ ワイルドカードパターンを強制します。代わりにこれを実装する一般的な方法は #[non_exhaustive] 属性を使用することです。

簡単なドキュメントの初期化

解説

構造体の初期化に多大な労力を要する場合は、ドキュメントを書く際に、構造体を引数に取るヘルパー関数を使用してサンプルをラップした方が早くなることがあります。

動機

複数または複雑なパラメータを持つ構造体と、複数のメソッドが存在することがあります。
これらのメソッドのそれぞれには例が必要です。

例えば、

struct Connection {
    name: String,
    stream: TcpStream,
}

impl Connection {
    /// コネクションを介してリクエストを送信する。
    ///
    /// # Example
    /// ```no_run
    /// # // ボイラープレートは、例の実行するために必要
    /// # let stream = TcpStream::connect("127.0.0.1:34254");
    /// # let connection = Connection { name: "foo".to_owned(), stream };
    /// # let request = Request::new("RequestId", RequestType::Get, "payload");
    /// let response = connection.send_request(request);
    /// assert!(response.is_ok());
    /// ```
    fn send_request(&self, request: Request) -> Result<Status, SendErr> {
        // ...
    }

    /// ボイラープレートをここで繰り返す必要がある!
    fn check_status(&self) -> Status {
        // ...
    }
}

ConnectionRequest を作成するボイラープレートをタイピングする代わりに、これらを引数として受け取るヘルパー関数を作る方が簡単です。

struct Connection {
    name: String,
    stream: TcpStream,
}

impl Connection {
    /// コネクションを介してリクエストを送信する。
    ///
    /// # Example
    /// ```
    /// # fn call_send(connection: Connection, request: Request) {
    /// let response = connection.send_request(request);
    /// assert!(response.is_ok());
    /// # }
    /// ```
    fn send_request(&self, request: Request) {
        // ...
    }
}

Note 上の例では assert!(response.is_ok()); という行はテスト中には実行されません。なぜなら関数の中身は呼び出されないからです。

利点

この方がはるかに簡潔で、例の中の反復的なコードを避けることができます。

欠点

関数の中に例があるため、コードはテストされません。
しかし、cargo testを実行したときにコンパイルされているかどうかはチェックされます。そのため、このパターンは no_run が必要なときに最も便利です。これを使えば no_run を追加する必要はありません。

議論

アサーションが必要ない場合、このパターンはうまく機能します。

アサーションが必要な場合は、#[doc(hidden)] で (ユーザには見えないように)アノテーションされたヘルパーインスタンスを作成するための public メソッドを作成することができます。そうすれば、このメソッドはクレートの公開 API の一部なので、rustdoc の中で呼び出すことができます。

一時的な mutabiliy

解説

データを準備して処理する必要がある場合が多いですが、その後のデータは検査するだけで、変更することはありません。この意図を明示的にするためには、ミュータブル変数を不変変数として再定義することができます。

これは、入れ子になったブロック内でデータを処理するか、変数を再定義することで行うことができます。

ベクトルを使用される前にソートされなければならないとします。

ネストしたブロックを使う場合:

let data = {
    let mut data = get_vec();
    data.sort();
    data
};

// ここでは `data` はイミュータブル

変数のリバインディングを使用する場合:

let mut data = get_vec();
data.sort();
let data = data;

// ここでは `data` はイミュータブル

利点

コンパイラは、ある時点でデータを誤って変更させないようにします。

欠点

入れ子になったブロックでは、ブロック本体のインデントを追加する必要があります。
ブロックからデータを返すか、変数を再定義するためにもう 1 行必要です。

156
152
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
156
152

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?