11. ジェネリクスとトレイト

🎯 この章のゴール

  • ジェネリクスで型に依存しないコードを書く
  • トレイトで「共通の振る舞い」を定義・実装する
  • トレイト境界で型に条件を付ける

💡 ひとことで言うと

ジェネリクスは 型の穴埋め問題、トレイトは 資格試験。「この関数は『比較できる』資格を持つ型ならなんでも受け付けます」と書けます。

ジェネリクス

#[derive(Debug)]
struct Point<T> {
    x: T,
    y: T,
}

impl<T> Point<T> {
    fn x(&self) -> &T {
        &self.x
    }
}

impl Point<f64> {                  // f64 のときだけ使えるメソッド
    fn dist(&self) -> f64 {
        (self.x * self.x + self.y * self.y).sqrt()
    }
}

fn main() {
    let a = Point { x: 5, y: 10 };       // Point<i32>
    let b = Point { x: 3.0, y: 4.0 };    // Point<f64>
    println!("{} {} {a:?}", a.x(), b.dist());
}
Playground で開く ↗

Option<T> や Vec<T>、Result<T, E> もジェネリクスで定義されています。

トレイト: 共通の振る舞い

trait Summary {
    fn author(&self) -> String;

    fn summarize(&self) -> String {          // デフォルト実装
        format!("{} さんの投稿を読む…", self.author())
    }
}

struct Tweet {
    user: String,
    text: String,
}

struct Article {
    title: String,
}

impl Summary for Tweet {
    fn author(&self) -> String {
        format!("@{}", self.user)
    }
    fn summarize(&self) -> String {          // 上書きもできる
        format!("{}: {}", self.author(), self.text)
    }
}

impl Summary for Article {
    fn author(&self) -> String {
        String::from("編集部")
    }
}

fn main() {
    let t = Tweet { user: "ferris".into(), text: "Rust 楽しい".into() };
    let a = Article { title: "所有権入門".into() };
    println!("{}", t.summarize());
    println!("{} / {}", a.title, a.summarize());
}
Playground で開く ↗

トレイト境界

use std::fmt::Display;

// 「PartialOrd(比較できる)かつ Copy な型」に限定
fn largest<T: PartialOrd + Copy>(list: &[T]) -> T {
    let mut max = list[0];
    for &item in list {
        if item > max {
            max = item;
        }
    }
    max
}

// impl Trait 記法(短く書ける)
fn show(item: &impl Display) {
    println!("【{item}】");
}

// where 句(境界が長いとき読みやすい)
fn pair_info<T, U>(t: T, u: U) -> String
where
    T: Display + Clone,
    U: std::fmt::Debug,
{
    format!("{t} / {u:?}")
}

fn main() {
    println!("{}", largest(&[3, 7, 2]));
    println!("{}", largest(&['a', 'z', 'm']));
    show(&42);
    println!("{}", pair_info("hi", vec![1, 2]));
}
Playground で開く ↗

よく使う標準トレイト

トレイトできることderive 可
Debug{:?} で表示✅
Display{} で表示❌ 手書き
Clone / Copy複製 / 暗黙コピー✅
PartialEq / Eq== で比較✅
PartialOrd / Ord< > で比較、ソート✅
HashHashMap のキーにする✅
DefaultT::default() で既定値✅
From / Into型変換❌

孤児ルール: トレイトか型のどちらかが自分のクレートのものでないと impl できません。impl Display for Vec<T> のように他人同士の組み合わせは不可です。

🧪 やってみよう

下の書きかけコードの TODO を埋めて、▶ 実行 で確かめよう。

Animal トレイトを Dog と Cat に実装して、「ポチ: ワン!」のように表示しよう。

trait Animal {
    fn name(&self) -> String;
    fn speak(&self) -> String;
}

struct Dog;
struct Cat;

// TODO: impl Animal for Dog { ... }
// TODO: impl Animal for Cat { ... }

fn intro(a: &impl Animal) {
    println!("{}: {}", a.name(), a.speak());
}

fn main() {
    // TODO: コメントを外そう
    // intro(&Dog);
    // intro(&Cat);
}
Playground で開く ↗
💡 解答例を見る
trait Animal {
    fn name(&self) -> String;
    fn speak(&self) -> String;
}

struct Dog;
struct Cat;

impl Animal for Dog {
    fn name(&self) -> String {
        String::from("ポチ")
    }

    fn speak(&self) -> String {
        String::from("ワン!")
    }
}

impl Animal for Cat {
    fn name(&self) -> String {
        String::from("タマ")
    }

    fn speak(&self) -> String {
        String::from("ニャー!")
    }
}

fn intro(a: &impl Animal) {
    println!("{}: {}", a.name(), a.speak());
}

fn main() {
    intro(&Dog);
    intro(&Cat);
}
Playground で開く ↗

📝 まとめ

  • ジェネリクスで型をパラメータ化。単相化でコストゼロ
  • トレイトで振る舞いを定義し、impl Trait for Type で実装
  • T: Trait / impl Trait / where で型に条件を付ける

✅ 理解度クイズ

Q1
次の関数はコンパイルできる?
fn largest<T>(list: &[T]) -> &T {
    let mut max = &list[0];
    for item in list {
        if item > max { max = item; }
    }
    max
}
Q2
トレイトを一言で説明すると?
Q3
fn notify(item: &impl Summary) と同じ意味になるのは?
Q4
ジェネリクスを使うと実行速度が遅くなる?