はじめに
データの形が1種類に固定されている場合は具体的な型が便利です。しかし、数値用とテキスト用にほぼ同じ型を繰り返し定義すると、プログラムの保守が難しくなります。これまでに、Option<u32>、Vec<String>、HashMap<String, u32>、Result<T, E> などの標準ジェネリック型を使ってきました。この実験では、自分でジェネリック型を定義します。Rust のジェネリクスを使うと、コンパイラーによる各使用箇所のチェックを維持したまま、1つの定義を複数の具体的な型で使えます。
この実験では、用意されている Pair 構造体をジェネリックにし、その後、小さな借用ヘルパーも一般化します。長いプログラムとサンプルデータはすでに記述されているため、再利用を実現する少数の型指定に集中して作業できます。
Pair 構造体をジェネリックにする
このステップでは、構造体の固定された i32 型を1つの型パラメーターに置き換えます。これにより、同じ定義でスコアとルート名のどちらも保持できるようにします。
用意されている Cargo パッケージのディレクトリに移動します。
cd /home/labex/project/pair-demo
短いソースファイルを確認します。sed -n コマンドは指定した行範囲を表示するだけで、ファイルは変更しません。
sed -n '1,120p' src/main.rs
scores の値には整数が入り、routes には所有権を持つ String 値が入ります。現在の Pair 定義では両方のフィールドが i32 に固定されているため、2種類の値を表現できません。用意されている #[derive(Debug)] 属性は、この型のデバッグ出力を生成します。ただし、表示されるプログラムではその出力形式を使っていないため、ジェネリック化の作業には関係ありません。
ジェネリック型では、山括弧の中に T のようなプレースホルダーを記述します。構築された各 Pair は、引き続き1つの具体的な型を持ちます。コンパイラーは scores については Pair<i32>、routes については Pair<String> と推論します。
Nano でソースを開きます。
nano src/main.rs
ファイル上部付近で、次の3箇所だけを変更します。
struct Pair {をstruct Pair<T> {に変更します。leftの型をi32からTに変更します。rightの型をi32からTに変更します。
完成した構造体は次のとおりです。
struct Pair<T> {
left: T,
right: T,
}
ここでは両方のフィールドが同じ T を使うため、1つのペアに無関係な型の値を誤って混在させることはできません。Ctrl+O で保存し、Enter を押してから、Ctrl+X で終了します。
Cargo にパッケージの型チェックを実行させます。
cargo check
Finished で終わる行が表示されれば、コンパイラーは Pair<i32> と Pair<String> の両方を受け入れたことになります。次にプログラムを実行します。
cargo run --quiet
scores: 82 and 91
routes: north and east
first score: 82
最初の2行から、1つの構造体定義で2種類の具体的な型を保持できるようになったことが分かります。既存の first ヘルパーはまだ Pair<i32> だけを受け取るため、次のステップで一般化します。
借用ヘルパーを一般化する
このステップでは、first 関数に独自の型パラメーターを追加します。これにより、任意の Pair<T> を借用し、その最初の値への参照を返せるようにします。
ファイル上部付近にある現在のヘルパーを確認します。
sed -n '1,30p' src/main.rs
入力の Pair<i32> と出力の &i32 が、スコア専用の関数にしています。ジェネリック関数では、名前の後ろに <T> を記述し、関連する入力型と出力型の各箇所で同じ T を使います。
ファイルを開きます。
nano src/main.rs
関数宣言だけを、次の状態から:
fn first(pair: &Pair<i32>) -> &i32 {
次の状態に変更します。
fn first<T>(pair: &Pair<T>) -> &T {
この関数はペアを借用し、借用したフィールドを返します。そのため、格納された値を移動もコピーもしません。Rust は各引数から T を推論できるため、呼び出し側で型を明示する必要はありません。
次に、// FIRST_ROUTE_REPORT を、ヘルパーがテキストにも使えることを確認する次の呼び出しに置き換えます。
println!("first route: {}", first(&routes));
Nano で保存して終了します。その後、完成したプログラムの型チェックと実行を行います。
cargo check
cargo run --quiet
scores: 82 and 91
routes: north and east
first score: 82
first route: north
2つの first 行は、同じジェネリック関数から出力されています。Rust はスコアに対する呼び出しでは T = i32、ルートに対する呼び出しでは T = String と推論し、それぞれの具体的な型に基づいて各呼び出しをチェックします。再利用しても、静的な型チェックが失われることはありません。
まとめ
1つの具体的なペアを Pair<T> に置き換え、整数と文字列の両方で使用しました。また、トレイトを導入したり実装を重複させたりせずに、借用ヘルパーを一般化しました。


