// book chapter
動かして学ぶRustプログラミング問題集第20章 トレイト境界とimpl Trait.
第17章でジェネリクス、第18章と第19章でトレイトを学びました。この章では、その2つを組み合わせる方法を7問で扱います。
第17章の型引数Tは「どんな型でもよい」という意味でした。しかし、どんな型か分からない値に対してできることはほとんどありません。表示することも、大小を比べることも、自作のメソッドを呼ぶこともできません。「どんな型でもよい。ただし、このトレイトを実装していること」と条件を付けて初めて、Tの値に対してそのトレイトのメソッドが使えるようになります。この条件をトレイト境界と呼びます。
前半では、境界のないジェネリック関数がコンパイルエラーになるところから始めて、境界の基本形、+による複数指定、where句への切り出しと、書き方を順に広げていきます。後半では、具体的な型名の代わりに「このトレイトを実装した型」とだけ書くimpl Trait記法を扱います。引数の位置に書くとトレイト境界の省略形になり、戻り値の位置に書くと「具体的な型を隠して返す」という別の意味になります。最後に、戻り値のimpl Traitが1つの型しか返せないことをエラーで体験します。
進め方は第19章までと同じです。各問題の冒頭に関連する辞書へのリンクを挙げているので、まずはリンク先で必要な知識を確認してから取り組んでください。
01 - トレイト境界で振る舞いを要求する
トレイト境界とジェネリクス、トレイトに関する問題です。
print_description関数は、どんな型の値でも受け取って説明文を出力しようとしていますが、コンパイルエラーになります。エラーメッセージを読んで、関数のシグネチャを修正してください。
書籍『Rust入門』 3200円
数値 42trait Describe {
fn describe(&self) -> String;
}
struct Book {
title: String,
price: u32,
}
impl Describe for Book {
fn describe(&self) -> String {
format!("書籍『{}』 {}円", self.title, self.price)
}
}
impl Describe for u32 {
fn describe(&self) -> String {
format!("数値 {}", self)
}
}
fn print_description<T>(item: T) {
println!("{}", item.describe());
}
fn main() {
let book = Book {
title: String::from("Rust入門"),
price: 3200,
};
print_description(book);
print_description(42u32);
}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)
}
}
impl Describe for u32 {
fn describe(&self) -> String {
format!("数値 {}", self)
}
}
fn print_description<T>(item: T) {
fn print_description<T: Describe>(item: T) {
println!("{}", item.describe());
}
fn main() {
let book = Book {
title: String::from("Rust入門"),
price: 3200,
};
print_description(book);
print_description(42u32);
}Playgroundで開くエラーメッセージはno method named 'describe' found for type parameter 'T' in the current scope(E0599)です。「型引数Tにはdescribeというメソッドがない」と言われています。
Bookにもu32にもdescribeは実装してあるのに、なぜ見つからないのでしょうか。第17章で学んだとおり、型引数Tは「呼び出し側がどんな型を渡してもよい」という意味です。コンパイラは関数の中身を検査するとき、TがBookかもしれないし、u32かもしれないし、describeを持たないStringやboolかもしれない、という前提で見ます。どんな型が来ても動くことを保証できない限り、item.describe()は認められません。
そこで、Tに条件を付けます。<T: Describe>と書くと、「TはDescribeトレイトを実装した型に限る」という意味になります。これがトレイト境界です。境界を付けると、関数の中ではDescribeが宣言するメソッドをTの値に対して呼べるようになります。代わりに、Describeを実装していない型を渡すと呼び出し側がエラーになります。
print_description(String::from("本")); // エラー: E0277(StringはDescribeを実装していない)第18章では、トレイトを実装した型に対してbook.describe()のように直接メソッドを呼んでいました。トレイト境界を使うと、「Describeを実装した型なら何でも受け取れる関数」が書けます。トレイトが本領を発揮するのは、この使い方からです。
02 - 最大値を求めるlargest関数
トレイト境界と比較のトレイト、ジェネリクスに関する問題です。
largest関数は、スライスの中で最大の要素への参照を返す関数です。整数でも浮動小数点数でも使えるようにジェネリックにしましたが、コンパイルエラーになります。シグネチャを修正してください。
最高価格: 980
最高気温: 37.2fn largest<T>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
fn main() {
let prices = [150, 980, 120, 450];
println!("最高価格: {}", largest(&prices));
let temperatures = [36.5, 37.2, 35.9];
println!("最高気温: {}", largest(&temperatures));
}Playgroundで開く解答例と解説
fn largest<T>(list: &[T]) -> &T {
fn largest<T: PartialOrd>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
fn main() {
let prices = [150, 980, 120, 450];
println!("最高価格: {}", largest(&prices));
let temperatures = [36.5, 37.2, 35.9];
println!("最高気温: {}", largest(&temperatures));
}Playgroundで開くエラーメッセージはbinary operation '>' cannot be applied to type '&T'(E0369)です。「&T同士に>は使えない」と言われています。
第19章の問題05で学んだとおり、<や>による大小比較は比較のトレイトのPartialOrdが担っています。i32やf64はPartialOrdを実装しているので比較できますが、境界のないTは「PartialOrdを実装していないかもしれない型」です。問題01と同じ理屈で、Tの値同士の比較は認められません。
修正は<T: PartialOrd>と境界を付けるだけです。TがPartialOrdを実装していることが保証されるので、>が使えるようになります。呼び出し側のi32とf64はどちらもPartialOrdを実装しているので、そのまま動きます。
&Tを返している理由
この関数は、最大の要素そのものではなく要素への参照&Tを返しています。スライスの要素は借りているだけなので、第8章で学んだとおり中身を勝手にムーブして持ち出すことはできないからです。参照を返す形にしておけば、Tがどんな型でも所有権の問題が起きません。
03 - 複数のトレイト境界
トレイト境界とDisplayトレイトに関する問題です。
print_larger関数は、2つの値を比べて大きいほうを出力します。比較のための境界は付けてありますが、まだコンパイルエラーになります。エラーメッセージを読んで、シグネチャを修正してください。
高いのは 980 です
高いのは 37.2 ですuse std::fmt::Display;
fn print_larger<T: PartialOrd>(a: T, b: T) {
if a > b {
println!("高いのは {} です", a);
} else {
println!("高いのは {} です", b);
}
}
fn main() {
print_larger(150, 980);
print_larger(36.5, 37.2);
}Playgroundで開く解答例と解説
use std::fmt::Display;
fn print_larger<T: PartialOrd>(a: T, b: T) {
fn print_larger<T: PartialOrd + Display>(a: T, b: T) {
if a > b {
println!("高いのは {} です", a);
} else {
println!("高いのは {} です", b);
}
}
fn main() {
print_larger(150, 980);
print_larger(36.5, 37.2);
}Playgroundで開くエラーメッセージは'T' doesn't implement 'std::fmt::Display'(E0277)です。「TはDisplayを実装していない」と言われています。
PartialOrdの境界があるので>は使えますが、{}で出力するには第19章の問題06で学んだDisplayトレイトが必要です。PartialOrdを実装しているからといってDisplayも実装しているとは限らないので、{}の部分が認められません。
要求するトレイトが複数あるときは、+でつなげて並べます。<T: PartialOrd + Display>は「PartialOrdとDisplayの両方を実装している型に限る」という意味です。どちらか一方だけ実装している型は受け付けません。
Displayはstd::fmtモジュールにあるので、use std::fmt::Display;で持ち込んでいます。第14章で学んだとおり、useなしでT: std::fmt::Displayと書いても同じです。
04 - where句
トレイト境界に関する問題です。
print_ranking関数は、2つの項目を比べてスコアの高い順に出力します。境界が2つの型引数に分かれて<>の中が長くなっているので、where句を使って同じ意味のまま読みやすく書き換えてください。出力は変わりません。
[価格ランキング] 1位: メロン(980) 2位: りんご(150)
[人気ランキング] 1位: 本店(4.5) 2位: 駅前店(3.8)use std::fmt::Display;
fn print_ranking<T: Display + PartialOrd, U: Display>(title: U, name_a: &str, score_a: T, name_b: &str, score_b: T) {
if score_a >= score_b {
println!("[{}] 1位: {}({}) 2位: {}({})", title, name_a, score_a, name_b, score_b);
} else {
println!("[{}] 1位: {}({}) 2位: {}({})", title, name_b, score_b, name_a, score_a);
}
}
fn main() {
print_ranking("価格ランキング", "りんご", 150, "メロン", 980);
print_ranking(String::from("人気ランキング"), "本店", 4.5, "駅前店", 3.8);
}Playgroundで開く解答例と解説
use std::fmt::Display;
fn print_ranking<T: Display + PartialOrd, U: Display>(title: U, name_a: &str, score_a: T, name_b: &str, score_b: T) {
fn print_ranking<T, U>(title: U, name_a: &str, score_a: T, name_b: &str, score_b: T)
where
T: Display + PartialOrd,
U: Display,
{
if score_a >= score_b {
println!("[{}] 1位: {}({}) 2位: {}({})", title, name_a, score_a, name_b, score_b);
} else {
println!("[{}] 1位: {}({}) 2位: {}({})", title, name_b, score_b, name_a, score_a);
}
}
fn main() {
print_ranking("価格ランキング", "りんご", 150, "メロン", 980);
print_ranking(String::from("人気ランキング"), "本店", 4.5, "駅前店", 3.8);
}Playgroundで開くwhere句は、トレイト境界を<>の中からシグネチャの後ろへ切り出す書き方です。<T, U>には型引数の名前だけを残し、引数リストと戻り値の型の後ろにwhereを置いて、型引数ごとに境界を1行ずつ書きます。
fn f<T: Display + PartialOrd, U: Display>(title: U, score: T)
fn f<T, U>(title: U, score: T)
where
T: Display + PartialOrd,
U: Display,意味はまったく同じで、どちらを使うかは読みやすさの問題です。型引数が1つで境界も1つなら<T: Display>の形で十分です。型引数が増えたり境界が増えたりして、引数リストが境界に埋もれて読みにくくなってきたらwhere句に切り出します。本体の{はwhere句の後ろに来ることに注意してください。
Uが別の型引数になっている理由
titleには&strとStringという違う型を渡しています。第17章の問題03で学んだとおり、1つの型引数は1つの型にしか決まらないので、score用のTとは別にtitle用のUを用意しています。Uは表示するだけなので、境界はDisplayだけです。
05 - 引数位置のimpl Trait
impl Traitとトレイト境界に関する問題です。
notify関数は、引数の型に&impl Summaryという見慣れない書き方をしています。関数の中身を書いて、受け取った値の要約を通知として出力してください。
【通知】新刊『Rust入門』が入荷しました
【通知】セール: りんごが150円trait Summary {
fn summarize(&self) -> String;
}
struct NewArrival {
title: String,
}
impl Summary for NewArrival {
fn summarize(&self) -> String {
format!("新刊『{}』が入荷しました", self.title)
}
}
struct Sale {
name: String,
price: u32,
}
impl Summary for Sale {
fn summarize(&self) -> String {
format!("セール: {}が{}円", self.name, self.price)
}
}
fn notify(item: &impl Summary) {
// 【通知】に続けて item の要約を出力せよ
}
fn main() {
let arrival = NewArrival {
title: String::from("Rust入門"),
};
let sale = Sale {
name: String::from("りんご"),
price: 150,
};
notify(&arrival);
notify(&sale);
}Playgroundで開く解答例と解説
trait Summary {
fn summarize(&self) -> String;
}
struct NewArrival {
title: String,
}
impl Summary for NewArrival {
fn summarize(&self) -> String {
format!("新刊『{}』が入荷しました", self.title)
}
}
struct Sale {
name: String,
price: u32,
}
impl Summary for Sale {
fn summarize(&self) -> String {
format!("セール: {}が{}円", self.name, self.price)
}
}
fn notify(item: &impl Summary) {
// 【通知】に続けて item の要約を出力せよ
println!("【通知】{}", item.summarize());
}
fn main() {
let arrival = NewArrival {
title: String::from("Rust入門"),
};
let sale = Sale {
name: String::from("りんご"),
price: 150,
};
notify(&arrival);
notify(&sale);
}Playgroundで開く引数の型の位置に書いたimpl Summaryは、「Summaryを実装した何らかの型」という意味です。item.summarize()と、Summaryが宣言するメソッドをそのまま呼べます。
実はこの書き方は、ここまで学んできたトレイト境界の省略形です。次の2つは同じ意味になります。
fn notify(item: &impl Summary)
fn notify<T: Summary>(item: &T)impl Summaryと書くと、コンパイラが名前のない型引数を1つ用意して、Summaryの境界を付けてくれます。型引数に名前を付ける必要がなく、境界が引数のすぐ隣に書けるので、引数が1つだけの単純な関数ではこちらのほうが簡潔です。
同じにならない場面
省略形なので、型引数の名前を使って表現していたことは書けません。たとえば問題03のprint_larger<T: PartialOrd + Display>(a: T, b: T)は「aとbは同じ型」という意味をTの名前で表していました。これをprint_larger(a: impl PartialOrd + Display, b: impl PartialOrd + Display)と書き換えると、aとbは別々の名前のない型引数になり、同じ型である保証がなくなるのでa > bが比較できなくなります。複数の引数を同じ型に揃えたいときは、<T: Trait>の形で書いてください。
06 - 戻り値位置のimpl Trait
impl Traitに関する問題です。
featured関数は、本日のおすすめとしてSaleを返しています。この関数の戻り値の型を、Saleという具体的な型名を書かずに「Summaryを実装した何らかの型」と宣言するように書き換えてください。出力は変わりません。
本日のおすすめ: セール: メロンが980円trait Summary {
fn summarize(&self) -> String;
}
struct Sale {
name: String,
price: u32,
}
impl Summary for Sale {
fn summarize(&self) -> String {
format!("セール: {}が{}円", self.name, self.price)
}
}
fn featured() -> Sale {
Sale {
name: String::from("メロン"),
price: 980,
}
}
fn main() {
let item = featured();
println!("本日のおすすめ: {}", item.summarize());
}Playgroundで開く解答例と解説
trait Summary {
fn summarize(&self) -> String;
}
struct Sale {
name: String,
price: u32,
}
impl Summary for Sale {
fn summarize(&self) -> String {
format!("セール: {}が{}円", self.name, self.price)
}
}
fn featured() -> Sale {
fn featured() -> impl Summary {
Sale {
name: String::from("メロン"),
price: 980,
}
}
fn main() {
let item = featured();
println!("本日のおすすめ: {}", item.summarize());
}Playgroundで開く戻り値の型の位置にimpl Summaryと書くと、「Summaryを実装した具体的な型を1つ返す」という意味になります。実際に返しているのはSaleですが、呼び出し側にはその型名が伏せられ、「Summaryを実装した何かが返ってくる」ことだけが伝わります。
引数位置のimpl Trait(問題05)と見た目は同じですが、役割は逆です。
| 観点 | 引数位置 fn f(x: impl Summary) |
戻り値位置 fn f() -> impl Summary |
|---|---|---|
| 具体的な型を決めるのは | 呼び出し側(渡した値の型) | 関数側(returnした値の型) |
| 呼び出しごとの型 | 渡した型ごとに変わる | 関数ごとに1つに固定される |
呼び出し側から見えるもの
呼び出し側のitemは「Summaryを実装した何か」なので、使えるのはSummaryが宣言するメソッドだけです。Saleが持つnameやpriceというフィールドには触れません。試しにmainにprintln!("{}", item.price);を足してみると、「impl Summary型にpriceというフィールドはない」というエラーになります。
trait Summary {
fn summarize(&self) -> String;
}
struct Sale {
name: String,
price: u32,
}
impl Summary for Sale {
fn summarize(&self) -> String {
format!("セール: {}が{}円", self.name, self.price)
}
}
fn featured() -> impl Summary {
Sale {
name: String::from("メロン"),
price: 980,
}
}
fn main() {
let item = featured();
println!("{}", item.price); // エラー: E0609(impl Summary に price というフィールドはない)
}
不便に見えるかもしれませんが、これは「この関数はSummaryの振る舞いだけを約束する」という宣言でもあります。あとでfeaturedがSaleではなくNewArrivalを返すように変更しても、呼び出し側がSummaryのメソッドしか使っていなければ何も壊れません。
07 - 戻り値のimpl Traitは1つの型だけ
impl Traitとif式に関する問題です。
choose_shipping関数は、急ぎかどうかで配送方法を切り替えて返そうとしていますが、コンパイルエラーになります。なぜエラーになるのかを考え、choose_shippingの戻り値の型はimpl Shippingのままで、どちらの場合も同じ型を返す形に修正してください。
速達: 翌日お届け
通常便: 3〜5日でお届けtrait Shipping {
fn describe(&self) -> String;
}
struct Express;
impl Shipping for Express {
fn describe(&self) -> String {
String::from("速達: 翌日お届け")
}
}
struct Standard;
impl Shipping for Standard {
fn describe(&self) -> String {
String::from("通常便: 3〜5日でお届け")
}
}
fn choose_shipping(urgent: bool) -> impl Shipping {
if urgent { Express } else { Standard }
}
fn main() {
println!("{}", choose_shipping(true).describe());
println!("{}", choose_shipping(false).describe());
}Playgroundで開く解答例と解説
trait Shipping {
fn describe(&self) -> String;
}
struct Express;
enum Delivery {
Express,
Standard,
}
impl Shipping for Express {
impl Shipping for Delivery {
fn describe(&self) -> String {
String::from("速達: 翌日お届け")
}
}
struct Standard;
impl Shipping for Standard {
fn describe(&self) -> String {
String::from("通常便: 3〜5日でお届け")
match self {
Delivery::Express => String::from("速達: 翌日お届け"),
Delivery::Standard => String::from("通常便: 3〜5日でお届け"),
}
}
}
fn choose_shipping(urgent: bool) -> impl Shipping {
if urgent { Express } else { Standard }
if urgent { Delivery::Express } else { Delivery::Standard }
}
fn main() {
println!("{}", choose_shipping(true).describe());
println!("{}", choose_shipping(false).describe());
}Playgroundで開くエラーメッセージは'if' and 'else' have incompatible types(E0308)で、expected 'Express', found 'Standard'と続きます。「ifとelseで返す型が食い違っている」と言われています。
ExpressもStandardもShippingを実装しているのだから、どちらを返してもimpl Shippingの条件は満たしているように見えます。しかし問題06で学んだとおり、戻り値のimpl Shippingは「Shippingを実装した具体的な型を1つ返す」という意味です。impl Shippingの裏側には隠された具体的な型が1つだけあり、関数のどの経路を通っても同じ型に決まらなければなりません。最初の経路でExpressと決まった以上、else側でStandardを返すことはできません。
同じ型を返す形に直す
どちらの経路でも同じ型を返せばよいので、2つの構造体を1つの型にまとめます。「速達か通常便か」は第11章で学んだ列挙型がちょうど合います。Deliveryという列挙型にExpressとStandardの2バリアントを持たせ、Shippingの実装はDeliveryに対して1つだけ書き、describeの中でmatch式を使って出し分けます。こうすればifの両方の経路がDelivery型になり、impl Shippingの裏側の型もDeliveryの1つに決まります。
実行時に型を切り替えたいときは
この問題のように「条件によって違う型を返したい」という要求は珍しくありません。どうしても別々の型のまま返したい場合は、impl ShippingではなくBox<dyn Shipping>というトレイトオブジェクトを使います。impl Traitがコンパイル時に型を1つに決める仕組みなのに対し、トレイトオブジェクトは実行時にどの型かを判別する仕組みです。こちらは次の第21章で扱います。