ジェネリック型、トレイト、ライフタイム
全てのプログラミング言語には、概念の重複を効率的に扱う道具があります。Rustにおいて、そのような道具の一つがジェネリクスです。 ジェネリクスは、具体型や他のプロパティの抽象的な代役です。コード記述の際、コンパイルやコード実行時に、 ジェネリクスの位置に何が入るかを知ることなく、ジェネリクスの振る舞いや他のジェネリクスとの関係を表現できるのです。
関数が未知の値の引数を取り、同じコードを複数の具体的な値に対して走らせるように、
i32
やString
などの具体的な型の代わりに何かジェネリックな型の引数を取ることができます。
実際、第6章でOption<T>
、第8章でVec<T>
とHashMap<K, V>
、第9章でResult<T, E>
を既に使用しました。
この章では、独自の型、関数、メソッドをジェネリクスとともに定義する方法を探究します!
まず、関数を抽出して、コードの重複を減らす方法を確認しましょう。次に同じテクニックを活用して、 引数の型のみが異なる2つの関数からジェネリックな関数を生成します。また、 ジェネリックな型を構造体やenum定義で使用する方法も説明します。
それから、トレイトを使用して、ジェネリックな方法で振る舞いを定義する方法を学びます。 ジェネリックな型にトレイトを組み合わせることで、ジェネリックな型を、単にあらゆる型に対してではなく、特定の振る舞いのある型のみに制限できます。
最後に、ライフタイムを議論します。ライフタイムとは、コンパイラに参照がお互いにどう関係しているかの情報を与える一種のジェネリクスです。 ライフタイムのおかげでコンパイラに参照が有効であることを確認してもらうことを可能にしつつ、多くの場面で値を借用できます。
関数を抽出することで重複を取り除く
ジェネリクスの記法に飛び込む前にまずは、関数を抽出することでジェネリックな型が関わらない重複を取り除く方法を見ましょう。 そして、このテクニックを適用してジェネリックな関数を抽出するのです!重複したコードを認識して関数に抽出できるのと同じように、 ジェネリクスを使用できる重複コードも認識し始めるでしょう。
リスト10-1に示したように、リスト内の最大値を求める短いプログラムを考えてください。
ファイル名: src/main.rs
fn main() { let number_list = vec![34, 50, 25, 100, 65]; let mut largest = number_list[0]; for number in number_list { if number > largest { largest = number; } } // 最大値は{}です println!("The largest number is {}", largest); assert_eq!(largest, 100); }
このコードは、整数のリストを変数number_list
に格納し、リストの最初の数字をlargest
という変数に配置しています。
それからリストの数字全部を走査し、現在の数字がlargest
に格納された数値よりも大きければ、
その変数の値を置き換えます。ですが、現在の数値が今まで見た最大値よりも小さければ、
変数は変わらず、コードはリストの次の数値に移っていきます。リストの数値全てを吟味した後、
largest
は最大値を保持しているはずで、今回は100になります。
2つの異なる数値のリストから最大値を発見するには、リスト10-1のコードを複製し、 プログラムの異なる2箇所で同じロジックを使用できます。リスト10-2のようにですね。
ファイル名: src/main.rs
fn main() { let number_list = vec![34, 50, 25, 100, 65]; let mut largest = number_list[0]; for number in number_list { if number > largest { largest = number; } } println!("The largest number is {}", largest); let number_list = vec![102, 34, 6000, 89, 54, 2, 43, 8]; let mut largest = number_list[0]; for number in number_list { if number > largest { largest = number; } } println!("The largest number is {}", largest); }
このコードは動くものの、コードを複製することは退屈ですし、間違いも起きやすいです。また、 コードを変更したい時に複数箇所、更新しなければなりません。
この重複を排除するには、引数で与えられた整数のどんなリストに対しても処理が行える関数を定義して抽象化できます。 この解決策によりコードがより明確になり、リストの最大値を探すという概念を抽象的に表現させてくれます。
リスト10-3では、最大値を探すコードをlargest
という関数に抽出しました。リスト10-1のコードは、
たった1つの特定のリストからだけ最大値を探せますが、それとは異なり、このプログラムは2つの異なるリストから最大値を探せます。
ファイル名: src/main.rs
fn largest(list: &[i32]) -> i32 { let mut largest = list[0]; for &item in list.iter() { if item > largest { largest = item; } } largest } fn main() { let number_list = vec![34, 50, 25, 100, 65]; let result = largest(&number_list); println!("The largest number is {}", result); assert_eq!(result, 100); let number_list = vec![102, 34, 6000, 89, 54, 2, 43, 8]; let result = largest(&number_list); println!("The largest number is {}", result); assert_eq!(result, 6000); }
largest
関数にはlist
と呼ばれる引数があり、これは、関数に渡す可能性のある、あらゆるi32
値の具体的なスライスを示します。
結果的に、関数呼び出しの際、コードは渡した特定の値に対して走るのです。
まとめとして、こちらがリスト10-2のコードからリスト10-3に変更するのに要したステップです:
- 重複したコードを見分ける。
- 重複コードを関数本体に抽出し、コードの入力と戻り値を関数シグニチャで指定する。
- 重複したコードの2つの実体を代わりに関数を呼び出すように更新する。
次は、この同じ手順をジェネリクスでも踏んで異なる方法でコードの重複を減らします。
関数本体が特定の値ではなく抽象的なlist
に対して処理できたのと同様に、
ジェネリクスは抽象的な型に対して処理するコードを可能にしてくれます。
例えば、関数が2つあるとしましょう: 1つはi32
値のスライスから最大の要素を探し、1つはchar
値のスライスから最大要素を探します。
この重複はどう排除するのでしょうか?答えを見つけましょう!