Progrust Library.

// book chapter

動かして学ぶRustプログラミング問題集

第18章 トレイト.

第17章で、型引数<T>を使って「どんな型でも受け取れる」関数や構造体を書けるようになりました。ただ、ジェネリックな関数の中でできることは、まだ「値を受け取って、そのまま返す」程度に限られています。Tがどんな型か分からない以上、そのメソッドを呼ぶことも、比較することもできないからです。

この章では、その穴を埋める道具であるトレイトを7問で身につけます。トレイトは「この型はこういう振る舞いを持つ」という約束を書いたもので、trait宣言でメソッドのシグネチャを並べ、impl トレイト名 for 型名で型ごとに中身を実装します。同じトレイトを実装した型は、型が違っても同じメソッド名で同じように呼び出せます。

後半ではデフォルト実装を扱います。トレイト側にあらかじめ処理を書いておくと、実装する側は「その型にしか書けない部分」だけを書けばよくなります。標準ライブラリの主要なトレイトもこの形で整理されていて、次の第19章で扱う#[derive(...)]や、第20章のトレイト境界(型引数に「このトレイトを実装していること」を要求する仕組み)へとつながっていきます。

進め方は第17章までと同じです。各問題の冒頭に関連する辞書へのリンクを挙げているので、まずはリンク先で必要な知識を確認してから取り組んでください。

01 - トレイトを定義する

トレイトとimplブロックに関する問題です。
「鳴き声を返す」という振る舞いを表すトレイトSpeakを定義し、構造体Dogに実装してください。

期待する出力
ポチ「ワン!」
「Playgroundで開く」をクリックして修正・実行してください
// メソッド fn speak(&self) -> String を持つトレイトSpeakを定義せよ

struct Dog {
    name: String,
}

// DogにSpeakを実装せよ
// speakは「名前「ワン!」」の形の文字列を返すこと

fn main() {
    let dog = Dog {
        name: String::from("ポチ"),
    };
    println!("{}", dog.speak());
}
Playgroundで開く
解答例と解説
// メソッド fn speak(&self) -> String を持つトレイトSpeakを定義せよ
trait Speak { 
    fn speak(&self) -> String; 
} 

struct Dog {
    name: String,
}

// DogにSpeakを実装せよ
// speakは「名前「ワン!」」の形の文字列を返すこと
impl Speak for Dog { 
    fn speak(&self) -> String { 
        format!("{}「ワン!」", self.name) 
    } 
} 

fn main() {
    let dog = Dog {
        name: String::from("ポチ"),
    };
    println!("{}", dog.speak());
}
Playgroundで開く

修正前のコードはno method named speakfound for structDog in the current scope(エラー: E0599)になります。第10章で見たのと同じ「そんなメソッドはない」というエラーです。

トレイトは、「この型はこういう振る舞いを持つ」という約束を書く仕組みです。書き方は2段階に分かれます。

1. trait宣言で約束の内容を決める
trait Speak { ... }の中に、求めるメソッドのシグネチャを並べます。fn speak(&self) -> String;のように、本体の{ ... }を書かずに;で終えるのが特徴です。「speakというメソッドがあって、&selfを受け取ってStringを返す」という形だけを決めていて、中身はここでは書きません。

2. impl トレイト名 for 型名で約束を果たす
impl Speak for Dog { ... }の中に、シグネチャどおりのメソッドを本体付きで書きます。ここは第10章のimplブロックとほぼ同じ見た目ですが、impl Dog { ... }が「Dog固有のメソッドを追加する」だったのに対し、impl Speak for Dog { ... }は「DogがSpeakの約束を果たす」という意味になります。

実装してしまえば、呼び出し方は普通のメソッドと変わりません。dog.speak()と書くだけです。

02 - 実装が足りない

トレイトに関する問題です。
次のコードはコンパイルエラーになります。エラーメッセージを読んで、原因を取り除いてください。

期待する出力
正方形の面積: 9
「Playgroundで開く」をクリックして修正・実行してください
trait Shape {
    fn name(&self) -> String;
    fn area(&self) -> u32;
}

struct Square {
    side: u32,
}

impl Shape for Square {
    fn area(&self) -> u32 {
        self.side * self.side
    }
}

fn main() {
    let square = Square { side: 3 };
    println!("{}の面積: {}", square.name(), square.area());
}
Playgroundで開く
解答例と解説
trait Shape {
    fn name(&self) -> String;
    fn area(&self) -> u32;
}

struct Square {
    side: u32,
}

impl Shape for Square {
    fn name(&self) -> String { 
        String::from("正方形") 
    } 

    fn area(&self) -> u32 {
        self.side * self.side
    }
}

fn main() {
    let square = Square { side: 3 };
    println!("{}の面積: {}", square.name(), square.area());
}
Playgroundで開く

エラーメッセージはnot all trait items implemented, missing: name``(エラー: E0046)です。「トレイトの項目がすべて実装されていない。nameが足りない」と、何が足りないのかまで教えてくれています。

trait Shapeはnameとareaの2つのメソッドを要求しています。impl Shape for Squareはareaしか書いていないので、「約束を果たしていない」と判定されます。;で終わる本体なしのメソッドは必須メソッドで、実装側は1つ残らず定義しなければなりません。

逆に、トレイトで宣言していないメソッドをimpl Shape for Squareの中に勝手に追加することもできません(エラー: E0407)。トレイトの実装ブロックに書けるのは、トレイトが宣言した項目だけです。Square固有のメソッドを足したいときは、別にimpl Square { ... }を用意します。

03 - 複数の型に同じトレイトを実装する

トレイトと構造体に関する問題です。
BookにはDescribeトレイトが実装済みです。Penにも同じトレイトを実装して、どちらの値でもdescribeメソッドを呼べるようにしてください。

期待する出力
書籍『Rust入門』 3200円
ボールペン(黒) 150円
「Playgroundで開く」をクリックして修正・実行してください
trait Describe {
    fn describe(&self) -> String;
}

struct Book {
    title: String,
    price: u32,
}

impl Describe for Book {
    fn describe(&self) -> String {
        format!("書籍『{}』 {}円", self.title, self.price)
    }
}

struct Pen {
    color: String,
    price: u32,
}

// PenにDescribeを実装せよ
// describeは「ボールペン(色) 価格円」の形の文字列を返すこと

fn main() {
    let book = Book {
        title: String::from("Rust入門"),
        price: 3200,
    };
    let pen = Pen {
        color: String::from("黒"),
        price: 150,
    };
    println!("{}", book.describe());
    println!("{}", pen.describe());
}
Playgroundで開く
解答例と解説
trait Describe {
    fn describe(&self) -> String;
}

struct Book {
    title: String,
    price: u32,
}

impl Describe for Book {
    fn describe(&self) -> String {
        format!("書籍『{}』 {}円", self.title, self.price)
    }
}

struct Pen {
    color: String,
    price: u32,
}

// PenにDescribeを実装せよ
// describeは「ボールペン(色) 価格円」の形の文字列を返すこと
impl Describe for Pen { 
    fn describe(&self) -> String { 
        format!("ボールペン({}) {}円", self.color, self.price) 
    } 
} 

fn main() {
    let book = Book {
        title: String::from("Rust入門"),
        price: 3200,
    };
    let pen = Pen {
        color: String::from("黒"),
        price: 150,
    };
    println!("{}", book.describe());
    println!("{}", pen.describe());
}
Playgroundで開く

1つのトレイトは、いくつの型にでも実装できます。impl Describe for Bookとimpl Describe for Penを並べて書けば、BookとPenはまったく別の構造体なのに、どちらも.describe()という同じ呼び出し方で説明文を返せるようになります。

ここで注目したいのは、BookとPenのdescribeの中身がまったく違うことです。Bookはtitleを、Penはcolorを使っていて、フィールドの構成も共通していません。それでもトレイトが決めているのは「describeを呼べばStringが返る」というシグネチャだけなので、中身は型ごとに自由に書けます。

この「呼び方は同じ、中身は型ごと」という性質が、トレイトの本質です。mainのように値ごとにメソッドを呼ぶ場面では、呼び出す側は相手がBookかPenかを意識せず、「Describeを実装している何か」として扱えます。

04 - デフォルト実装

デフォルト実装とトレイトに関する問題です。
次のコードはコンパイルエラーになります。implブロックには手を加えず、トレイト側を修正して動くようにしてください。

期待する出力
こんにちは
こんにちは
「Playgroundで開く」をクリックして修正・実行してください
trait Greet {
    // 「こんにちは」を返す既定の処理をここに書け
    fn greet(&self) -> String;
}

struct Staff;
struct Customer;

impl Greet for Staff {}
impl Greet for Customer {}

fn main() {
    println!("{}", Staff.greet());
    println!("{}", Customer.greet());
}
Playgroundで開く
解答例と解説
trait Greet {
    // 「こんにちは」を返す既定の処理をここに書け
    fn greet(&self) -> String; 
    fn greet(&self) -> String { 
        String::from("こんにちは") 
    } 
}

struct Staff;
struct Customer;

impl Greet for Staff {}
impl Greet for Customer {}

fn main() {
    println!("{}", Staff.greet());
    println!("{}", Customer.greet());
}
Playgroundで開く

エラーは問題02と同じnot all trait items implemented, missing: greet``(エラー: E0046)です。impl Greet for Staff {}とimpl Greet for Customer {}はどちらも中身が空なので、必須メソッドのgreetが足りないと指摘されています。

問題02ではimpl側にメソッドを足して解決しましたが、今回は別の方法を使います。トレイト宣言の中で、シグネチャだけでなく本体まで書いてしまうのです。これをデフォルト実装(デフォルト実装)と呼びます。

本体を持つメソッドは必須ではなくなり、実装側が書かなければトレイト側の本体がそのまま使われます。StaffもCustomerもgreetを書いていないので、どちらも「こんにちは」が返ります。すべてのメソッドにデフォルト実装があるなら、今回のように中身が空のimplブロックでトレイトを実装できます。

「必須メソッド」と「デフォルト実装付きメソッド」の違いは、本体があるかどうかだけです。

トレイト側の書き方 実装側の扱い
fn greet(&self) -> String; 必ず定義する(書かないとE0046)
fn greet(&self) -> String { ... } 書かなくてよい(書かなければデフォルトが使われる)

05 - デフォルト実装を上書きする

デフォルト実装に関する問題です。
Staffはデフォルトの挨拶のままにし、Robotだけ独自の挨拶を返すようにしてください。

期待する出力
こんにちは
ピピッ、コンニチハ
「Playgroundで開く」をクリックして修正・実行してください
trait Greet {
    fn greet(&self) -> String {
        String::from("こんにちは")
    }
}

struct Staff;
struct Robot;

impl Greet for Staff {}

// Robotのgreetは「ピピッ、コンニチハ」を返すようにせよ
impl Greet for Robot {}

fn main() {
    println!("{}", Staff.greet());
    println!("{}", Robot.greet());
}
Playgroundで開く
解答例と解説
trait Greet {
    fn greet(&self) -> String {
        String::from("こんにちは")
    }
}

struct Staff;
struct Robot;

impl Greet for Staff {}

// Robotのgreetは「ピピッ、コンニチハ」を返すようにせよ
impl Greet for Robot {} 
impl Greet for Robot { 
    fn greet(&self) -> String { 
        String::from("ピピッ、コンニチハ") 
    } 
} 

fn main() {
    println!("{}", Staff.greet());
    println!("{}", Robot.greet());
}
Playgroundで開く

デフォルト実装は「実装側が書かなかったときに使われる処理」なので、実装側が同じ名前・同じシグネチャでメソッドを定義すれば、そちらが優先されます。これを上書きと呼びます。

Staffはimplブロックを空のままにしているのでデフォルトの「こんにちは」が、Robotはgreetを定義しているので独自の「ピピッ、コンニチハ」が返ります。上書きするかどうかは型ごとに自由に選べます。

このとき、上書きする側のシグネチャは、トレイトで宣言したもの(fn greet(&self) -> String)と一致している必要があります。戻り値の型を変えたり引数を増やしたりすると、「トレイトと実装で型が合わない」というコンパイルエラー(E0053)になります。

06 - デフォルト実装から必須メソッドを呼ぶ

デフォルト実装とトレイトに関する問題です。
ExamとQuizの2つの型にScoreトレイトを実装してください。実装するのはpointsだけで構いません。

期待する出力
期末試験: 85点 → 合格(B)
小テスト: 40点 → 不合格(D)
「Playgroundで開く」をクリックして修正・実行してください
trait Score {
    fn points(&self) -> u32;

    fn rank(&self) -> String {
        if self.points() >= 90 {
            String::from("A")
        } else if self.points() >= 70 {
            String::from("B")
        } else if self.points() >= 60 {
            String::from("C")
        } else {
            String::from("D")
        }
    }

    fn passed(&self) -> bool {
        self.points() >= 60
    }

    fn summary(&self) -> String {
        let result = if self.passed() { "合格" } else { "不合格" };
        format!("{}点 → {}({})", self.points(), result, self.rank())
    }
}

struct Exam {
    score: u32,
}

struct Quiz {
    correct: u32, // 正答数。1問10点
}

// ExamにScoreを実装せよ(pointsはscoreをそのまま返す)

// QuizにScoreを実装せよ(pointsは正答数×10を返す)

fn main() {
    let exam = Exam { score: 85 };
    let quiz = Quiz { correct: 4 };
    println!("期末試験: {}", exam.summary());
    println!("小テスト: {}", quiz.summary());
}
Playgroundで開く
解答例と解説
trait Score {
    fn points(&self) -> u32;

    fn rank(&self) -> String {
        if self.points() >= 90 {
            String::from("A")
        } else if self.points() >= 70 {
            String::from("B")
        } else if self.points() >= 60 {
            String::from("C")
        } else {
            String::from("D")
        }
    }

    fn passed(&self) -> bool {
        self.points() >= 60
    }

    fn summary(&self) -> String {
        let result = if self.passed() { "合格" } else { "不合格" };
        format!("{}点 → {}({})", self.points(), result, self.rank())
    }
}

struct Exam {
    score: u32,
}

struct Quiz {
    correct: u32, // 正答数。1問10点
}

// ExamにScoreを実装せよ(pointsはscoreをそのまま返す)
impl Score for Exam { 
    fn points(&self) -> u32 { 
        self.score 
    } 
} 

// QuizにScoreを実装せよ(pointsは正答数×10を返す)
impl Score for Quiz { 
    fn points(&self) -> u32 { 
        self.correct * 10
    } 
} 

fn main() {
    let exam = Exam { score: 85 };
    let quiz = Quiz { correct: 4 };
    println!("期末試験: {}", exam.summary());
    println!("小テスト: {}", quiz.summary());
}
Playgroundで開く

Scoreトレイトには4つのメソッドがありますが、必須なのは本体のないpointsだけです。残りのrank・passed・summaryはデフォルト実装で、しかもその中身はすべてself.points()を呼んで組み立てられています。

デフォルト実装の本体では、selfを通じて同じトレイトの他のメソッドを呼べます。呼ぶ相手が必須メソッドで、トレイト宣言の時点ではまだ中身が決まっていなくても構いません。実際にexam.summary()が実行されるときにはselfはExamなので、Examのpoints(self.score)が呼ばれます。quiz.summary()ならQuizのpoints(self.correct * 10)です。

この形にすると、トレイト側が多くの機能を提供しながら、実装側に要求するのはごく一部で済みます。ExamとQuizはpointsを3行書いただけで、rank・passed・summaryの3つをまとめて手に入れました。型が10個に増えても、書くのはそれぞれのpointsだけです。

07 - 応用: 通知システム

トレイトとデフォルト実装に関する問題です。第18章の総復習として、通知メッセージを組み立てる仕組みをトレイトで作ります。

まず、次の4つのメソッドを持つトレイトNotifyを定義します。

メソッド 種類 内容
recipient(&self) -> String 必須 宛先の表記を返す
content(&self) -> String 必須 本文を返す
channel(&self) -> String デフォルト実装 通知の種別。既定では「通知」を返す
message(&self) -> String デフォルト実装 [種別] 宛先: 本文の形に組み立てて返す

そのうえで、EmailとSmsにNotifyを実装し、すべてのテストに合格させてください。

型 recipient content channel
Email 名前 <アドレス> 件名 / 本文 「メール」に上書きする
Sms 名前 (電話番号) 本文そのまま デフォルトのまま
「Playgroundで開く」をクリックしてTESTを実行してください
// トレイトNotifyを定義せよ(recipient・contentは必須、channel・messageはデフォルト実装)

struct Email {
    to: String,
    address: String,
    subject: String,
    body: String,
}

struct Sms {
    to: String,
    phone: String,
    text: String,
}

// EmailにNotifyを実装せよ(channelは「メール」に上書き)

// SmsにNotifyを実装せよ(channelはデフォルトのまま)

fn sample_email() -> Email {
    Email {
        to: String::from("太郎"),
        address: String::from("[email protected]"),
        subject: String::from("請求書"),
        body: String::from("今月分を送付しました"),
    }
}

fn sample_sms() -> Sms {
    Sms {
        to: String::from("花子"),
        phone: String::from("090-0000-0000"),
        text: String::from("配達が完了しました"),
    }
}

#[test]
fn test_channel() {
    assert_eq!(sample_email().channel(), "メール");
    assert_eq!(sample_sms().channel(), "通知");
}

#[test]
fn test_recipient_and_content() {
    let email = sample_email();
    assert_eq!(email.recipient(), "太郎 <[email protected]>");
    assert_eq!(email.content(), "請求書 / 今月分を送付しました");

    let sms = sample_sms();
    assert_eq!(sms.recipient(), "花子 (090-0000-0000)");
    assert_eq!(sms.content(), "配達が完了しました");
}

#[test]
fn test_message() {
    assert_eq!(
        sample_email().message(),
        "[メール] 太郎 <[email protected]>: 請求書 / 今月分を送付しました"
    );
    assert_eq!(
        sample_sms().message(),
        "[通知] 花子 (090-0000-0000): 配達が完了しました"
    );
}
Playgroundで開く
解答例と解説
// トレイトNotifyを定義せよ(recipient・contentは必須、channel・messageはデフォルト実装)
trait Notify { 
    fn recipient(&self) -> String; 
    fn content(&self) -> String; 

    fn channel(&self) -> String { 
        String::from("通知") 
    } 

    fn message(&self) -> String { 
        format!("[{}] {}: {}", self.channel(), self.recipient(), self.content()) 
    } 
} 

struct Email {
    to: String,
    address: String,
    subject: String,
    body: String,
}

struct Sms {
    to: String,
    phone: String,
    text: String,
}

// EmailにNotifyを実装せよ(channelは「メール」に上書き)
impl Notify for Email { 
    fn recipient(&self) -> String { 
        format!("{} <{}>", self.to, self.address) 
    } 

    fn content(&self) -> String { 
        format!("{} / {}", self.subject, self.body) 
    } 

    fn channel(&self) -> String { 
        String::from("メール") 
    } 
} 

// SmsにNotifyを実装せよ(channelはデフォルトのまま)
impl Notify for Sms { 
    fn recipient(&self) -> String { 
        format!("{} ({})", self.to, self.phone) 
    } 

    fn content(&self) -> String { 
        self.text.clone() 
    } 
} 

fn sample_email() -> Email {
    Email {
        to: String::from("太郎"),
        address: String::from("[email protected]"),
        subject: String::from("請求書"),
        body: String::from("今月分を送付しました"),
    }
}

fn sample_sms() -> Sms {
    Sms {
        to: String::from("花子"),
        phone: String::from("090-0000-0000"),
        text: String::from("配達が完了しました"),
    }
}

#[test]
fn test_channel() {
    assert_eq!(sample_email().channel(), "メール");
    assert_eq!(sample_sms().channel(), "通知");
}

#[test]
fn test_recipient_and_content() {
    let email = sample_email();
    assert_eq!(email.recipient(), "太郎 <[email protected]>");
    assert_eq!(email.content(), "請求書 / 今月分を送付しました");

    let sms = sample_sms();
    assert_eq!(sms.recipient(), "花子 (090-0000-0000)");
    assert_eq!(sms.content(), "配達が完了しました");
}

#[test]
fn test_message() {
    assert_eq!(
        sample_email().message(),
        "[メール] 太郎 <[email protected]>: 請求書 / 今月分を送付しました"
    );
    assert_eq!(
        sample_sms().message(),
        "[通知] 花子 (090-0000-0000): 配達が完了しました"
    );
}
Playgroundで開く

この章で学んだことが、1つのトレイトに全部入っています。

必須メソッドとデフォルト実装の切り分け
recipientとcontentは型ごとに中身がまったく違うので必須メソッドにし、channelとmessageは共通の処理で済むのでデフォルト実装にしています。「型ごとに違う部分だけを必須にし、残りは共通化する」のがトレイト設計の基本です。

デフォルト実装どうしの組み合わせ
messageは、必須のrecipient・contentだけでなく、デフォルト実装のchannelも呼んでいます。問題06の形の発展で、呼ぶ相手が必須かデフォルトかは関係ありません。

上書きは「呼ばれる側」にも効く
Emailはchannelを「メール」に上書きしています。message自体は上書きしていませんが、messageの中のself.channel()はEmailの実装を呼ぶので、email.message()の結果も[メール] ...に変わります。1か所の上書きが、それを利用するデフォルト実装すべてに波及します。

self.text.clone()と書く理由
contentはStringを返す約束なので、self.textをそのまま返すと&self越しにフィールドの所有権を持ち出すことになり、第8章で学んだ「借用からムーブできない」エラーになります。.clone()で複製を返せば、selfのフィールドはそのまま残ります。

assert_eq!でStringと&strを比べられる理由
recipient()が返すのはString、比較相手は&strです。第11章でも触れたとおり、標準ライブラリがStringと&strの比較をあらかじめ用意しているので、そのまま比べられます。実はこの「あらかじめ用意されている比較」も、標準ライブラリがPartialEqというトレイトをStringに実装していることで実現されています。