導入
「i32 の最大値を返す関数」と「f64 の最大値を返す関数」を別々に書くのは面倒です。中身のロジックは同じなのだから、型をパラメータにして1回だけ書きたい——それを叶えるのがジェネリクスです。
説明
型を <T> というパラメータにして、関数や構造体を「型に対して汎用的」にします。
// どんな型 T でも動く「ペア」構造体
struct Pair<T> {
first: T,
second: T,
}
impl<T> Pair<T> {
fn new(first: T, second: T) -> Pair<T> {
Pair { first, second }
}
}
fn main() {
let ints = Pair::new(1, 2); // T = i32
let words = Pair::new("a", "b"); // T = &str
println!("{} {}", ints.first, words.second);
}
Pair<T>…Tは「あとで決まる型」のプレースホルダ。Pair::new(1, 2)ならT = i32、Pair::new("a", "b")ならT = &strとコンパイラが決めます。impl<T> Pair<T>… ジェネリックな型にメソッドを実装するときはimpl<T>と書きます。
ここで大事なのは、ジェネリクスは実行時のコストがゼロだということです。Rust はコンパイル時に、実際に使われた型ごとに具体的なコードを生成します(単相化 / monomorphization)。Pair<i32> と Pair<&str> はそれぞれ専用のコードに展開されるので、「汎用にしたから遅くなる」ことがありません。抽象化してもゼロコスト、という Rust の哲学がここにも表れています。
ただし、上の Pair はフィールドを保持するだけでした。もし関数の中で T の値どうしを「比較する」「足す」といった操作をしたくなったら? 「T はどんな型でもいい」だけでは、その型が比較や加算をできるか分かりません。そこで必要になるのが、次のレッスンのトレイトです。
試すには
Pair::new(1, "a") のように first と second で違う型を渡すとエラーになります(両方 T なので同じ型でなければならない)。違う型を許したいなら Pair<T, U> と2つの型パラメータにします。ジェネリクスが「型の約束」をどう表しているか感じてみましょう。