コンテンツにスキップ

Rustのジェネリクス

実は、名前を意識していなくても、あなたはもうジェネリクス(generics)を使っています。Vec<i32> も Option<T> もジェネリクスを使った型です。このページでは、ジェネリックな関数や構造体を自分で書く方法を学びます。

数値のリストから最大値を返す関数を、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回書くだけで済みます。

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));
}

実行結果:

47
2.8
▶ 実際に試してみよう

<T: PartialOrd + Copy> は「比較でき(PartialOrd)、コピーできる(Copy)型なら、どんな型 T でもよい」と読みます。T はただの仮の名前(プレースホルダー)です。Rustは、実際にその関数を呼び出した具体的な型(i32 や f64 など)ごとに、関数のバージョンを生成してくれます。

構造体もジェネリックにできます。

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回だけです。

かかりません。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 句に移してシグネチャを読みやすくできます。これは高度なトレイトのページで扱います。

いちばん小さい要素を返す 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']));
}

実行結果:

3
r
▶ 実際に試してみよう

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());
}

実行結果:

9
pear
▶ 実際に試してみよう

文字列どうしの比較は、辞書順(正確には UTF-8 のバイト順)で行われます。"pear" は "apple" よりあとに来るので、pear が「大きい」ほうになります。日本語の単語にすると並び順が変わってしまうので、ここでは英語のままにしています。

  • ジェネリクスを使うと、コードを重複させずに、複数の型で使える関数や構造体を1つだけ書けます。
  • <T> は仮の型で、トレイト境界(T: PartialOrd)で T に使える型を制限します。
  • impl<T> Pair<T> はすべての T にメソッドを追加し、impl Pair<f64> は具体的な1つの型だけにメソッドを追加します。
  • ジェネリックなコードは型ごとの専用バージョンにコンパイルされるので、実行時の性能コストはありません。

理解度チェック

1. ジェネリクスはどんな問題を解決しますか?

2. fn largest<T: PartialOrd + Copy>(...) の PartialOrd + Copy は何をしていますか?

3. ジェネリクスを使うと、手で書いたコードに比べて実行時のコストはかかりますか?

4. struct Pair<T> の定義が1つあると、何ができますか?

スコア: 0 / 4