Rustのジェネリクス:型パラメータ化プログラミング
ジェネリクスは「型レベルでのパラメータ化プログラミング」であり、型をパラメータとして渡すことで、型ごとにコードを書き直すことなく、同じコードを複数の型に適用できるようにするものです。
関数が「値をパラメータに抽象化」しているのなら、ジェネリックも同様に「型をパラメータに抽象化」していると言えます。これは、テイクアウトを注文する際に「ご飯を1人前ください」と言い、チャーハンにするか、他の料理に添えるご飯にするかを指定しないのと同じようなものです。どちらにするかは、店に着いてから決めるのです。
1. 学習内容
- ジェネリック関数の定義と呼び出し
fn foo<T>(x: T) - 汎用構造体の使い方
struct Point<T> - ジェネリック列挙型の詳細な理解
Option<T>およびResult<T, E> - ジェネリックメソッドの書き方
impl<T> - モノモルフィゼーション — コンパイル時展開のためのメカニズム
- 複数の型パラメータの使用
2. 概念図
以下の「Mermaid」図は、コンパイル時のモノモルフィゼーション処理において、ジェネリック型パラメータ T が具体的な型に置き換えられる仕組みを示しています:
graph LR
A["Generic Functions<br/>fn identity<T>(x: T) -> T"] --> B["Compile-Time Singletonization<br/>Monomorphization"]
B --> C["T → i32<br/>fn identity_i32(x: i32) -> i32"]
B --> D["T → String<br/>fn identity_string(x: String) -> String"]
B --> E["T → f64<br/>fn identity_f64(x: f64) -> f64"]
C --> F["Call identity(42)"]
D --> G["Call identity("hello".to_string())"]
E --> H["Call identity(3.14)"]
3. 多用途コンテナの物語
(1) 悩み:タイプごとに同じコードを繰り返し書くこと
Luna(ルナ)はツールキットを開発中ですが、「最大値を見つける」という機能が必要です。
最初は、種類ごとに1つずつ書いた:
fn max_i32(a: i32, b: i32) -> i32 {
if a > b { a } else { b }
}
fn max_f64(a: f64, b: f64) -> f64 {
if a > b { a } else { b }
}
fn max_str(a: &str, b: &str) -> &str {
if a > b { a } else { b }
}
fn main() {
println!("{}", max_i32(3, 7)); // 7
println!("{}", max_f64(2.5, 1.8)); // 2.5
println!("{}", max_str("apple", "banana")); // banana
}
型が異なる点を除けば、これら3つの関数のロジックはまったく同じです。もしu32、u64、charといった型も存在した場合、追加される型ごとにコードをコピーしなければなりません。これは「コピー&ペーストプログラミング」であり、洗練されておらず、保守性も低いものです。
(2) Rustにおけるジェネリクスの扱い
fn max<T: std::cmp::PartialOrd>(a: T, b: T) -> T {
if a > b { a } else { b }
}
fn main() {
println!("{}", max(3, 7)); // 7
println!("{}", max(2.5, 1.8)); // 2.5
println!("{}", max("apple", "banana")); // banana
}
ジェネリック関数は、特定の型に対する3つの関数を置き換えるものであり、コンパイラは実際に使用される各型に対して、自動的に特化されたコードを生成します(単一型性)。1つ記述するだけで、コンパイラがそれを複数の関数に展開してくれます。
4. 基本概念
(1) 汎用システム
graph TB
A[Generics Generics] --> B[Generic Functions]
A --> C[Generic Structures]
A --> D[Generic Enumerations]
A --> E[Generic Methods]
B --> F["fn identity<T>(x: T) -> T"]
C --> G["struct Point<T> { x: T, y: T }"]
D --> H["Option<T>, Result<T, E>"]
E --> I["impl<T> Point<T> { fn x(&self) -> &T }"]
A --> J[Singleton Monomorphization]
J --> K["At compile time:T → i32, f64, String ..."]
J --> L["Generate separate code for each type"]
(2) ジェネリック型、具体型、動的ディスパッチ
| 機能 | 特定型(非ジェネリック) | ジェネリック(単一型) | 動的ディスパッチ(dyn Trait) |
|---|---|---|---|
| コードの重複 | 型ごとに1つずつ記述する | コンパイラによる自動展開 | コードは1つで、実行時に分散される |
| パフォーマンス | 最高値 | 最高値(仮想関数のオーバーヘッドを除く) | 仮想関数のオーバーヘッドを含む |
| コンパイル時間 | 長い(手書きのコードが多い) | かなり長い(展開が多い) | 短い |
| バイナリのボリューム | 大 | 中(種類ごとに1部) | 小 |
| 柔軟性 | 低い | コンパイル時に決定される | 実行時に決定される |
(3) 一般的な汎用列挙型
| 列挙 | 定義 | 目的 |
|---|---|---|
Option<T> |
enum Option<T> { Some(T), None } |
空になる可能性のある値 |
Result<T, E> |
enum Result<T, E> { Ok(T), Err(E) } |
失敗する可能性のある操作 |
Vec<T> |
struct Vec<T> { ... } |
動的配列 |
HashMap<K, V> |
struct HashMap<K, V> { ... } |
キーと値の対応 |
(4) 汎用制約手法の比較
| 制約の種類 | 構文 | 使用例 | 例 |
|---|---|---|---|
| インライン制約 | fn foo<T: Trait>(x: T) |
単純な単一制約 | fn max<T: PartialOrd>(a: T, b: T) |
| 複数の制約 + | fn foo<T: Trait1 + Trait2>(x: T) |
複数の制約 | fn print<T: Display + Clone>(x: T) |
| WHERE句 | fn foo<T>(x: T) where T: Trait |
複合制約、複数型のパラメータ | where T: Display + Clone, U: Debug |
| impl トレイト | fn foo(x: impl Trait) |
省略表記(シンタックスシュガー) | fn plug(d: &impl USBDevice) |
5. 一般的な例
(1) ▶ サンプル:汎用関数—配列の最大値を求める(難易度 ⭐)
// ============================================
// Generic Functions:Applies to any comparable type
// ============================================
// PartialOrd Constraint Assurance T Supports comparison operations
fn find_max<T: std::cmp::PartialOrd>(list: &[T]) -> &T {
let mut max = &list[0];
for item in list.iter() {
if item > max {
max = item;
}
}
max
}
fn main() {
let numbers = vec![3, 7, 1, 9, 4];
println!("i32 Maximum value: {}", find_max(&numbers));
let floats = vec![2.5, 1.8, 3.14, 0.99];
println!("f64 Maximum value: {}", find_max(&floats));
let strings = vec!["apple", "banana", "cherry", "date"];
println!("&str Maximum value: {}", find_max(&strings));
// The Same Function,Three Types,Compiler Auto-Expansion
}
出力:
i32 Maximum value: 9
f64 Maximum value: 3.14
&str Maximum value: date
find_max<T>はジェネリック関数、Tは型パラメータ、<T: std::cmp::PartialOrd>はトレイトバインド(「T は比較可能な型でなければならない」という意味)です。関数が呼び出されると、コンパイラは実際の引数の型に基づいてTを自動的に推論します。
(2) ▶ サンプル:汎用構造体 — 座標系 (難易度 ⭐⭐)
// ============================================
// Generic Structures:Point Can store coordinates of any type
// ============================================
#[derive(Debug)]
struct Point<T> {
x: T,
y: T,
}
// Implementation Methods for Generic Structures
impl<T> Point<T> {
// Back x Citation
fn x(&self) -> &T {
&self.x
}
// Back y Citation
fn y(&self) -> &T {
&self.y
}
}
// For Point of a specific type f64, implement additional methods
impl Point<f64> {
fn distance_from_origin(&self) -> f64 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}
fn main() {
let int_point = Point { x: 5, y: 10 };
let float_point = Point { x: 3.0, y: 4.0 };
let string_point = Point {
x: "left",
y: "right",
};
println!("int_point: {:?}", int_point);
println!("float_point x: {}", float_point.x());
println!("string_point: {:?}", string_point);
// Only `Point<f64>` has the distance_from_origin method
println!("Distance from the origin: {:.2}", float_point.distance_from_origin());
// Compilation Error: int_point is Point<i32>, no such method
// println!("{}", int_point.distance_from_origin());
}
出力:
int_point: Point { x: 5, y: 10 }
float_point x: 3.0
string_point: Point { x: "left", y: "right" }
Distance from the origin: 5.00
Point<T>はジェネリック構造体であり、xとyは同じ型(どちらもT)です。impl<T> Point<T>は、すべてのTインスタンスに共通するメソッドを実装しています。impl Point<f64>は、特定の型に対してのみ固有のメソッドを実装しています。これがジェネリクスの大きな利点の一つです。
(3) ▶ サンプル:ジェネリック列挙型 — Option と Result の実践例 (難易度 ⭐⭐)
// ============================================
// Generic Enumerations: Option<T> and Result<T, E> Usage
// ============================================
// Custom Result Style Enumeration
#[derive(Debug)]
enum MyResult<T, E> {
Success(T),
Failure(E),
}
// Division Function:Back Result Style
fn safe_divide<T>(a: T, b: T) -> MyResult<T, String>
where
T: std::ops::Div<Output = T> + std::cmp::PartialEq + From<u8> + Copy,
{
if b == 0.into() {
MyResult::Failure("Division by zero".to_string())
} else {
MyResult::Success(a / b)
}
}
// Using the standard library Option<T>
fn find_in_vector<T: PartialEq>(vec: &[T], target: &T) -> Option<usize> {
for (i, item) in vec.iter().enumerate() {
if item == target {
return Some(i);
}
}
None
}
// Using the standard library Result<T, E>
fn parse_number(s: &str) -> Result<i32, String> {
s.parse::<i32>().map_err(|e| format!("Parse error: {}", e))
}
fn main() {
// Option Usage
let numbers = vec![10, 20, 30, 40, 50];
match find_in_vector(&numbers, &30) {
Some(index) => println!("Found 30,Index: {}", index),
None => println!("Not found"),
}
match find_in_vector(&numbers, &99) {
Some(index) => println!("Found 99,Index: {}", index),
None => println!("Not found 99"),
}
// Result Usage
match parse_number("42") {
Ok(n) => println!("Parsing Successful: {}", n),
Err(e) => println!("Parsing Failed: {}", e),
}
match parse_number("hello") {
Ok(n) => println!("Parsing Successful: {}", n),
Err(e) => println!("Parsing Failed: {}", e),
}
// Custom MyResult Usage
let result = safe_divide(10.0, 3.0);
println!("Custom Result: {:?}", result);
let result = safe_divide(10.0, 0.0);
println!("Custom Result: {:?}", result);
}
出力:
Found 30,Index: 2
Not found 99
Parsing Successful: 42
Parsing Failed: Parse error: invalid digit found in string
Custom Result: Success(3.3333333333333335)
Custom Result: Failure("Division by zero")
Option<T>には型パラメータが 1 つだけ(T、値の有無にかかわらず)あるのに対し、Result<T, E>には 2 つの型パラメータ(成功時の型とエラー時の型)があります。ジェネリック列挙型により、これらの型を任意のデータ型に適用することが可能になります。これは、Rust 標準ライブラリの設計における中核的な原則の 1 つです。
(4) ▶ サンプル:複数の型パラメータとモノモルフィズム (難易度 ⭐⭐⭐)
// ============================================
// Multiple Type Parameters + Combining Generic Methods
// ============================================
use std::fmt::Display;
// Generic struct with two type parameters
#[derive(Debug)]
struct Pair<K, V> {
key: K,
value: V,
}
// Implement methods for Pair<K, V>
impl<K, V> Pair<K, V> {
fn new(key: K, value: V) -> Self {
Pair { key, value }
}
}
// Constrained methods: only available when both K and V implement Display
impl<K: Display, V: Display> Pair<K, V> {
fn print(&self) {
println!("Key: {}, Value: {}", self.key, self.value);
}
}
// Generic Methods:Mixing Different Types of Parameters
fn mix_and_match<T, U>(a: T, b: U) -> String
where
T: Display,
U: Display,
{
format!("Mixed: {} and {}", a, b)
}
fn main() {
// Multiple Type Parameters: String and i32
let pair1 = Pair::new("Age".to_string(), 25);
pair1.print();
// Multiple Type Parameters: &str and f64
let pair2 = Pair::new("PI", 3.14159);
pair2.print();
// Different Types of Combinations
let pair3 = Pair::new(100, "HTTP OK");
// pair3.print(); // ❌ Compilation Error: i32 and &str both implement Display, but no error here.
// In fact i32 and &str both implement Display, so you can call it
// This is just to demonstrate the concept of constraint methods.
pair3.print();
// Mixing Different Types
println!("{}", mix_and_match(42, "answer"));
println!("{}", mix_and_match(3.14, 100));
}
出力:
Key: Age, Value: 25
Key: PI, Value: 3.14159
Key: 100, Value: HTTP OK
Mixed: 42 and answer
Mixed: 3.14 and 100
複数の型パラメータ (
<K, V>) を使用することで、構造体は異なる型のデータを保持できるようになります。where節は、型パラメータが満たすべき条件を制約するために使用されます。モノモルフィゼーションの際、コンパイラはPair<String, i32>やPair<&str, f64>といった各組み合わせごとに個別のコードを生成します。
(5) ▶ サンプル:総合演習—汎用コンテナとアルゴリズム(難易度 ⭐⭐⭐)
// ============================================
// Comprehensive Example:Generic Stack + Generic Search Algorithms
// ============================================
use std::fmt::Display;
struct Stack<T> {
items: Vec<T>,
}
impl<T> Stack<T> {
fn new() -> Self {
Stack { items: Vec::new() }
}
fn push(&mut self, item: T) {
self.items.push(item);
}
fn pop(&mut self) -> Option<T> {
self.items.pop()
}
fn peek(&self) -> Option<&T> {
self.items.last()
}
fn is_empty(&self) -> bool {
self.items.is_empty()
}
fn len(&self) -> usize {
self.items.len()
}
}
impl<T: Display> Stack<T> {
fn print_all(&self) {
for item in &self.items {
print!("{} ", item);
}
println!();
}
}
fn find_first<T: PartialEq>(items: &[T], target: &T) -> Option<usize> {
items.iter().position(|x| x == target)
}
fn swap_if_greater<T: PartialOrd>(a: &mut T, b: &mut T) {
if *a > *b {
std::mem::swap(a, b);
}
}
fn main() {
let mut int_stack: Stack<i32> = Stack::new();
int_stack.push(10);
int_stack.push(20);
int_stack.push(30);
println!("=== Integer Stack ===");
println!("Stack Contents: ");
int_stack.print_all();
println!("Stack top: {:?}", int_stack.peek());
println!("Pop up: {:?}", int_stack.pop());
println!("Remaining {} element", int_stack.len());
let mut str_stack: Stack<&str> = Stack::new();
str_stack.push("Rust");
str_stack.push("is");
str_stack.push("awesome");
println!("\n=== String Stack ===");
str_stack.print_all();
let nums = vec![10, 20, 30, 40, 50];
println!("\n=== Generic Search ===");
println!("Find 30: Index {:?}", find_first(&nums, &30));
println!("Find 99: Index {:?}", find_first(&nums, &99));
let words = vec!["apple", "banana", "cherry"];
println!("Find 'banana': Index {:?}", find_first(&words, &"banana"));
let mut x = 42;
let mut y = 10;
println!("\n=== Generic Swaps ===");
println!("Before the exchange: x={}, y={}", x, y);
swap_if_greater(&mut x, &mut y);
println!("After the exchange: x={}, y={}", x, y);
}
出力:
=== Integer Stack ===
Stack Contents:
10 20 30
Stack top: Some(30)
Pop up: Some(30)
Remaining 2 element
=== String Stack ===
Rust is awesome
=== Generic Search ===
Find 30: Index Some(2)
Find 99: Index None
Find 'banana': Index Some(1)
=== Generic Swaps ===
Before the exchange: x=42, y=10
After the exchange: x=10, y=42
ジェネリック型
Stack<T>は i32 と &str; の両方で機能します。impl<T: Display>制約は、その型が Display を実装している場合にのみprint_allメソッドが利用可能になることを保証します。PartialOrd制約を使用して、ジェネリック比較演算子を実装してください。
❓ よくある質問
Box<dyn Any>の違いは何ですか?dyn Anyは実行時に型を決定(動的ディスパッチ)します。ジェネリックは、コンパイラが型ごとに特化されたコードを生成するため、仮想関数のオーバーヘッドが排除され、パフォーマンスが向上します。dyn Anyは、実行時に任意の型を扱えるため柔軟性が高いですが、実行時のオーバーヘッドが発生します。dyn Trait の使用を検討してください。fn foo<T>(x: T)。ジェネリックメソッド:impl<T> MyType<T> { fn bar(&self) }。ジェネリックメソッドはSelf型にアクセスできますが、ジェネリック関数はアクセスできません。impl<T> ブロックと impl ブロックの違いは何ですか?impl<T> はすべての T 型に対する実装方法ですが、標準の impl は特定の型に対する実装方法です。impl<T> Point<T> { fn x(&self) } はすべての Point 型で使用可能です。impl Point<f64> { fn distance(&self) }はPoint<f64>でのみ利用可能です。where 句と、角括弧で直接記述する場合の違いは何ですか?where 句の方が可読性が高くなります。 fn foo<T: Display + Clone, U: Debug>(t: T, u: U)はfn foo<T, U>(t: T, u: U) where T: Display + Clone, U: Debugと同等です。複数の制約がある場合は、where句の使用が推奨されます。📖 まとめ
- 汎用関数
fn foo<T>(x: T)を使用すると、同じ関数を複数の型に適用することができます - 汎用構造体
struct Point<T>では、構造体のフィールドに任意の型を格納できます - ジェネリック列挙型
Option<T>およびResult<T, E>は、Rust 標準ライブラリにおけるジェネリック設計の代表的な例です。 - ジェネリックメソッド
impl<T> Type<T>は、すべての型パラメータに対してメソッドを実装するほか、特定の型に対して個別のメソッドを実装することもできます - モノモルフィゼーションとは、コンパイラがコンパイル時にジェネリックなコードを特定の型向けのコードに展開するプロセスであり、実行時のオーバーヘッドは一切発生しません。
- 複数の型パラメータ
<T, U>およびwhereを含む制約句により、ジェネリクスは柔軟性と安全性の両方を兼ね備えています
📝 練習問題
- 難易度 ⭐:値を受け取り、そのまま返す汎用関数
fn echo<T>(x: T) -> Tを作成してください。main関数内で、それぞれi32、f64、&strを引数としてこの関数を呼び出してください。 - 難易度 ⭐⭐: フィールド
value: Tを持つ汎用構造体Container<T>を定義してください。実装fn get(&self) -> &Tは値への参照を返し、fn set(&mut self, val: T)はその値を変更します。main 関数内で、それぞれContainer<String>とContainer<i32>を使用してこれをテストしてください。 - 難易度 ⭐⭐⭐: 2つのスライスを結合して新しいVecを返す汎用関数
fn merge_arrays<T>(a: &[T], b: &[T]) -> Vec<T>を実装してください。型 T はCloneを実装している必要があります。その後、main関数内で、2つのi32スライスと2つの&strスライスを、それぞれ結合してください。



