ジェネリクス
型をパラメータ化する「ジェネリクス」について学びます。
ジェネリクスとは
ジェネリクスは型を抽象化し、同じコードを複数の型で使えるようにします。
#![allow(unused)]
fn main() {
// i32用
fn largest_i32(list: &[i32]) -> i32 { ... }
// f64用
fn largest_f64(list: &[f64]) -> f64 { ... }
// ジェネリクスで1つに
fn largest<T>(list: &[T]) -> T { ... }
}
関数のジェネリクス
fn largest<T: PartialOrd + Copy>(list: &[T]) -> T {
let mut largest = list[0];
for &item in list {
if item > largest {
largest = item;
}
}
largest
}
fn main() {
let numbers = vec![34, 50, 25, 100, 65];
println!("最大: {}", largest(&numbers));
let chars = vec!['y', 'm', 'a', 'q'];
println!("最大: {}", largest(&chars));
}
型パラメータの慣習
T- Type(一般的な型)E- Error(エラー型)K,V- Key, Value(マップ用)R- Result(結果型)
構造体のジェネリクス
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 };
}
複数の型パラメータ
struct Point<T, U> {
x: T,
y: U,
}
fn main() {
let mixed = Point { x: 5, y: 4.0 };
}
ジェネリクス構造体のメソッド
#![allow(unused)]
fn main() {
struct Point<T> {
x: T,
y: T,
}
impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
}
// 特定の型にのみ実装
impl Point<f64> {
fn distance_from_origin(&self) -> f64 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}
}
列挙型のジェネリクス
標準ライブラリのOptionとResultがまさにこれです。
#![allow(unused)]
fn main() {
enum Option<T> {
Some(T),
None,
}
enum Result<T, E> {
Ok(T),
Err(E),
}
}
自作のジェネリクス列挙型
enum Response<T> {
Success(T),
Loading,
Error(String),
}
fn main() {
let user_response: Response<String> = Response::Success(String::from("太郎"));
let count_response: Response<i32> = Response::Success(42);
}
トレイト境界
ジェネリクスに「このトレイトを実装している型のみ」という制約をつけます。
#![allow(unused)]
fn main() {
use std::fmt::Display;
fn print_info<T: Display>(item: T) {
println!("情報: {}", item);
}
}
複数のトレイト境界
#![allow(unused)]
fn main() {
fn compare_and_display<T: PartialOrd + Display>(a: T, b: T) {
if a > b {
println!("{} > {}", a, b);
} else {
println!("{} <= {}", a, b);
}
}
}
where句
境界が複雑な場合に読みやすくなります。
#![allow(unused)]
fn main() {
fn some_function<T, U>(t: T, u: U) -> i32
where
T: Display + Clone,
U: Clone + Debug,
{
// ...
}
}
実践例: ジェネリックなペア
#[derive(Debug)]
struct Pair<T> {
first: T,
second: T,
}
impl<T> Pair<T> {
fn new(first: T, second: T) -> Pair<T> {
Pair { first, second }
}
fn swap(&mut self) {
std::mem::swap(&mut self.first, &mut self.second);
}
}
impl<T: PartialOrd + Display> Pair<T> {
fn cmp_display(&self) {
if self.first > self.second {
println!("最大は {}", self.first);
} else {
println!("最大は {}", self.second);
}
}
}
fn main() {
let mut pair = Pair::new(5, 10);
pair.cmp_display();
pair.swap();
println!("{:?}", pair);
}
ジェネリクスとパフォーマンス
Rustのジェネリクスは単相化(Monomorphization)されます。
#![allow(unused)]
fn main() {
// このコードは
fn largest<T: PartialOrd>(a: T, b: T) -> T { ... }
largest(5, 10);
largest(1.0, 2.0);
// コンパイル時にこうなる
fn largest_i32(a: i32, b: i32) -> i32 { ... }
fn largest_f64(a: f64, b: f64) -> f64 { ... }
}
つまり、実行時のコストはゼロです!
標準ライブラリのジェネリクス例
#![allow(unused)]
fn main() {
// Vec<T>
let v: Vec<i32> = vec![1, 2, 3];
let v: Vec<String> = vec![String::from("a")];
// HashMap<K, V>
use std::collections::HashMap;
let mut map: HashMap<String, i32> = HashMap::new();
// Option<T>
let some: Option<i32> = Some(5);
// Result<T, E>
let result: Result<i32, String> = Ok(42);
}
まとめ
| 概念 | 説明 |
|---|---|
| ジェネリクス | 型をパラメータ化 |
<T> | 型パラメータ |
| トレイト境界 | T: Traitで制約を追加 |
where句 | 複雑な境界を読みやすく |
| 単相化 | コンパイル時に具体型に展開(ゼロコスト) |
確認テスト
Q1. ジェネリクスの主な目的は?
正解: C) ジェネリクスは型を抽象化し、コードの再利用性を高めます。例えば
Vec<T>はVec<i32>にもVec<String>にもなれます。
Q2. fn foo<T: Clone + Debug>(x: T)のトレイト境界が意味するのは?
正解: B)
+は「かつ」を意味し、両方のトレイトを実装している型のみ受け付けます。
Q3. 以下のコードの出力は?struct Container<T> { value: T } impl<T> Container<T> { fn new(value: T) -> Self { Container { value } } fn get(&self) -> &T { &self.value } } let c = Container::new(42); println!("{}", c.get());
正解: B)
Container<i32>が作成され、getメソッドで内部の値への参照が返されます。println!は参照を自動的に解決して42を出力します。
Q4. fn print_largest<T>(a: T, b: T)でa > bを使うために必要なトレイト境界は?
正解: C) 比較演算子(
>、<など)を使うにはPartialOrdトレイトが必要です。さらにprintln!で表示するにはDisplayも必要になります。
Q5. Rustのジェネリクスの「単相化(Monomorphization)」とは?
正解: A) 単相化とは、コンパイル時にジェネリックなコードが具体的な型ごとに展開されることです。これにより実行時のオーバーヘッドがゼロになります。
次のドキュメント: 05_modules.md