コンテンツにスキップ

Rustのトレイト

トレイト(trait)は、型が実装できるふるまいのセットを定義するものです。ほかの言語のインターフェースに少し似ています。Rustでは、トレイトを使って「この型はXができる」ということを表します。ジェネリクスのページで見たトレイト境界も、このトレイトの仕組みで動いています。

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

これは「Speak を実装する型は、String を返す speak メソッドを必ず用意しなければならない」という意味です。トレイトはそれだけではただの約束で、どこかの型が実装するまでは何もしません。

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 トレイトをこうやって満たします」と言っているだけです。

トレイトの本当のうれしさはここからです。具体的にどの型なのかを気にせず、あるトレイトを実装しているどんな型でも受け取れる関数が書けます。

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(< や > のため。ジェネリクスのページでトレイト境界として使いましたね)があります。

トレイトはメソッドの本体をデフォルトとして用意しておくこともできます。実装する型は、それをそのまま使うことも、上書き(オーバーライド)することもできます。

trait Greet {
fn name(&self) -> String;
fn greet(&self) -> String {
format!("こんにちは、{}さん!", self.name())
}
}

Greet を実装する型は name() を定義するだけでよく、greet() は自動的についてきます。もちろん、greet() も自分で上書きしてかまいません。

トレイトには、このページで紹介した以上のことがあります。よく使うトレイトのページでは毎日使う標準のトレイトを順に見ていきます。高度なトレイトでは、大きめのコードベースで見かける関連型やスーパートレイトなどの道具を紹介します。

メソッドを一つだけ持つトレイト 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
正方形: 25
▶ 実際に試してみよう

2. いろいろな型を受け取る一つの関数

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(...)] で自動的に実装できます。

理解度チェック

1. トレイトは何を定義しますか?

2. 引数の型としての animal: &impl Speak は何を意味しますか?

3. #[derive(Debug)] は実際には何をしていますか?

4. トレイトのデフォルト実装とは何ですか?

5. このページによると、構造体やenumではなくトレイトなのはどれですか?

スコア: 0 / 5