Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

どのプログラミング言語にも、概念の重複を効果的に扱うための手段があります。Rust では、そのような手段のひとつが ジェネリクス です。これは、具体的な型やその他の性質の代わりとなる抽象的な代役です。ジェネリクスの振る舞いや、ほかのジェネリクスとどのように関係するかを、コードのコンパイル時や実行時に何がその場所に入るのかを知らなくても表現できます。

関数は、i32String のような具体的な型ではなく、何らかのジェネリック型のパラメータを受け取ることができます。これは、複数の具体的な値に対して同じコードを実行するために、未知の値を持つパラメータを受け取れるのと同じです。実際、すでに第6章では Option<T>、第8章では Vec<T>HashMap<K, V>、第9章では Result<T, E> でジェネリクスを使っています。この章では、ジェネリクスを使って独自の型、関数、メソッドを定義する方法を見ていきます。

まず、コードの重複を減らすために関数を抽出する方法を復習します。次に、パラメータの型だけが異なる2つの関数から、同じ手法を使ってジェネリックな関数を作ります。さらに、struct と enum の定義でジェネリック型を使う方法も説明します。

その後、トレイトを使って振る舞いをジェネリックに定義する方法を学びます。トレイトとジェネリック型を組み合わせることで、ジェネリック型が受け入れる型を、単なる任意の型ではなく、特定の振る舞いを持つ型だけに制約できます。

最後に、ライフタイム について説明します。これは、参照どうしがどのように関係しているかについてコンパイラに情報を与える、ジェネリクスの一種です。ライフタイムによって、借用された値に関する十分な情報をコンパイラに与えられるため、私たちの助けがない場合よりも多くの状況で参照が有効であることを保証できるようになります。

関数を抽出して重複を取り除く

ジェネリクスを使うと、複数の型を表すプレースホルダーで特定の型を置き換えることで、コードの重複を取り除けます。ジェネリクスの構文に入る前に、まずは、複数の値を表すプレースホルダーで特定の値を置き換える関数を抽出することで、ジェネリック型を使わずに重複を取り除く方法を見てみましょう。その後、同じ手法を適用してジェネリックな関数を抽出します。関数として抽出できる重複コードの見分け方を見ることで、ジェネリクスを使える重複コードも見分けられるようになっていきます。

まずは、リスト内の最大の数値を見つける、リスト10-1の短いプログラムから始めます。

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に示されています。

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-2の2つのリストで最大の数値を見つけます。将来、ほかの i32 値のリストがあれば、そのリストに対してもこの関数を使えます。

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);
}

largest 関数には list という名前のパラメータがあり、これはその関数に渡す可能性のある任意の i32 値のスライスを表します。その結果、この関数を呼び出すと、コードは私たちが渡した具体的な値に対して実行されます。

まとめると、リスト10-2のコードをリスト10-3に変更するために行った手順は次のとおりです。

  1. 重複しているコードを特定する。
  2. 重複しているコードを関数本体に抽出し、そのコードの入力と戻り値を関数シグネチャで指定する。
  3. 重複していた2か所のコードを、代わりにその関数を呼び出すよう更新する。

次に、同じ手順をジェネリクスに対して使って、コードの重複を減らします。関数本体が特定の値ではなく抽象的な list に対して操作できるのと同じように、ジェネリクスを使うとコードは抽象的な型に対して操作できます。

たとえば、2つの関数があるとします。ひとつは i32 値のスライス内で最大の要素を見つける関数、もうひとつは char 値のスライス内で最大の要素を見つける関数です。この重複をどのように取り除けばよいでしょうか。見ていきましょう!