あっぽログ
← 記事一覧に戻る

RustのGenericを使いこなす:型パラメータ・トレイト境界・where句を徹底解説

ジェネリクスとは何か

ジェネリクス(Generics)とは、型をパラメータとして受け取ることができる仕組みです。同じロジックを複数の型に対して使い回したいとき、型ごとに関数や構造体を書き直す必要がなくなります。

たとえば「2つの値のうち大きい方を返す関数」を i32 用と f64 用に別々に書くのは冗長です。ジェネリクスを使えば1つの定義で済みます。

fn largest<T: PartialOrd>(a: T, b: T) -> T {
    if a > b {
        a
    } else {
        b
    }
}

fn main() {
    println!("{}", largest(10, 20));       // 20
    println!("{}", largest(3.5_f64, 2.1)); // 3.5
}

<T> が型パラメータです。T: PartialOrd は「T は比較可能な型でなければならない」という制約(トレイト境界)を表しています。


構造体にジェネリクスを使う

関数だけでなく、構造体にも型パラメータを持たせることができます。

struct Point<T> {
    x: T,
    y: T,
}

fn main() {
    let int_point = Point { x: 1, y: 2 };
    let float_point = Point { x: 1.5, y: 3.0 };

    println!("({}, {})", int_point.x, int_point.y);
    println!("({}, {})", float_point.x, float_point.y);
}

xy が異なる型を持ちたい場合は、型パラメータを複数定義します。

struct Point<T, U> {
    x: T,
    y: U,
}

fn main() {
    let mixed = Point { x: 5, y: 3.14 };
    println!("x={}, y={}", mixed.x, mixed.y);
}

メソッドにジェネリクスを使う

impl ブロックでもジェネリクスを使えます。

struct Wrapper<T> {
    value: T,
}

impl<T> Wrapper<T> {
    fn new(value: T) -> Self {
        Wrapper { value }
    }

    fn get(&self) -> &T {
        &self.value
    }
}

fn main() {
    let w = Wrapper::new(42);
    println!("{}", w.get()); // 42

    let ws = Wrapper::new("hello");
    println!("{}", ws.get()); // hello
}

impl<T><T> を忘れずに書くのがポイントです。


トレイト境界(Trait Bounds)

ジェネリクスに制約を付けることで、型パラメータが持っていなければならない機能を指定できます。

use std::fmt::Display;

fn print_value<T: Display>(value: T) {
    println!("Value: {}", value);
}

fn main() {
    print_value(100);
    print_value("Rust");
    print_value(3.14);
}

複数のトレイト境界を同時に指定したい場合は + でつなぎます。

use std::fmt::{Debug, Display};

fn print_debug_and_display<T: Debug + Display>(value: T) {
    println!("Display: {}", value);
    println!("Debug:   {:?}", value);
}

where句でスッキリ書く

型パラメータが増えたり、トレイト境界が複雑になるとシグネチャが読みにくくなります。そんなときは where 句を使いましょう。

where句を使わない例(読みにくい):

fn compare_and_print<T: PartialOrd + Display, U: Debug>(a: T, b: T, extra: U) {
    if a > b {
        println!("{} is larger. Extra: {:?}", a, extra);
    } else {
        println!("{} is larger. Extra: {:?}", b, extra);
    }
}

where句を使った例(スッキリ):

use std::fmt::{Debug, Display};

fn compare_and_print<T, U>(a: T, b: T, extra: U)
where
    T: PartialOrd + Display,
    U: Debug,
{
    if a > b {
        println!("{} is larger. Extra: {:?}", a, extra);
    } else {
        println!("{} is larger. Extra: {:?}", b, extra);
    }
}

fn main() {
    compare_and_print(10, 20, vec![1, 2, 3]);
}

where 句は可読性が高まるだけでなく、複雑な条件を整理するのにも役立ちます。


ジェネリクスとモノモーフィゼーション

Rustのジェネリクスはコンパイル時にモノモーフィゼーションが行われます。これは、コンパイラが実際に使われる型ごとに専用のコードを生成する仕組みです。

fn double<T: std::ops::Add<Output = T> + Copy>(x: T) -> T {
    x + x
}

fn main() {
    double(5_i32);   // i32用のコードが生成される
    double(2.5_f64); // f64用のコードが生成される
}

これにより、実行時のオーバーヘッドがなく、高速なコードが生成されます。動的ディスパッチ(dyn Trait)とは異なる点です。


よくあるミスと対処法

型に必要なトレイトを指定し忘れる

// コンパイルエラー:`T` に `Display` の境界がない
fn print_it<T>(value: T) {
    println!("{}", value); // エラー!
}

// 正しい
fn print_it<T: std::fmt::Display>(value: T) {
    println!("{}", value);
}

impl ブロックで型パラメータを書き忘れる

struct Container<T> {
    item: T,
}

// 間違い:impl Container<T> { ... }
// 正しい:
impl<T> Container<T> {
    fn get(&self) -> &T {
        &self.item
    }
}

まとめ

概念説明
型パラメータ <T>関数・構造体・enumなどを型に依存せず定義する
トレイト境界 T: Traitジェネリクスに制約を与える
複数境界 T: A + B複数のトレイトを同時に要求する
where複雑なトレイト境界を整理して書く
モノモーフィゼーションコンパイル時に型ごとのコードを生成・実行時コストなし

Rustのジェネリクスは最初は少し戸惑うかもしれませんが、<T>where 句の使い方さえ押さえれば、柔軟で再利用性の高いコードが書けるようになります。トレイトと組み合わせることで、Rustの型システムの力を最大限に引き出せます。ぜひ実際にコードを書きながら慣れていきましょう。

← 記事一覧に戻る