Rustのジェネリクス
実は、名前を意識していなくても、あなたはもうジェネリクス(generics)を使っています。Vec<i32> も Option<T> もジェネリクスを使った型です。このページでは、ジェネリックな関数や構造体を自分で書く方法を学びます。
ジェネリクスが解決する問題
Section titled “ジェネリクスが解決する問題”数値のリストから最大値を返す関数を、i32 と f64 の両方で使いたいとします。ジェネリクスがなければ、同じ関数を2回書くことになります。
fn largest_i32(numbers: &[i32]) -> i32 { let mut largest = numbers[0]; for &n in numbers { if n > largest { largest = n; } } largest}
fn largest_f64(numbers: &[f64]) -> f64 { let mut largest = numbers[0]; for &n in numbers { if n > largest { largest = n; } } largest}中身はまったく同じで、型ごとにコピー&ペーストしただけです。ジェネリクスを使えば、1回書くだけで済みます。
ジェネリックな関数
Section titled “ジェネリックな関数”fn largest<T: PartialOrd + Copy>(numbers: &[T]) -> T { let mut largest = numbers[0]; for &n in numbers { if n > largest { largest = n; } } largest}
fn main() { let integers = [10, 25, 3, 47, 8]; let floats = [1.5, 2.8, 0.3];
println!("{}", largest(&integers)); println!("{}", largest(&floats));}実行結果:
472.8<T: PartialOrd + Copy> は「比較でき(PartialOrd)、コピーできる(Copy)型なら、どんな型 T でもよい」と読みます。T はただの仮の名前(プレースホルダー)です。Rustは、実際にその関数を呼び出した具体的な型(i32 や f64 など)ごとに、関数のバージョンを生成してくれます。
ジェネリックな構造体
Section titled “ジェネリックな構造体”構造体もジェネリックにできます。
struct Pair<T> { first: T, second: T,}
fn main() { let numbers = Pair { first: 5, second: 10 }; let words = Pair { first: "こんにちは", second: "世界" };
println!("{} {}", numbers.first, numbers.second); println!("{} {}", words.first, words.second);}実行結果:
5 10こんにちは 世界1つの Pair<T> の定義が、数値にもテキストにも、ほかのどんな型にも使えます。numbers は Pair<i32>、words は Pair<&str> ですが、構造体を書いたのは1回だけです。
実行時のコストはかかるの?
Section titled “実行時のコストはかかるの?”かかりません。Rustは、実際に使った具体的な型ごとに、関数や構造体の専用バージョンを別々に生成します(これを単相化(monomorphization)と呼びます)。プログラムが実行されるときには、ジェネリックなコードはもう残っていません。あるのは、型がすべて決まった普通の関数だけで、1つずつ手で書いた場合とまったく同じ速さで動きます。
ジェネリックな構造体のメソッド
Section titled “ジェネリックな構造体のメソッド”構造体のページと同じように、impl ブロックを使えばジェネリックな構造体にもメソッドを追加できます。新しいのは、impl にも専用の <T> が必要だという点だけです。
struct Pair<T> { first: T, second: T,}
impl<T> Pair<T> { fn new(first: T, second: T) -> Self { Pair { first, second } }
fn swap(self) -> Pair<T> { Pair { first: self.second, second: self.first } }}
impl Pair<f64> { fn average(&self) -> f64 { (self.first + self.second) / 2.0 }}
fn main() { let words = Pair::new("左", "右").swap(); println!("{} {}", words.first, words.second);
let scores = Pair::new(7.0, 9.0); println!("{}", scores.average());}実行結果:
右 左8ここには2種類の impl ブロックがあります。
impl<T> Pair<T>は「これらのメソッドは、どんな型のPairにもある」という意味です。最初の<T>は「どんな型Tについても」と読みます。これがないと、Rust はTを実在する型の名前だと思い、見つからないと文句を言います。impl Pair<f64>には<T>がありません。このメソッドはPair<f64>だけにあります。だからaverageの中で+や/ 2.0が使えるのです。試しにwords.average()を呼んでみると、Pair<&str>にはそんなメソッドはないとコンパイラが教えてくれます。
2つのフィールドの型が違ってもいいときは、struct Pair<T, U> { first: T, second: U } のように、型パラメータを複数持つ構造体も作れます。また、トレイト境界が長くなってきたら、where 句に移してシグネチャを読みやすくできます。これは高度なトレイトのページで扱います。
1. ジェネリックなsmallest
Section titled “1. ジェネリックなsmallest”いちばん小さい要素を返す fn smallest<T: PartialOrd + Copy>(items: &[T]) -> T を書いてください。[8, 3, 5] と、文字の ['r', 'u', 's', 't'] で試してみましょう。ヒント:このページの largest 関数の比較を1か所逆にするだけです。
解答を見る
fn smallest<T: PartialOrd + Copy>(items: &[T]) -> T { let mut smallest = items[0]; for &item in items { if item < smallest { smallest = item; } } smallest}
fn main() { println!("{}", smallest(&[8, 3, 5])); println!("{}", smallest(&['r', 'u', 's', 't']));}実行結果:
3r2. Pairの大きいほう
Section titled “2. Pairの大きいほう”Pair<T> に、大きいほうのフィールドへの参照を返すメソッド larger(&self) -> &T を追加してください。数値の Pair と、単語の Pair で試してみましょう。ヒント:比較には PartialOrd が必要なので、境界を impl そのものに付けます(impl<T: PartialOrd> Pair<T>)。
解答を見る
struct Pair<T> { first: T, second: T,}
impl<T: PartialOrd> Pair<T> { fn larger(&self) -> &T { if self.first > self.second { &self.first } else { &self.second } }}
fn main() { let numbers = Pair { first: 3, second: 9 }; let words = Pair { first: "pear", second: "apple" };
println!("{}", numbers.larger()); println!("{}", words.larger());}実行結果:
9pear文字列どうしの比較は、辞書順(正確には UTF-8 のバイト順)で行われます。"pear" は "apple" よりあとに来るので、pear が「大きい」ほうになります。日本語の単語にすると並び順が変わってしまうので、ここでは英語のままにしています。
- ジェネリクスを使うと、コードを重複させずに、複数の型で使える関数や構造体を1つだけ書けます。
<T>は仮の型で、トレイト境界(T: PartialOrd)でTに使える型を制限します。impl<T> Pair<T>はすべてのTにメソッドを追加し、impl Pair<f64>は具体的な1つの型だけにメソッドを追加します。- ジェネリックなコードは型ごとの専用バージョンにコンパイルされるので、実行時の性能コストはありません。
理解度チェック
Section titled “理解度チェック”理解度チェック
1. ジェネリクスはどんな問題を解決しますか?
2. fn largest<T: PartialOrd + Copy>(...) の PartialOrd + Copy は何をしていますか?
3. ジェネリクスを使うと、手で書いたコードに比べて実行時のコストはかかりますか?
4. struct Pair<T> の定義が1つあると、何ができますか?
スコア: 0 / 4
さらに詳しく
Section titled “さらに詳しく”- ジェネリックなデータ型 — The Rust Programming Language 日本語版:関数、構造体、列挙型、メソッドでのジェネリクスを、
largestの例も含めて解説しています。 - ジェネリクス — Rust By Example 日本語版:ジェネリックな関数や構造体とその書き方を、短い実行できる例で紹介しています。
- メソッド — Rust By Example 日本語版:
impl<T>ブロックや、具体的な1つの型だけにメソッドを実装する方法をさらに詳しく見られます。 - ジェネリック境界 — Rust By Example 日本語版:トレイト境界がジェネリックな型をどう制限するのか、なぜそれが必要なのかがわかります。
- Generic parameters — The Rust Reference(英語):細かいところまで知りたくなったときのための、正確なルールです。