ジェネリクス(<T>)/ 型パラメータ
| 対応: | Rust 1.0(2015) |
|---|
ジェネリクスは、型をパラメータとして受け取れる汎用的な関数・構造体・列挙型を定義する仕組みで、コードの重複を避けながら型安全性を保てます。
構文
// 関数のジェネリクス
fn 関数名<T>(引数: T) -> T { ... }
// 複数の型パラメータ
fn 関数名<T, U>(a: T, b: U) { ... }
// 構造体のジェネリクス
struct 構造体名<T> {
フィールド: T,
}
// 列挙型のジェネリクス(Optionの定義例)
enum Option<T> {
Some(T),
None,
}
// implブロックのジェネリクス
impl<T> 構造体名<T> {
fn メソッド名(&self) -> &T { &self.フィールド }
}
型パラメータ一覧
| 記法 | 概要 |
|---|---|
| <T> | 単一の型パラメータです。慣例的に大文字1文字を使います。 |
| <T, U> | 複数の型パラメータです。それぞれ独立した型を受け取ります。 |
| <T: Trait> | トレイト境界付き型パラメータです。特定のトレイトを実装した型に限定します。 |
| struct Foo<T> | 構造体をジェネリクスにします。フィールドの型を外部から指定できます。 |
| impl<T> Foo<T> | ジェネリクス構造体にメソッドを実装します。 |
サンプルコード
ジェネリクス関数とジェネリクス構造体の基本的な使い方を確認するサンプルコードです。
sample_generics_basic.rs
// ジェネリクス関数:どんな型の最大値も求められます。
fn largest<T: PartialOrd>(a: T, b: T) -> T {
if a > b { a } else { b }
}
// ジェネリクス構造体:任意の型の値を保持します。
struct Pair<T> {
first: T,
second: T,
}
impl<T: std::fmt::Display> Pair<T> {
fn show(&self) {
println!("({}, {})", self.first, self.second);
}
}
fn main() {
// 整数でも浮動小数点でも同じ関数が使えます。
println!("{}", largest(3, 7)); // 7
println!("{}", largest(2.5, 1.8)); // 2.5
// 構造体の型パラメータは推論されます。
let p = Pair { first: 10, second: 20 };
p.show(); // (10, 20)
let s = Pair { first: "hello", second: "world" };
s.show(); // (hello, world)
}
コンパイルして実行すると次のように出力されます。
rustc generics_basic.rs ./generics_basic 7 2.5 (10, 20) (hello, world)
よくあるミス
よくあるミス1: トレイト境界なしで演算子を使おうとするコンパイルエラー
トレイト境界のない型パラメータでは、演算子(+、>、==など)が使用できません。使いたい演算子を提供するトレイトを境界として指定する必要があります。
// コンパイルエラー: T に PartialOrd トレイト境界がないため > を使えない
// fn largest<T>(a: T, b: T) -> T {
// if a > b { a } else { b } // error: binary operation `>` cannot be applied to type `T`
// }
// OK: PartialOrd 境界を追加する
fn largest<T: PartialOrd>(a: T, b: T) -> T {
if a > b { a } else { b }
}
fn main() {
println!("{}", largest(3, 7)); // 7
println!("{}", largest(2.5_f64, 1.8)); // 2.5
}
よくあるミス2: ジェネリクス構造体と具体型の実装を混同する
ジェネリクス構造体に impl ブロックを書く場合、`impl<T>` と `impl 構造体名<T>` の両方が必要です。具体型(例: impl Pair<i32>)は特定の型のみのメソッドを追加する場合に使います。
struct Pair<T> {
first: T,
second: T,
}
// すべての型 T に適用されるメソッド
impl<T> Pair<T> {
fn new(first: T, second: T) -> Self {
Pair { first, second }
}
}
// Display + PartialOrd を実装した型にのみ適用されるメソッド
impl<T: std::fmt::Display + PartialOrd> Pair<T> {
fn cmp_display(&self) {
if self.first >= self.second {
println!("大きい方: {}", self.first);
} else {
println!("大きい方: {}", self.second);
}
}
}
fn main() {
let p = Pair::new(10, 20);
p.cmp_display(); // 大きい方: 20
}
よくあるミス3: 型推論が失敗するケース
ジェネリクスの型が推論できない場合、コンパイラはエラーになります。変数の型注釈かターボフィッシュ構文(::`<型>`)で明示します。
fn main() {
// コンパイルエラー: collect() の型を推論できない
// let v = (1..=5).collect(); // error: type annotations needed
// OK: 型注釈で明示する
let v: Vec<i32> = (1..=5).collect();
println!("{:?}", v);
// OK: ターボフィッシュ構文でも指定できる
let v2 = (1..=5).collect::<Vec<i32>>();
println!("{:?}", v2);
}
概要
ジェネリクスを使うと、同じロジックを異なる型に対して再利用できます。Rustはコンパイル時に各型について具体的なコードを生成する「単相化(monomorphization)」を行うため、実行時のオーバーヘッドはありません。
型パラメータ名は慣例的に『T』『U』『V』のような大文字1文字が使われますが、任意の名前を付けることもできます。複数の型パラメータが必要な場合は『<T, U>』のようにカンマで区切ります。
トレイト境界なしの型パラメータは、演算子(>、+など)を使用できません。型に対して特定の操作を行いたい場合は、トレイト境界を指定する必要があります。
記事の間違いや著作権の侵害等ございましたらお手数ですがこちらまでご連絡頂ければ幸いです。