Trait
Rust の型に共通の振る舞いを定義するインターフェース機構
Trait とは
Trait は Rust の型に共通の振る舞い (メソッド) を定義するインターフェース機構である。Rust に型の継承はなく、共通の振る舞いは Trait の定義と impl ブロックによる実装の組み合わせで表す。The Rust Reference が挙げる言語の影響元一覧には Haskell (GHC) の型クラスが含まれており、Java や TypeScript の interface よりも型クラスに近い性質 (デフォルト実装、関連型、既存の型への後付け実装) を持つ。
実装が型の定義から切り離されている点が要点である。impl Summary for Article のように「型と Trait の組」ごとに実装を書くので、自分が定義していない型にも自分の Trait を後から実装できる。この自由度に歯止めをかけるのが後述の孤児ルールで、言語全体の位置づけは Rust、型パラメーター側の一般論は ジェネリクス にまとめている。
デフォルト実装
Trait 本体にはメソッドのシグネチャだけを並べる。本体まで書くとデフォルト実装になり、実装側で同名のメソッドを定義すれば置き換わる。
trait Summary {
fn summarize(&self) -> String;
// 本体を書くとデフォルト実装になる (実装側でオーバーライドできる)
fn preview(&self) -> String {
let s = self.summarize();
let head: String = s.chars().take(50).collect();
if head.len() < s.len() { format!("{head}...") } else { head }
}
}
struct Article { title: String, content: String }
impl Summary for Article {
fn summarize(&self) -> String {
let head: String = self.content.chars().take(100).collect();
format!("{}: {}", self.title, head)
}
}
struct Tweet { username: String, text: String }
impl Summary for Tweet {
fn summarize(&self) -> String {
format!("@{}: {}", self.username, self.text)
}
// デフォルト実装を差し替える
fn preview(&self) -> String {
format!("@{}", self.username)
}
}
デフォルト実装から Trait 内の他のメソッドを呼べるので、preview は切り詰め方だけを持ち、内容の作り方は各型の summarize に任せられる。ここで文字列を切るときに &s[..50] のようなバイト添字のスライスを使うと、長さが 50 バイトに届かない場合と添字が文字境界に落ちない場合に panic する。日本語は 1 文字が 3 バイトなので後者を踏みやすく、&"日本語"[..2] は「byte index 2 is not a char boundary」で停止する。文字数で切りたいなら chars().take(n) を使う。
Trait 境界 (ジェネリクスの制約)
型パラメーターに「この Trait を実装している型だけ」という条件を付けるのが Trait 境界である。関数本体から呼べるのは境界に書いた Trait のメソッドに限られるので、境界は制約であると同時に、その関数が何に依存しているかの宣言でもある。
use std::fmt::Display;
// 引数位置の impl Trait: 匿名の型パラメータとして働く
fn notify(item: &impl Summary) {
println!("新着: {}", item.summarize());
}
// where 句なら複数の境界を読みやすく並べられる
fn describe<T>(item: &T) -> String
where
T: Summary + Display,
{
format!("{item} / {}", item.summarize())
}
impl Trait は書く位置で意味が変わる。引数位置の impl Trait は Reference が「匿名の型パラメータ」と説明する糖衣構文で、<T: Trait> とほぼ同じだが型に名前が付かないため、2 つの引数に同じ型を要求する書き方はできない。戻り値位置の impl Trait は抽象戻り型で、具体型を決めるのは関数側であり呼び出し側は選べない。
// 戻り値位置: 関数が具体型を 1 つ決め、呼び出し側からは Trait のメソッドだけが見える
fn make_tweet(username: &str) -> impl Summary {
Tweet { username: username.to_string(), text: String::new() }
}
したがって条件分岐で Article と Tweet を返し分けることはできない。返す型が実行時に変わるなら Box<dyn Summary> にする。
静的ディスパッチと動的ディスパッチ
型パラメーター経由の呼び出しは単相化 (monomorphization) される。使われた型ごとに関数の実体が複製され、呼び出し先がコンパイル時に確定するのでインライン化も効く。代償はコード量で、多くの型に対して使うジェネリック関数はコンパイル時間とバイナリを膨らませる。
dyn Trait を使うと実体は 1 つのままで、データへのポインターと vtable (メソッドのアドレス表) へのポインターを組にした値を通じ、実行時に呼び出し先が決まる。間接呼び出しが 1 段入りインライン化は原則できないが、異なる型を同じコレクションに混ぜて持てる。
// 静的ディスパッチ: T ごとに実体が作られる
fn print_static<T: Summary>(item: &T) { println!("{}", item.preview()); }
// 動的ディスパッチ: vtable 経由で呼び出し先が決まる
fn print_dynamic(item: &dyn Summary) { println!("{}", item.preview()); }
// 型を混ぜて保持できるのは dyn 側だけ
let feed: Vec<Box<dyn Summary>> = vec![Box::new(article), Box::new(tweet)];
判断は単純で、静的ディスパッチを既定にし、型を混ぜて持つ必要が出た時点、またはコンパイル時間とバイナリ量が問題になった時点で dyn へ切り替える。呼び出し 1 回の差が問題になる場面は、内側のループで毎回呼ぶような箇所に限られる。
dyn 互換性 (旧称 オブジェクト安全性)
すべての Trait を dyn Trait にできるわけではない。条件は vtable を組めることで、2026 年 8 月時点の The Rust Reference はこの条件を dyn 互換性 (dyn compatibility) と呼び、以前の呼称であるオブジェクト安全性 (object safety) を旧称として注記している。コンパイラーの診断も E0038 で「is not dyn compatible」と伝える形になっている。Reference が挙げる主な条件は次のとおり。
- 上位 Trait もすべて dyn 互換であること。
Self: Sizedを要求しないこと - 関連定数を持たないこと。ジェネリクスを伴う関連型を持たないこと
- vtable から呼ぶメソッドは型パラメーターを持たず (ライフタイム引数は可)、レシーバーの型以外に
Selfを使わないこと - レシーバーが
&self/&mut self/Box<Self>/Rc<Self>/Arc<Self>/Pin<P>のいずれかであること - 戻り型が不透明でないこと (
async fnと戻り値位置のimpl Traitは該当するため不可)
ジェネリックなメソッドを持たせたい Trait でも、そのメソッドに where Self: Sized を付ければ vtable から除外され、Trait 全体は dyn 互換を保てる。標準ライブラリの Iterator が map などを持ちながら dyn Iterator として使えるのはこの仕組みによる。
孤児ルール
外部クレートの型に外部クレートの Trait を実装することはできない。型と Trait の少なくとも一方が自分のクレートにある必要があり、これを孤児ルールと呼ぶ。同じ組み合わせの実装が複数のクレートから現れて衝突するのを防ぐための規則である。回避したいときはタプル構造体で包む newtype パターンを使う。
// 外部の型に外部の Trait を実装したい場合は、自分のクレートの型で包む
struct Lines(Vec<String>);
impl Summary for Lines {
fn summarize(&self) -> String { self.0.join(" / ") }
}
TypeScript の interface との比較
| 観点 | Rust Trait | TypeScript interface |
|---|---|---|
| デフォルト実装 | あり | なし (抽象クラスで代替) |
| 実装の宣言 | impl Trait for 型 と明示して書く | 構造的部分型 (形が合えば実装とみなされる) |
| 孤児ルール | 外部の型に外部の Trait は実装できない | 該当する制限なし |
| 静的ディスパッチ | 単相化で呼び出し先が確定する | 型が実行時に消えるため該当しない |
| 動的ディスパッチ | dyn Trait (vtable) | 常に実行時の解決 |
| 関連型 | あり (type Output) | なし |
名前が似ていても成り立ちは違う。TypeScript は形が合致すれば実装とみなす一方、Rust は impl を書いた組み合わせだけが実装になるため、意図しない一致が起こらない。TypeScript 側の詳細は TypeScript を参照。
よく使う標準ライブラリの Trait
| Trait | 用途 | 例 |
|---|---|---|
Display | 人間向けの文字列表現 | println!("{}", value) |
Debug | デバッグ用の文字列表現 | println!("{:?}", value) |
Clone | 明示的な複製 | value.clone() |
Copy | 代入で複製される型の印 | 整数や f64 |
Iterator | next を実装して反復処理を与える | iter.map(...) |
IntoIterator | for 式で回せるようにする | for item in collection |
From / Into | 型変換 | String::from("hello") |
変換は From だけ実装すればよい。標準ライブラリに U: From<T> なら T: Into<U> という包括実装があるため、Into は自動的に付いてくる。逆向きに Into を手で実装すると、この包括実装と衝突する形になりやすい。
derive マクロ
#[derive(...)] は Trait の実装を機械的に生成する。Debug や Clone はコンパイラー組み込みの派生で、クレートが派生マクロを提供する例もある (serde の Serialize / Deserialize は derive フィーチャを有効にして使う)。
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
struct User {
id: String,
name: String,
}
AWS Lambda を Rust で書く場合も、イベントとレスポンスの構造体に serde の派生を付けて JSON との相互変換を任せるのが定型になる。派生には条件があり、標準ライブラリの派生は全フィールドが同じ Trait を実装していることを前提に展開されるため、1 つでも Clone でないフィールドがあれば #[derive(Clone)] は通らない。ジェネリックな型では型パラメーターにも同じ境界が付いた実装が生成されるので、必要より厳しい境界になることがある。その場合は手で impl を書く。
Trait を設計するときに迷いやすいのは境界の置き場所である。関数側の境界を厚くすると呼び出せる型が狭まり、Trait 側にメソッドを足すと実装者全員に作業が回る。まずは具体型で動くコードを書き、同じ処理を 2 つ目の型へ広げる段階で共通部分を Trait に切り出すと、境界が過剰になりにくい。
この記事は役に立ちましたか?
関連用語
Rust
メモリ安全性とパフォーマンスを両立する、GC なしのシステムプログラミング言語
ジェネリクス
型をパラメーター化し、型安全性を保ちながら汎用的なコードを書く仕組み
TypeScript
JavaScript に型を書く手段を足した言語。構造的型付け、型が実行時に残らない仕組み、消せない構文と型定義ファイルの扱いを解説
Null Object パターン
null チェックの代わりに、何もしないオブジェクトを使って条件分岐を排除するデザインパターン
インターフェース分離の原則
SOLID の I - クライアントが使わないメソッドへの依存を強制しない原則
リスコフの置換原則
SOLID の L - サブタイプはスーパータイプと置換可能でなければならない原則
関連する記事
コードを書かない日に読む本
休日や有給休暇など、コードを書かない日にこそ読むべき本があります。実装から離れた日に読むと効果が高い本のジャンルと、その理由を解説します。
LLM / 生成 AI 本ガイド - 仕組み / 実装 / エージェント開発の 3 層で選ぶ (2026 年 8 月時点)
LLM (大規模言語モデル) / 生成 AI を学ぶ技術書の選び方を 3 層 (読み物で仕組みを掴む → 中身を実装で理解する → RAG / AI エージェント開発) で整理。2026 年 8 月時点の定番書と、変化の速い分野で本を選ぶときの注意点を解説します。
ゼロから作る Deep Learning シリーズの読む順番 - 全 6 巻の内容と選び方を整理
ゼロから作る Deep Learning シリーズ全 6 巻 (基礎 / 自然言語処理 / フレームワーク / 強化学習 / 生成モデル / LLM) の読む順番を解説。各巻の内容 / 発売年 / 前提知識を一覧表で整理し、目的別にどの巻から読むべきかを案内します。