404 Not Found

404 Not Found


nginx

Rustのジェネリクス:型パラメータ化プログラミング

ジェネリクスは「型レベルでのパラメータ化プログラミング」であり、型をパラメータとして渡すことで、型ごとにコードを書き直すことなく、同じコードを複数の型に適用できるようにするものです。

関数が「値をパラメータに抽象化」しているのなら、ジェネリックも同様に「型をパラメータに抽象化」していると言えます。これは、テイクアウトを注文する際に「ご飯を1人前ください」と言い、チャーハンにするか、他の料理に添えるご飯にするかを指定しないのと同じようなものです。どちらにするかは、店に着いてから決めるのです。


1. 学習内容


2. 概念図

以下の「Mermaid」図は、コンパイル時のモノモルフィゼーション処理において、ジェネリック型パラメータ T が具体的な型に置き換えられる仕組みを示しています:

100%
graph LR
    A["Generic Functions<br/>fn identity&lt;T&gt;(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(&#34;hello&#34;.to_string())"]
    E --> H["Call identity(3.14)"]

3. 多用途コンテナの物語

(1) 悩み:タイプごとに同じコードを繰り返し書くこと

Luna(ルナ)はツールキットを開発中ですが、「最大値を見つける」という機能が必要です。

最初は、種類ごとに1つずつ書いた:

RUST
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におけるジェネリクスの扱い

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) 汎用システム

TEXT
graph TB
    A[Generics Generics] --> B[Generic Functions]
    A --> C[Generic Structures]
    A --> D[Generic Enumerations]
    A --> E[Generic Methods]

    B --> F["fn identity&lt;T&gt;(x: T) -> T"]
    C --> G["struct Point&lt;T&gt; { x: T, y: T }"]
    D --> H["Option&lt;T&gt;, Result&lt;T, E&gt;"]
    E --> I["impl&lt;T&gt; Point&lt;T&gt; { 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) ▶ サンプル:汎用関数—配列の最大値を求める(難易度 ⭐)

RUST
// ============================================
// 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
}

出力:

TEXT
i32 Maximum value: 9
f64 Maximum value: 3.14
&str Maximum value: date

find_max<T> はジェネリック関数、T は型パラメータ、<T: std::cmp::PartialOrd> はトレイトバインド(「T は比較可能な型でなければならない」という意味)です。関数が呼び出されると、コンパイラは実際の引数の型に基づいて T を自動的に推論します。


(2) ▶ サンプル:汎用構造体 — 座標系 (難易度 ⭐⭐)

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

出力:

TEXT
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> はジェネリック構造体であり、xy は同じ型(どちらも T)です。impl<T> Point<T> は、すべての T インスタンスに共通するメソッドを実装しています。impl Point<f64> は、特定の型に対してのみ固有のメソッドを実装しています。これがジェネリクスの大きな利点の一つです。


(3) ▶ サンプル:ジェネリック列挙型 — Option と Result の実践例 (難易度 ⭐⭐)

RUST
// ============================================
// 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);
}

出力:

TEXT
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) ▶ サンプル:複数の型パラメータとモノモルフィズム (難易度 ⭐⭐⭐)

RUST
// ============================================
// 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));
}

出力:

TEXT
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) ▶ サンプル:総合演習—汎用コンテナとアルゴリズム(難易度 ⭐⭐⭐)

RUST
// ============================================
// 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);
}

出力:

TEXT
=== 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 制約を使用して、ジェネリック比較演算子を実装してください。


❓ よくある質問

Q ジェネリックとBox<dyn Any>の違いは何ですか?
A ジェネリックはコンパイル時に型を決定(静的ディスパッチ)するのに対し、dyn Anyは実行時に型を決定(動的ディスパッチ)します。ジェネリックは、コンパイラが型ごとに特化されたコードを生成するため、仮想関数のオーバーヘッドが排除され、パフォーマンスが向上します。dyn Anyは、実行時に任意の型を扱えるため柔軟性が高いですが、実行時のオーバーヘッドが発生します。
Q モノモルフィゼーションを行うと、バイナリファイルのサイズが大きくなりますか?
A はい、ですがその影響は通常ごくわずかです。実際に使用される型の組み合わせごとに、個別のコードファイルが生成されます。ジェネリック関数が多数の異なる型で呼び出される場合、バイナリサイズは増加します。ただし、Rustコンパイラは最適化を行うため、現代のCPUでは「コードサイズ」がボトルネックになることは一般的にありません。サイズが懸念される場合は、代わりに dyn Trait の使用を検討してください。
Q ジェネリック関数とジェネリックメソッドの違いは何ですか?
A ジェネリック関数は独立した関数であるのに対し、ジェネリックメソッドは型に対して定義される関数です。ジェネリック関数:fn foo<T>(x: T)。ジェネリックメソッド:impl<T> MyType<T> { fn bar(&self) }。ジェネリックメソッドはSelf型にアクセスできますが、ジェネリック関数はアクセスできません。
Q impl<T> ブロックと impl ブロックの違いは何ですか?
A impl<T> はすべての T 型に対する実装方法ですが、標準の impl は特定の型に対する実装方法です。impl<T> Point<T> { fn x(&self) } はすべての Point 型で使用可能です。impl Point<f64> { fn distance(&self) }Point<f64>でのみ利用可能です。
Q トレイトの境界を表す where 句と、角括弧で直接記述する場合の違いは何ですか?
A 目的は同じですが、複雑な制約の場合、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句の使用が推奨されます。

📖 まとめ


📝 練習問題

  1. 難易度 ⭐:値を受け取り、そのまま返す汎用関数 fn echo<T>(x: T) -> T を作成してください。main 関数内で、それぞれ i32f64&str を引数としてこの関数を呼び出してください。
  2. 難易度 ⭐⭐: フィールド value: T を持つ汎用構造体 Container<T> を定義してください。実装 fn get(&self) -> &T は値への参照を返し、fn set(&mut self, val: T) はその値を変更します。main 関数内で、それぞれ Container<String>Container<i32> を使用してこれをテストしてください。
  3. 難易度 ⭐⭐⭐: 2つのスライスを結合して新しいVecを返す汎用関数 fn merge_arrays<T>(a: &[T], b: &[T]) -> Vec<T> を実装してください。型 T は Clone を実装している必要があります。その後、main関数内で、2つの i32 スライスと2つの &str スライスを、それぞれ結合してください。
Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%