Rustのジェネリクスとトレイト
ジェネリクスとトレイトをマスターし、実行時のオーバーヘッドなしに、再利用可能で型安全な高効率コードを記述する方法を学びます。
ソフトウェアを開発する際、異なるデータ型に対して同じコードをコピー&ペーストすることは、メンテナンス性を著しく低下させる要因になります。例えば、整数のリストから最大値を見つける関数を書いた場合、浮動小数点数や文字列のために同じ処理をもう一度書きたくはないはずです。
Rustでは、ジェネリクス(Generics)とトレイト(Traits)という2つの強力なツールを使って、完全に型安全で、実行時オーバーヘッドがゼロ(Zero Runtime Overhead)の再利用可能なコードを記述することができます。
この記事では、ジェネリクスとトレイトの仕組み、トレイト境界(Trait Bounds)を使用した組み合わせ方法、そしてそれらを驚くほど高速にするコンパイラの魔法について解説します。
ジェネリクス:型のプレースホルダー#
ジェネリクスを使用すると、構造体、列挙型、関数を具体的な型の代わりに「プレースホルダー型」で定義できます。i32やStringといった具体的な型をハードコーディングする代わりに、ジェネリック型パラメータ(慣習的にTと名付けられます)を使用します。
ジェネリックデータ構造#
代表的な例は Point(座標)構造体です。任意の型(整数、浮動小数点数など)の座標を保持できる構造体を定義したい場合、次のようにジェネリック型パラメータを使用します。
struct Point<T> {
x: T,
y: T,
}
fn main() {
let integer_point = Point { x: 5, y: 10 };
let float_point = Point { x: 1.0, y: 4.0 };
}rust構造体名の後に <T> を指定することで、この Point が任意の型 T に対してジェネリックであることをコンパイラに伝えます。
ゼロコスト抽象化:単態化(Monomorphization)#
「ジェネリクスを使うと、実行時にプログラムが遅くなるのではないか?」と疑問に思うかもしれません。
答えは いいえ(No) です。Rustは、単態化(monomorphization)と呼ばれるプロセスを使用してジェネリックコードをコンパイルします。コンパイル時に、コンパイラはジェネリックコードで使用されている具体的な型を調べ、それぞれの型に特化した専用のマシンコードを自動的に生成します。
graph TD
A["Point<T> (ソースコード)"] --> B["コンパイラ (単態化)"]
B --> C["Point_i32 (マシンコード)"]
B --> D["Point_f64 (マシンコード)"]
このため、実行時のパフォーマンスペナルティは一切発生しません。手動で型ごとの構造体を個別に書いた場合と全く同じ速度で動作します。
トレイト:振る舞いの契約#
ジェネリクスを使えば任意の型を扱えますが、実際にはその型が特定の「振る舞い」を持っていることを保証したい場合が多々あります。例えば、座標を表示したい場合や、リストをソートしたい場合、それぞれの型が画面表示や値の比較をサポートしている必要があります。
トレイト(Trait)は、特定の振るれるために型が実装しなければならないメソッドのセットを定義します。
トレイトの定義と実装#
summarize メソッドを要求する Summary トレイトを定義してみましょう:
pub trait Summary {
fn summarize(&self) -> String;
}
pub struct NewsArticle {
pub headline: String,
pub author: String,
pub content: String,
}
impl Summary for NewsArticle {
fn summarize(&self) -> String {
format!("{} (著者: {})", self.headline, self.author)
}
}
pub struct Tweet {
pub username: String,
pub content: String,
}
impl Summary for Tweet {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}rustこれで、NewsArticle と Tweet のインスタンスに対して共通の .summarize() メソッドを呼び出せるようになります。
デフォルト実装#
トレイトにはデフォルトの振る舞い(デフォルト実装)を持たせることもできます。トレイトを実装する型は、必要に応じてデフォルトの処理をオーバーライドすることも、そのまま利用することもできます。
pub trait Summary {
fn summarize(&self) -> String {
String::from("(続きを読む...)")
}
}rustトレイト境界:ジェネリクスとトレイトの融合#
ジェネリクスとトレイトを組み合わせることで、真の力を発揮します。トレイト境界(Trait Bounds)を使用すると、「特定のトレイトを実装している型のみ」をジェネリック型パラメータとして受け取れるように制限できます。
例えば、ジェネリックな引数を受け取り、その要約を表示する関数を書いてみましょう:
// T は Summary トレイトを実装している必要があります
pub fn notify<T: Summary>(item: &T) {
println!("ニュース速報です! {}", item.summarize());
}rustwhere 句による整理#
複数のジェネリックパラメータと多くのトレイト境界を持つ複雑な関数の場合、関数シグネチャが非常に長くなり読みづらくなります。Rustは、これを整理するための where 句を提供しています。
// 読みづらい例:
fn some_function<T: Clone + Debug, U: Serialize + Clone>(t: &T, u: &U) {}
// where句で整理した例:
fn some_function<T, U>(t: &T, u: &U)
where
T: Clone + Debug,
U: Serialize + Clone,
{}rust実践例:最大値を見つける関数#
スライスの中から最大値を返す実践的な関数を書いてみましょう。要素同士を比較できるようにするために、ジェネリック型 T は標準ライブラリの PartialOrd トレイトを実装している必要があります。
fn largest<T: PartialOrd>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
println!("最大値は {}", largest(&number_list));
let char_list = vec!['y', 'm', 'a', 'q'];
println!("最大の文字は {}", largest(&char_list));
}rustもし PartialOrd のトレイト境界を指定しない場合、コンパイラは「型 T が > 演算子による比較をサポートしているかどうかわからない」ため、ビルドエラーを発生させます。
まとめ#
- ジェネリクスは、コードの再利用性を高めるための型のプレースホルダーです。
- 単態化により、ジェネリックコードは実行時の追加コストなしでネイティブコードと同等の速度で実行されます。
- トレイトはインターフェースや契約として機能し、型ができる振る舞いを定義します。
- トレイト境界は、ジェネリック型が特定の機能を備えていることを保証し、安全なメソッド呼び出しを可能にします。
次回の記事では、参照の安全性を保つためにジェネリクスと密接に関係するライフタイム(Lifetimes)について解説します。