ジェネリック型、トレイト、ライフタイム

全てのプログラミング言語には、概念の重複を効率的に扱う道具があります。 Rustにおいて、そのような道具の一つがジェネリクスです。 ジェネリクスは、具体型や他のプロパティの抽象的な代役です。コンパイルやコード実行時に、 ジェネリクスの位置に何が入るかを知ることなく、ジェネリクスの振る舞いや他のジェネリクスとの関係を表現できるのです。

関数が未知の値の引数を取り、同じコードを複数の具体的な値に対して走らせるように、 i32Stringなどの具体的な型の代わりに何かジェネリックな型の引数を取ることができます。 実際、第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);
}

リスト10-1: 数字のリストから最大値を求める

このコードは、整数のリストを変数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-2: 2つの数値のリストから最大値を探すコード

このコードは動くものの、コードを複製することは退屈ですし、間違いも起きやすいです。 また、コードを変更したい時には、複数箇所更新することを覚えておかなくてはなりません。

この重複を排除するには、引数で与えられた整数のどんなリストに対しても処理が行える関数を定義して抽象化しましょう。 この解決策によりコードがより明確になり、リストの最大値を探すという概念を抽象的に表現させてくれます。

リスト10-3では、最大値を探すコードをlargestという関数に抽出しています。 そして、リスト10-2の2つのリストから最大値を探すために、この関数を呼んでいます。 将来持つことになるかもしれない他のどんなi32値のリストに対しても、この関数を使用することができます。

ファイル名: src/main.rs

fn largest(list: &[i32]) -> &i32 {
    let mut largest = &list[0];

    for item in list {
        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);
}

リスト10-3: 2つのリストから最大値を探す抽象化されたコード

largest関数にはlistと呼ばれる引数があり、これは、関数に渡す可能性のある、あらゆるi32値の具体的なスライスを示します。 結果的に、関数呼び出しの際、コードは渡した特定の値に対して走るのです。

まとめとして、こちらがリスト10-2のコードからリスト10-3に変更するのに要したステップです:

  1. 重複したコードを見分ける。
  2. 重複コードを関数本体に抽出し、コードの入力と戻り値を関数シグニチャで指定する。
  3. 重複したコードの2つの実体を代わりに関数を呼び出すように更新する。

次は、この同じ手順をジェネリクスでも踏んでコードの重複を減らします。 関数本体が特定の値ではなく抽象的なlistに対して処理できたのと同様に、 ジェネリクスは抽象的な型に対して処理するコードを可能にしてくれます。

例えば、関数が2つあるとしましょう: 1つはi32値のスライスから最大の要素を探し、1つはchar値のスライスから最大要素を探します。 この重複はどう排除するのでしょうか?答えを見つけましょう!