- Authors

- Name
- Youngju Kim
- @fjvbn20031
- はじめに — Rustの抽象化ツール
- トレイト — インターフェース、そしてそれ以上
- ジェネリクスとトレイト境界
- 静的ディスパッチ — ゼロコストの多相性
- 動的ディスパッチ — dynトレイトオブジェクト
- 静的 vs 動的 — 何をいつ
- 関連型 — トレイトに付属する型
- ブランケット実装 — 条件を満たすすべての型に
- derive — トレイトの自動実装
- 手で覚える
- おわりに
- 参考資料
はじめに — Rustの抽象化ツール
オブジェクト指向言語は、継承とインターフェースで抽象化を組み立てます。Rustにはクラス継承がありません。代わりに**トレイト(trait)とジェネリクス(generic)**という2つの軸から、コード再利用と多相性を得ます。この2つを理解すると、Rustライブラリのシグネチャが急に読めるようになります。T: Display + Cloneという表記が何を意味するのか、impl IteratorとBox<dyn Iterator>がなぜ違うのか、といったことです。
核心となる直観はこうです。トレイトは「この型が何をできるか」を定義し、ジェネリクスは「その能力を要求するコード」を書く。 そして、その要求を実行する方法が2つある(静的ディスパッチ / 動的ディスパッチ)ことが、この記事のハイライトです。1つずつ積み上げましょう。
トレイト — インターフェース、そしてそれ以上
トレイトは、型が実装できるメソッドの集合です。ここまではJava・C#のインターフェースや、Goのインターフェースに似ています。
trait Summary {
fn summarize(&self) -> String; // シグネチャのみ(本体なし)
fn preview(&self) -> String { // デフォルト実装を提供
format!("{}...", &self.summarize()[..5.min(self.summarize().len())])
}
}
struct Article {
title: String,
body: String,
}
impl Summary for Article { // ArticleにSummaryを実装
fn summarize(&self) -> String {
format!("{}: {}", self.title, self.body)
}
// previewはデフォルト実装をそのまま使う
}
trait Summaryが契約を定義し、impl Summary for ArticleがArticleにそれを満たさせます。デフォルト実装(preview)を提供できる点は、素のインターフェースより一歩進んだ部分です(Javaのdefaultメソッドに似ています)。
インターフェースとの決定的な違いは2つです。
1つ目、型を定義する場所とトレイトを実装する場所が分離しています。 Javaではクラスを宣言するときにimplements Comparableを前もって書く必要があります。Rustでは、すでに存在する型(自分が作っていない型を含む)に後からトレイトを実装できます。標準ライブラリのi32に自分のトレイトを実装することも可能です。
2つ目、その自由には**孤児ルール(orphan rule)**という制約が付きます。トレイトを実装するには、トレイトか型のどちらか一方が自分のクレート所有でなければなりません。他人のトレイトを他人の型に実装するのは禁止です。理由は一貫性です。もし2つのクレートがそれぞれDisplayをVecに実装したら、どちらを使うべきか衝突するからです。このルールが「1つの(トレイト, 型)の組に実装は最大1つ」を保証します。
ジェネリクスとトレイト境界
さて、トレイトを要求するコードを書きます。ジェネリック関数は「どんな型Tでも受け取る」と書きますが、たいていTに特定の能力を求めます。その要求が**トレイト境界(trait bound)**です。
use std::fmt::Display;
// TはDisplayを実装するどんな型でもよい
fn announce<T: Display>(item: T) {
println!("お知らせ: {item}"); // Displayのおかげで{}フォーマットが可能
}
fn main() {
announce(42); // i32はDisplayを実装
announce("hello"); // &strもDisplayを実装
announce(3.14); // f64も
}
<T: Display>が「TはかならずDisplayを実装しなければならない」という境界です。これがないと、announceの本体で{item}として出力できません(コンパイラが「TがDisplayか分からない」と拒否します)。境界は、ジェネリックコードがTについて何を仮定してよいかをコンパイラに(そして読者に)伝えます。
複数の境界は+でつなぎ、多くなればwhere節できれいに外に出します。
use std::fmt::{Debug, Display};
// インライン境界 — 短いとき
fn show<T: Display + Clone>(x: T) { /* ... */ }
// where節 — 境界が多いと読みやすい
fn process<T, U>(t: T, u: U) -> String
where
T: Display + Clone,
U: Debug + Default,
{
format!("{t} {u:?}")
}
ここでJavaジェネリクスとの根本的な違いを1つ。Javaジェネリクスは型消去(type erasure)で、実行時には型情報が消され、境界はたいていキャストで処理されます。Rustジェネリクスは単相化(monomorphization)されます。コンパイラがannounce(42)とannounce("hello")のために、それぞれi32用、&str用の関数を別々に生成します。だからジェネリックなのに実行時コストがゼロです。これが次節の「静的ディスパッチ」です。
静的ディスパッチ — ゼロコストの多相性
単相化の結果、ジェネリック関数の呼び出しは静的ディスパッチ(static dispatch)されます。どの実装を呼ぶかがコンパイル時に決まり、各型に特化したコードが生成され、関数はインライン化されることもあります。実行時に「どのメソッドだ?」を探すコストがありません。
トレイトを引数に取る慣用的な方法はimpl Trait構文です。これはジェネリクスの糖衣構文です。
use std::fmt::Display;
// 下の2つは実質同じ(impl Traitはジェネリクスの省略形)
fn v1(item: impl Display) { println!("{item}"); }
fn v2<T: Display>(item: T) { println!("{item}"); }
返り値の位置でもimpl Traitを使えます。これは特に便利です。イテレータやクロージャのように名前を付けにくい具体型を返すとき、「このトレイトを満たす何らかの具体型」とだけ表明すればよいのです。
// 返り型の本当の名前は複雑なMap<...>だが、隠せる
fn evens(max: u32) -> impl Iterator<Item = u32> {
(0..max).filter(|n| n % 2 == 0)
}
fn main() {
for n in evens(10) {
print!("{n} "); // 0 2 4 6 8
}
}
静的ディスパッチのトレードオフはコードサイズです。単相化は型ごとに別々のコードを刻むので、バイナリが大きくなりえます(コード膨張、code bloat)。しかし速度が最優先の大半の場合、このトレードオフは割に合います。
動的ディスパッチ — dynトレイトオブジェクト
ところが、異なる型を1つのコレクションに混ぜて入れたいときがあります。「描画可能な図形のリスト」のように。円、正方形、三角形がすべてDrawトレイトを実装していても、ジェネリックなVec<T>は1つの具体型しか入れられません。こういうときに必要なのがトレイトオブジェクト(trait object)、構文ではdyn Traitです。
trait Draw {
fn draw(&self) -> String;
}
struct Circle;
struct Square;
impl Draw for Circle { fn draw(&self) -> String { "○".into() } }
impl Draw for Square { fn draw(&self) -> String { "□".into() } }
fn main() {
// 異なる型を1つのVecに!(ボックスに入れてトレイトオブジェクトとして)
let shapes: Vec<Box<dyn Draw>> = vec![
Box::new(Circle),
Box::new(Square),
Box::new(Circle),
];
for s in &shapes {
print!("{} ", s.draw()); // 実行時に実際の型のdrawを呼ぶ
}
}
Box<dyn Draw>は「ヒープにある、Drawを実装する何らかの型」です。ここでs.draw()を呼ぶと**動的ディスパッチ(dynamic dispatch)**が起きます。コンパイル時にはsの本当の型が分からないので、実行時に決まります。
仕組みは**仮想関数テーブル(vtable)**です。Box<dyn Draw>は実は2つのポインタです。1つは実データを指し(データポインタ)、もう1つはその型のDrawメソッド実装のアドレスが入ったvtableを指します。s.draw()はvtableからdrawのアドレスを探してジャンプします。C++の仮想関数と同じメカニズムです。だからdyn Traitを「太ったポインタ(fat pointer)」と呼びます。普通のポインタの2倍のサイズだからです。
静的 vs 動的 — 何をいつ
2つのディスパッチを並べると、トレードオフが明確になります。
- 静的ディスパッチ(ジェネリクス /
impl Trait): コンパイル時に解決。インライン・最適化がよく効く。実行時オーバーヘッドがゼロ。代わりに型ごとにコードが生成されてバイナリが大きくなりえ、異種コレクションを入れられない。 - 動的ディスパッチ(
dyn Trait): 実行時にvtableで解決。異種の型を1つのコレクションに入れられ、バイナリが小さい(実装は1つだけ)。代わりに呼び出しごとにvtable参照のコスト(間接呼び出し、インライン困難)がある。
実用的な結論はこうです。基本は静的ディスパッチを使いましょう。 大半の場合、その方が速く自然です。動的ディスパッチは、異種コレクションが必要なとき(上の図形リスト)、コード膨張を避けたいとき、プラグインのように実行時にはじめて型が決まるときに使います。大半のアプリケーションでvtable参照のコストは無視できるので、性能のためにdynを避ける必要はまずありません。表現力とコード構造を見て選べばよいのです。
1つ制約があります。すべてのトレイトがトレイトオブジェクトになれるわけではありません。**オブジェクト安全(object-safe)**のルールを満たす必要があります(おおよそ、メソッドがジェネリックでなく、Selfを値で返さないこと)。このルールを破ったトレイトをdynで使うと、コンパイラが理由とともに教えてくれます。
関連型 — トレイトに付属する型
トレイトはメソッドだけでなく、**関連型(associated type)**も持てます。代表例が標準のIteratorです。
trait Iterator {
type Item; // 関連型: このイテレータが出すものの型
fn next(&mut self) -> Option<Self::Item>;
}
struct Counter { count: u32 }
impl Iterator for Counter {
type Item = u32; // ここでItemをu32に確定
fn next(&mut self) -> Option<u32> {
if self.count < 3 {
self.count += 1;
Some(self.count)
} else {
None
}
}
}
type Itemはトレイトが要求する「空欄の型」で、実装するときにtype Item = u32で埋めます。するとnextがOption<u32>を返すようになります。
ここで自然な疑問。「これ、ただのジェネリックIterator<Item>ではダメなのか?」できますが、違いがあります。関連型は1つの型に対して実装を1つに固定します。CounterはItem = u32のイテレータとして、ちょうど1つだけになれます。一方ジェネリックパラメータだったら、CounterがIterator<u32>でありながら同時にIterator<String>にもなれてしまい、型推論が曖昧になります。「この型に対してこの関係は一意だ」を表現するときは関連型を、「複数の関係がありうる」を表現するときはジェネリックパラメータを使います。イテレータが出す要素の型は1つだけなのが自然なので、関連型が正解です。
ブランケット実装 — 条件を満たすすべての型に
トレイトの強力さが爆発する地点がブランケット実装(blanket implementation)です。「あるトレイトを実装するすべての型に対して、別のトレイトを自動で実装する」ものです。
標準ライブラリの実例が圧巻です。ToStringトレイト(.to_string()メソッド)はこう定義されています。
// 標準ライブラリの実際(単純化)
impl<T: Display> ToString for T {
fn to_string(&self) -> String {
// Displayのフォーマット機能で文字列を生成
format!("{self}")
}
}
この1つの断片がすることを味わってみてください。「Displayを実装するすべての型Tは、自動的にToStringも実装する」。だからあなたが新しい型にDisplayだけ実装すれば、.to_string()がタダで付いてきます。誰も型ごとにToStringを手で実装しません。条件付きのブランケット実装1つが、エコシステム全体を覆うのです。
ブランケット実装は「能力の組み合わせ」を表現する慣用句でもあります。impl<T: A + B> C for Tは「AとBを両方備えた型はCにもなる」というルールを宣言します。トレイト・ジェネリクス・ブランケット実装がかみ合うと、継承階層なしで非常に表現力の高い抽象化が組み上がります。
derive — トレイトの自動実装
最後に、実戦で毎日使う便利機能です。多くの標準トレイトは、**#[derive(...)]**アトリビュートで自動実装できます。コンパイラがフィールド構造を見て、当たり前の実装を代わりに生成します。
#[derive(Debug, Clone, PartialEq)]
struct Point {
x: i32,
y: i32,
}
fn main() {
let p1 = Point { x: 1, y: 2 };
let p2 = p1.clone(); // Cloneのおかげ
println!("{p1:?}"); // Debugのおかげ: Point { x: 1, y: 2 }
println!("{}", p1 == p2); // PartialEqのおかげ: true
}
#[derive(Debug, Clone, PartialEq)]の1行が、Debug(デバッグ出力)、Clone(複製)、PartialEq(等価比較)の3つのトレイトの実装を自動生成します。手で書けば数十行になるボイラープレートを、コンパイラがフィールド単位で再帰的に生成します。よく導出するトレイトはDebug、Clone、Copy、PartialEq、Eq、Hash、Default、PartialOrd、Ordなどです。
deriveの仕組みは手続き的マクロ(procedural macro)です。つまりこれは魔法ではなく、コンパイル時にコードを生成するマクロにすぎません。だから自分で作ったトレイトにも導出マクロを付けられ(例: serdeの#[derive(Serialize)])、これがRustエコシステムの使いやすさを支える大きな柱になっています。
手で覚える
トレイト境界、静的/動的ディスパッチ、関連型は、シグネチャを読む感覚が付いてこそ楽になります。Rust学習ラボでトレイトを定義してジェネリック関数に境界を掛け、同じコードをimpl Trait(静的)とBox<dyn Trait>(動的)でそれぞれ書いて違いを自分で作ってみると、ライブラリのドキュメントの複雑なシグネチャがずっと楽に読めるようになります。
おわりに
Rustは継承なしで豊かな抽象化を提供します。トレイトが能力を定義し、ジェネリクスとトレイト境界がその能力を要求し、静的ディスパッチがゼロコストでそれを実行します。異なる型をひとまとめにする必要があるときは、dynトレイトオブジェクトがvtableを通じた動的ディスパッチで柔軟さをくれます。関連型がトレイトに付属する型の関係を表現し、ブランケット実装が条件を満たすすべての型を一度に覆い、deriveが当たり前の実装をタダで作ってくれます。
これらの断片がどうかみ合うかを理解した瞬間、Rustコードのシグネチャがノイズではなく情報として読めるようになります。fn foo<T: Trait>(...)を見れば「この関数はTにこの能力を要求しているな」、Box<dyn Trait>を見れば「ここでは異種の型を実行時ディスパッチで扱っているな」がすぐ分かります。それがRustの抽象化を手に入れた合図です。
参考資料
- The Rust Programming Language, 10.2 "Traits: Defining Shared Behavior": https://doc.rust-lang.org/book/ch10-02-traits.html
- The Rust Programming Language, 18.2 "Using Trait Objects": https://doc.rust-lang.org/book/ch18-02-trait-objects.html
- Rust Reference — Traits: https://doc.rust-lang.org/reference/items/traits.html
- Rust by Example — Generics: https://doc.rust-lang.org/rust-by-example/generics.html
- The Rust Reference — Object safety (dyn compatibility): https://doc.rust-lang.org/reference/items/traits.html#object-safety