ジェネリクスとは何か
ジェネリクス(Generics)とは、型をパラメータとして受け取ることができる仕組みです。同じロジックを複数の型に対して使い回したいとき、型ごとに関数や構造体を書き直す必要がなくなります。
たとえば「2つの値のうち大きい方を返す関数」を i32 用と f64 用に別々に書くのは冗長です。ジェネリクスを使えば1つの定義で済みます。
fn largest<T: PartialOrd>(a: T, b: T) -> T {
if a > b {
a
} else {
b
}
}
fn main() {
println!("{}", largest(10, 20)); // 20
println!("{}", largest(3.5_f64, 2.1)); // 3.5
}
<T> が型パラメータです。T: PartialOrd は「T は比較可能な型でなければならない」という制約(トレイト境界)を表しています。
構造体にジェネリクスを使う
関数だけでなく、構造体にも型パラメータを持たせることができます。
struct Point<T> {
x: T,
y: T,
}
fn main() {
let int_point = Point { x: 1, y: 2 };
let float_point = Point { x: 1.5, y: 3.0 };
println!("({}, {})", int_point.x, int_point.y);
println!("({}, {})", float_point.x, float_point.y);
}
x と y が異なる型を持ちたい場合は、型パラメータを複数定義します。
struct Point<T, U> {
x: T,
y: U,
}
fn main() {
let mixed = Point { x: 5, y: 3.14 };
println!("x={}, y={}", mixed.x, mixed.y);
}
メソッドにジェネリクスを使う
impl ブロックでもジェネリクスを使えます。
struct Wrapper<T> {
value: T,
}
impl<T> Wrapper<T> {
fn new(value: T) -> Self {
Wrapper { value }
}
fn get(&self) -> &T {
&self.value
}
}
fn main() {
let w = Wrapper::new(42);
println!("{}", w.get()); // 42
let ws = Wrapper::new("hello");
println!("{}", ws.get()); // hello
}
impl<T> の <T> を忘れずに書くのがポイントです。
トレイト境界(Trait Bounds)
ジェネリクスに制約を付けることで、型パラメータが持っていなければならない機能を指定できます。
use std::fmt::Display;
fn print_value<T: Display>(value: T) {
println!("Value: {}", value);
}
fn main() {
print_value(100);
print_value("Rust");
print_value(3.14);
}
複数のトレイト境界を同時に指定したい場合は + でつなぎます。
use std::fmt::{Debug, Display};
fn print_debug_and_display<T: Debug + Display>(value: T) {
println!("Display: {}", value);
println!("Debug: {:?}", value);
}
where句でスッキリ書く
型パラメータが増えたり、トレイト境界が複雑になるとシグネチャが読みにくくなります。そんなときは where 句を使いましょう。
where句を使わない例(読みにくい):
fn compare_and_print<T: PartialOrd + Display, U: Debug>(a: T, b: T, extra: U) {
if a > b {
println!("{} is larger. Extra: {:?}", a, extra);
} else {
println!("{} is larger. Extra: {:?}", b, extra);
}
}
where句を使った例(スッキリ):
use std::fmt::{Debug, Display};
fn compare_and_print<T, U>(a: T, b: T, extra: U)
where
T: PartialOrd + Display,
U: Debug,
{
if a > b {
println!("{} is larger. Extra: {:?}", a, extra);
} else {
println!("{} is larger. Extra: {:?}", b, extra);
}
}
fn main() {
compare_and_print(10, 20, vec![1, 2, 3]);
}
where 句は可読性が高まるだけでなく、複雑な条件を整理するのにも役立ちます。
ジェネリクスとモノモーフィゼーション
Rustのジェネリクスはコンパイル時にモノモーフィゼーションが行われます。これは、コンパイラが実際に使われる型ごとに専用のコードを生成する仕組みです。
fn double<T: std::ops::Add<Output = T> + Copy>(x: T) -> T {
x + x
}
fn main() {
double(5_i32); // i32用のコードが生成される
double(2.5_f64); // f64用のコードが生成される
}
これにより、実行時のオーバーヘッドがなく、高速なコードが生成されます。動的ディスパッチ(dyn Trait)とは異なる点です。
よくあるミスと対処法
型に必要なトレイトを指定し忘れる
// コンパイルエラー:`T` に `Display` の境界がない
fn print_it<T>(value: T) {
println!("{}", value); // エラー!
}
// 正しい
fn print_it<T: std::fmt::Display>(value: T) {
println!("{}", value);
}
impl ブロックで型パラメータを書き忘れる
struct Container<T> {
item: T,
}
// 間違い:impl Container<T> { ... }
// 正しい:
impl<T> Container<T> {
fn get(&self) -> &T {
&self.item
}
}
まとめ
| 概念 | 説明 |
|---|---|
型パラメータ <T> | 関数・構造体・enumなどを型に依存せず定義する |
トレイト境界 T: Trait | ジェネリクスに制約を与える |
複数境界 T: A + B | 複数のトレイトを同時に要求する |
where 句 | 複雑なトレイト境界を整理して書く |
| モノモーフィゼーション | コンパイル時に型ごとのコードを生成・実行時コストなし |
Rustのジェネリクスは最初は少し戸惑うかもしれませんが、<T> と where 句の使い方さえ押さえれば、柔軟で再利用性の高いコードが書けるようになります。トレイトと組み合わせることで、Rustの型システムの力を最大限に引き出せます。ぜひ実際にコードを書きながら慣れていきましょう。