// dictionary
ジェネリクス.
ジェネリクスは、<T>のような型引数(型パラメータ)を使って「具体的な型は後から決める」定義を書く仕組みです。i32用・String用と同じ処理を型ごとに書き分ける代わりに、型を引数にした定義を1つ書けば、呼び出し側が使った型に合わせてコンパイラが具体化してくれます。関数・構造体・列挙型・implブロック・トレイトなどが型引数を持てます1。
// 関数: どんな型のペアでも順番を入れ替えられる
fn swap_pair<T>(pair: (T, T)) -> (T, T) {
(pair.1, pair.0)
}
// 構造体: 値の型を後から決める「ラベル付きの値」
struct Labeled<T> {
label: String,
value: T,
}
// implブロック: impl<T> と宣言してから Labeled<T> に使う
impl<T> Labeled<T> {
fn new(label: &str, value: T) -> Self {
Labeled { label: label.to_string(), value }
}
}
// 特定の型だけに追加するメソッド(T = u32 のときだけ使える)
impl Labeled<u32> {
fn with_tax(&self) -> u32 {
self.value * 110 / 100
}
}
fn main() {
// 先攻と後攻を入れ替える(T = &str と推論される)
let (first, second) = swap_pair(("赤組", "白組"));
println!("先攻: {} / 後攻: {}", first, second);
let price = Labeled::new("りんご", 150u32); // T = u32
let memo = Labeled::new("備考", String::from("特売品")); // T = String
println!("{}: {}円(税込{}円)", price.label, price.value, price.with_tax());
println!("{}: {}", memo.label, memo.value);
}Playgroundで開く使える場所と書き方
| 場所 | 書き方 | 補足 |
|---|---|---|
| 関数 | fn f<T>(x: T) -> T |
呼び出し時の引数からTが型推論で決まる |
| 構造体 | struct S<T> { value: T } |
型引数を使ったS<i32>とS<String>は別の型 |
| 列挙型 | enum E<T> { A(T), B } |
Option型のOption<T>・Result型のResult<T, E>がこの形 |
| implブロック | impl<T> S<T> { ... } |
implの直後でTを宣言する(宣言しないと具体的な型名扱い)2 |
単相化と実行時コスト
ジェネリックなコードは、コンパイル時に「実際に使われた具体的な型の組み合わせ」ごとに専用のコードへ展開されます。この処理を単相化(monomorphization)と呼びます3。上の例ではswap_pairが&str版として、Labeled::newがu32版とString版として、それぞれ別々に生成されます。
実行時には、型ごとに手書きした関数を呼ぶのと同じコードが動くため、ジェネリクスを使ってもプログラムは遅くなりません3。型の解決が実行時に行われることはなく、これを静的ディスパッチと呼びます4。実行時に呼び先を決めるdyn Trait(トレイトオブジェクト)とは対照的です。
補足
implブロックの型引数の制約とターボフィッシュ
implブロックで宣言した型引数は、実装対象の型か実装するトレイト(またはそれらの境界の関連型)に現れる必要があります。impl<T> S { ... }のように、どこにも使われないTはエラー: E0207になります5。
呼び出し側で型を推論できないときは、ターボフィッシュ記法::<>で明示します(例: "42".parse::<u32>()、iter.collect::<Vec<_>>())6。
型引数以外のジェネリックパラメータ
ジェネリックパラメータには型引数のほかに、ライフタイムパラメータ('a)と定数パラメータ(const N: usize)があります1。並べる順序は「ライフタイム → 型と定数(混在可)」と決まっています1。配列型[T; N]の要素数Nのように、値をコンパイル時の引数として受け取れるのが定数パラメータです。
// N 個分の合計を返す(要素数が型の一部なので、どの長さの配列でも1つの定義で済む)
fn total<const N: usize>(prices: [u32; N]) -> u32 {
prices.iter().sum()
}
fn main() {
println!("{}円", total([120, 350])); // N = 2
println!("{}円", total([100, 200, 300])); // N = 3
}Playgroundで開くジェネリクスでできないこと
Vec<T>のTは1つの型に決まるため、「1つのコレクションに異なる型を混在させる」ことはジェネリクスではできません。そうした用途にはトレイトオブジェクトを使います4。
Footnotes
-
The Rust Reference - Generic parameters “Functions, type aliases, structs, enumerations, unions, traits, and implementations may be parameterized by types, constants, and lifetimes.” / “The order of generic parameters is restricted to lifetime parameters and then type and const parameters intermixed.” ↩ ↩2 ↩3
-
The Rust Programming Language - Generic Data Types “By declaring T as a generic type after impl, Rust can identify that the type in the angle brackets in Point is a generic type rather than a concrete type.” ↩
-
The Rust Programming Language - Performance of Code Using Generics “Monomorphization is the process of turning generic code into specific code by filling in the concrete types that are used when compiled.” / “When the code runs, it performs just as it would if we had duplicated each definition by hand.” ↩ ↩2
-
The Rust Programming Language - Trait Objects Perform Dynamic Dispatch “The code that results from monomorphization is doing static dispatch, which is when the compiler knows what method you’re calling at compile time.” / “If you’ll only ever have homogeneous collections, using generics and trait bounds is preferable because the definitions will be monomorphized at compile time to use the concrete types.” ↩ ↩2
-
The Rust Reference - Generic implementations “Type and const parameters must always constrain the implementation.” / Error code E0207 ↩
-
The Rust Reference - Glossary: Turbofish “Paths with generic parameters in expressions must prefix the opening brackets with a ::. Combined with the angular brackets for generics, this looks like a fish ::<>.” ↩
この辞書が使われているページ
backlinks 17