Rustのトレイト
トレイト(trait)は、型が実装できるふるまいのセットを定義するものです。ほかの言語のインターフェースに少し似ています。Rustでは、トレイトを使って「この型はXができる」ということを表します。ジェネリクスのページで見たトレイト境界も、このトレイトの仕組みで動いています。
トレイトを定義する
Section titled “トレイトを定義する”trait Speak { fn speak(&self) -> String;}これは「Speak を実装する型は、String を返す speak メソッドを必ず用意しなければならない」という意味です。トレイトはそれだけではただの約束で、どこかの型が実装するまでは何もしません。
トレイトを実装する
Section titled “トレイトを実装する”trait Speak { fn speak(&self) -> String;}
struct Dog;struct Cat;
impl Speak for Dog { fn speak(&self) -> String { String::from("ワン!") }}
impl Speak for Cat { fn speak(&self) -> String { String::from("ニャー!") }}
fn main() { let dog = Dog; let cat = Cat;
println!("{}", dog.speak()); println!("{}", cat.speak());}実行結果:
ワン!ニャー!Dog と Cat はまったく関係のない型ですが、どちらも Speak を実装しているので、どちらでも .speak() を呼べます。これは構造体のページで見た impl ブロックと同じパターンです。impl Speak for Dog は「Dog は Speak トレイトをこうやって満たします」と言っているだけです。
関数の引数にトレイトを使う
Section titled “関数の引数にトレイトを使う”トレイトの本当のうれしさはここからです。具体的にどの型なのかを気にせず、あるトレイトを実装しているどんな型でも受け取れる関数が書けます。
trait Speak { fn speak(&self) -> String;}
struct Dog;impl Speak for Dog { fn speak(&self) -> String { String::from("ワン!") }}
fn announce(animal: &impl Speak) { println!("動物の鳴き声: {}", animal.speak());}
fn main() { let dog = Dog; announce(&dog);}animal: &impl Speak は「Speak を実装している型なら何でもよい。どの型かは知らなくていい」という意味です。Speak を実装した Bird 型を追加しても、announce は何も変えずにそのまま使えます。
もう使ったことのあるトレイト
Section titled “もう使ったことのあるトレイト”構造体のページの #[derive(Debug)] を覚えていますか? Debug もトレイトです。derive は、本来なら手で書く実装を自動で生成してくれているだけなのです。ほかにもよく出会うものとして、Clone(.clone() のため)、PartialEq(== のため)、PartialOrd(< や > のため。ジェネリクスのページでトレイト境界として使いましたね)があります。
デフォルト実装
Section titled “デフォルト実装”トレイトはメソッドの本体をデフォルトとして用意しておくこともできます。実装する型は、それをそのまま使うことも、上書き(オーバーライド)することもできます。
trait Greet { fn name(&self) -> String;
fn greet(&self) -> String { format!("こんにちは、{}さん!", self.name()) }}Greet を実装する型は name() を定義するだけでよく、greet() は自動的についてきます。もちろん、greet() も自分で上書きしてかまいません。
トレイトには、このページで紹介した以上のことがあります。よく使うトレイトのページでは毎日使う標準のトレイトを順に見ていきます。高度なトレイトでは、大きめのコードベースで見かける関連型やスーパートレイトなどの道具を紹介します。
1. 面積を持つ図形
Section titled “1. 面積を持つ図形”メソッドを一つだけ持つトレイト Area(fn area(&self) -> f64)を定義してください。それを Rectangle(width と height を持つ)と Square(side を持つ)に実装します。3×4の長方形と、一辺が5の正方形の面積を表示してください。ヒント:impl Area for ... のブロックにはメソッドが一つあればよく、本体はかけ算一つだけです。
解答を見る
trait Area { fn area(&self) -> f64;}
struct Rectangle { width: f64, height: f64,}
struct Square { side: f64,}
impl Area for Rectangle { fn area(&self) -> f64 { self.width * self.height }}
impl Area for Square { fn area(&self) -> f64 { self.side * self.side }}
fn main() { let rect = Rectangle { width: 3.0, height: 4.0 }; let square = Square { side: 5.0 };
println!("長方形: {}", rect.area()); println!("正方形: {}", square.area());}実行結果:
長方形: 12正方形: 252. いろいろな型を受け取る一つの関数
Section titled “2. いろいろな型を受け取る一つの関数”fn describe(&self) -> String を持つトレイト Describe を定義してください。それを Book(title を持つ)と Movie(title と minutes を持つ)に実装します。そして説明を表示する関数 fn show(item: &impl Describe) を一つ書き、両方で呼び出してください。ヒント:説明の文字列は format! で組み立てます。
解答を見る
trait Describe { fn describe(&self) -> String;}
struct Book { title: String,}
struct Movie { title: String, minutes: u32,}
impl Describe for Book { fn describe(&self) -> String { format!("本: {}", self.title) }}
impl Describe for Movie { fn describe(&self) -> String { format!("映画: {}({}分)", self.title, self.minutes) }}
fn show(item: &impl Describe) { println!("{}", item.describe());}
fn main() { let book = Book { title: String::from("ホビットの冒険") }; let movie = Movie { title: String::from("カールじいさんの空飛ぶ家"), minutes: 96 };
show(&book); show(&movie);}実行結果:
本: ホビットの冒険映画: カールじいさんの空飛ぶ家(96分)3. デフォルトを使うか、上書きするか
Section titled “3. デフォルトを使うか、上書きするか”このページの、デフォルトの greet() メソッドを持つ Greet トレイトを使います。デフォルトをそのまま使う Person と、greet() を上書きして代わりに アホイ、...! とあいさつする Pirate(海賊)に実装してください。ヒント:どちらの型も name() は定義しなければなりません。自分の greet() も書くのは Pirate だけです。
解答を見る
trait Greet { fn name(&self) -> String;
fn greet(&self) -> String { format!("こんにちは、{}さん!", self.name()) }}
struct Person { name: String,}
struct Pirate { name: String,}
impl Greet for Person { fn name(&self) -> String { self.name.clone() }}
impl Greet for Pirate { fn name(&self) -> String { self.name.clone() }
fn greet(&self) -> String { format!("アホイ、{}!", self.name()) }}
fn main() { let ana = Person { name: String::from("アナ") }; let jack = Pirate { name: String::from("ジャック") };
println!("{}", ana.greet()); println!("{}", jack.greet());}実行結果:
こんにちは、アナさん!アホイ、ジャック!- トレイトは、型が実装できるふるまいを定義します。インターフェースに似ています。
impl TraitName for Typeで実装を用意します。- 引数の型に
&impl TraitNameと書くと、そのトレイトを実装したどんな型でも受け取れます。 - トレイトはメソッドのデフォルト実装を用意でき、実装する側はそれを使うことも上書きすることもできます。
Debug、Clone、PartialEqは標準ライブラリのよく使うトレイトで、#[derive(...)]で自動的に実装できます。
理解度チェック
Section titled “理解度チェック”理解度チェック
1. トレイトは何を定義しますか?
2. 引数の型としての animal: &impl Speak は何を意味しますか?
3. #[derive(Debug)] は実際には何をしていますか?
4. トレイトのデフォルト実装とは何ですか?
5. このページによると、構造体やenumではなくトレイトなのはどれですか?
スコア: 0 / 5
さらに詳しく
Section titled “さらに詳しく”- トレイト:共通の振る舞いを定義する — The Rust Programming Language 日本語版:トレイトの定義、デフォルトメソッド、
impl Trait引数、トレイト境界を扱う公式の章です。 - トレイト — Rust By Example 日本語版:トレイト、デフォルトメソッド、実装をまとめて試せる実行可能な例。
- 導出(derive) — Rust By Example 日本語版:
#[derive(...)]で自動実装できる標準トレイトの一覧と例。 - 高度なトレイト — The Rust Programming Language 日本語版:次のステップ。関連型、スーパートレイト、ニュータイプパターンを扱います。
- Traits — The Rust Reference(英語):トレイト定義の正確なルール。細かいところまで知りたいときに。