// book chapter
動かして学ぶRustプログラミング問題集第19章 derive属性と標準トレイト.
第18章で、trait宣言で振る舞いを定め、impl トレイト名 for 型名で型ごとに実装する仕組みを学びました。この章では、標準ライブラリがあらかじめ用意している標準トレイトを、自分の型に与える方法を8問で扱います。
実は、みなさんはすでに標準トレイトを使っています。第9章から何度も書いてきた#[derive(Debug)]がそれです。あのときは「{:?}で出力するためのおまじない」として扱いましたが、その正体はトレイト実装の自動生成です。Debugという標準トレイトのimplブロックを、コンパイラが構造体の定義から書き起こしてくれていました。
Debugのほかにも、値を複製するClone、==で比べるPartialEq、<で比べるPartialOrd、既定値を作るDefaultといった標準トレイトがderiveで導出できます。一方で、利用者向けの表示を担うDisplayのように、導出できないので手で実装するトレイトもあります。この章では「deriveで済むもの」と「手で書くもの」の両方を扱い、最後に「自分のものでない型にトレイトを実装しようとすると何が起きるか」を確かめます。
進め方は第18章までと同じです。各問題の冒頭に関連する辞書へのリンクを挙げているので、まずはリンク先で必要な知識を確認してから取り組んでください。
01 - deriveの正体
derive属性とクローンとトレイトに関する問題です。
次のコードはコンパイルエラー(E0599)になります。Itemの定義に1行足して修正してください。
原本: コーヒー 500円
控え: コーヒー 500円struct Item {
name: String,
price: u32,
}
fn main() {
let item = Item {
name: String::from("コーヒー"),
price: 500,
};
let spare = item.clone();
println!("原本: {} {}円", item.name, item.price);
println!("控え: {} {}円", spare.name, spare.price);
}Playgroundで開く解答例と解説
#[derive(Clone)]
struct Item {
name: String,
price: u32,
}
fn main() {
let item = Item {
name: String::from("コーヒー"),
price: 500,
};
let spare = item.clone();
println!("原本: {} {}円", item.name, item.price);
println!("控え: {} {}円", spare.name, spare.price);
}Playgroundで開くエラーメッセージはno method named 'clone' found for struct 'Item' in the current scope(E0599)です。第7章でStringに対して.clone()を呼びましたが、自作のItemにはそのメソッドがありません。
.clone()は、Stringに生まれつき備わっているメソッドではなく、クローンで扱っている標準ライブラリのCloneトレイトが定めるメソッドです。StringはCloneを実装しているので.clone()が呼べ、Itemは実装していないので呼べません。第18章で学んだ「トレイトを実装した型だけがそのメソッドを持つ」という規則が、そのまま当てはまっています。
ですから、第18章どおりに手で実装すれば直ります。
impl Clone for Item {
fn clone(&self) -> Self {
Item {
name: self.name.clone(),
price: self.price.clone(),
}
}
}ただ、この中身は「全フィールドを順に.clone()して詰め直す」だけの定型文です。フィールドが増えるたびに書き足すのも退屈ですし、書き忘れも起こります。そこで登場するのがderive属性です。
#[derive(Clone)]と定義の直前に書くと、コンパイラが上のimplブロックとまったく同じものを自動生成します。deriveは「導出する」という意味で、構造体の定義内容からトレイトの実装を導き出す、ということです。
第9章からずっと書いてきた#[derive(Debug)]も同じ仕組みです。あれはDebugトレイトのimplブロックを自動生成していたのでした。{:?}で出力できるようになったのは、Debugトレイトの実装が裏で用意されていたからです。
deriveで導出できる標準トレイトは、次の9種類に限られます。
| トレイト | 自動生成される実装 |
|---|---|
Clone |
全フィールドを.clone()した複製を返す |
Copy |
ビット単位の複製でよいと宣言する |
Debug |
{:?}で型名・フィールド名・値を並べて表示する |
Default |
全フィールドをその型の既定値で埋めた値を作る |
PartialEq |
全フィールドの比較で==・!=を定義する |
Eq |
同値関係が成り立つと宣言する |
PartialOrd |
フィールドの宣言順に比較して<などを定義する |
Ord |
同じ順序づけを全順序として定義する |
Hash |
全フィールドをハッシュ値の計算に混ぜ込む |
この章では、このうちDebug・PartialEq・PartialOrd・Defaultを順に扱います。
02 - Debugトレイトと整形出力
Debugトレイトとderive属性とコンソール出力に関する問題です。
orderを{:?}と{:#?}で1回ずつ出力してください。
Order { item: Item { name: "コーヒー", price: 500 }, count: 2 }
Order {
item: Item {
name: "コーヒー",
price: 500,
},
count: 2,
}#[derive(Debug)]
struct Item {
name: String,
price: u32,
}
#[derive(Debug)]
struct Order {
item: Item,
count: u32,
}
fn main() {
let order = Order {
item: Item {
name: String::from("コーヒー"),
price: 500,
},
count: 2,
};
// orderを{:?}で出力せよ
// orderを{:#?}で出力せよ
}Playgroundで開く解答例と解説
#[derive(Debug)]
struct Item {
name: String,
price: u32,
}
#[derive(Debug)]
struct Order {
item: Item,
count: u32,
}
fn main() {
let order = Order {
item: Item {
name: String::from("コーヒー"),
price: 500,
},
count: 2,
};
// orderを{:?}で出力せよ
println!("{:?}", order);
// orderを{:#?}で出力せよ
println!("{:#?}", order);
}Playgroundで開く{:?}と{:#?}は、どちらもDebugトレイトの実装を呼び出す書式指定です。違いは見た目だけで、{:?}は1行に詰めて、{:#?}は1フィールド1行に改行とインデントを付けて表示します。#は「整形して表示せよ」という意味の指定で、pretty printと呼ばれます。
#[derive(Debug)]が生成する表示は、「型名、波括弧、フィールド名と値をカンマ区切りで並べる」という機械的なものです。フィールドの値もそれぞれの型のDebug実装で表示されるため、itemの位置にはItemのDebug表示がそのまま入れ子になり、Stringの値はダブルクォートで囲まれます。
今回のように構造体が入れ子になっていると、{:?}では1行が長くなって読みづらくなります。デバッグ中に中身を確認するなら{:#?}のほうが見やすい場面が多いです。
03 - PartialEqで等値比較
比較のトレイトとderive属性に関する問題です。
次のコードはコンパイルエラー(E0369)になります。Itemの定義に1行足して修正してください。
同じ商品か: true
同じ商品か: falsestruct Item {
name: String,
price: u32,
}
fn main() {
let a = Item {
name: String::from("コーヒー"),
price: 500,
};
let b = Item {
name: String::from("コーヒー"),
price: 500,
};
let c = Item {
name: String::from("紅茶"),
price: 500,
};
println!("同じ商品か: {}", a == b);
println!("同じ商品か: {}", a == c);
}Playgroundで開く解答例と解説
#[derive(PartialEq)]
struct Item {
name: String,
price: u32,
}
fn main() {
let a = Item {
name: String::from("コーヒー"),
price: 500,
};
let b = Item {
name: String::from("コーヒー"),
price: 500,
};
let c = Item {
name: String::from("紅茶"),
price: 500,
};
println!("同じ商品か: {}", a == b);
println!("同じ商品か: {}", a == c);
}Playgroundで開くエラーメッセージはbinary operation '==' cannot be applied to type 'Item'(E0369)です。続けてnote: an implementation of 'PartialEq' might be missing for 'Item'と、足りないものの名前まで教えてくれています。
第2章から当たり前のように使ってきた==ですが、Rustでは==もトレイトのメソッド呼び出しです。a == bはPartialEqトレイトのeqメソッドの呼び出しa.eq(&b)に展開されます。整数やStringで==が使えたのは、それらの型がPartialEqを実装していたからで、自作のItemには実装がないため演算子が使えません。
#[derive(PartialEq)]を付けると、全フィールドをそれぞれ==で比べ、すべて等しければ等しいという実装が生成されます。aとbはnameもpriceも同じなのでtrue、cはnameが違うのでfalseです。!=も同時に使えるようになります。
比較のトレイトで扱っているとおり、等価性のトレイトにはPartialEqのほかにEqもあります。Eqはメソッドを持たず「この型では必ずa == aが成り立つ」と宣言するだけのトレイトで、==を使うだけならPartialEqで足ります。Eqが必要になる場面と、Partialという名前の意味は問題05で扱います。
04 - deriveは全フィールドに要求する
derive属性と構造体に関する問題です。
次のコードはコンパイルエラー(E0277)になります。Orderの#[derive(Clone)]は消さずに修正してください。
コーヒー ×2
コーヒー ×2struct Item {
name: String,
price: u32,
}
#[derive(Clone)]
struct Order {
item: Item,
count: u32,
}
fn main() {
let order = Order {
item: Item {
name: String::from("コーヒー"),
price: 500,
},
count: 2,
};
let spare = order.clone();
println!("{} ×{}", order.item.name, order.count);
println!("{} ×{}", spare.item.name, spare.count);
}Playgroundで開く解答例と解説
#[derive(Clone)]
struct Item {
name: String,
price: u32,
}
#[derive(Clone)]
struct Order {
item: Item,
count: u32,
}
fn main() {
let order = Order {
item: Item {
name: String::from("コーヒー"),
price: 500,
},
count: 2,
};
let spare = order.clone();
println!("{} ×{}", order.item.name, order.count);
println!("{} ×{}", spare.item.name, spare.count);
}Playgroundで開くエラーメッセージはthe trait bound 'Item: Clone' is not satisfied(E0277)で、エラーの位置はOrderのitem: Itemフィールドです。Orderに#[derive(Clone)]を付けているのに、エラーになっています。
理由は、問題01で見たderiveの生成コードを思い出すと分かります。OrderのClone実装は、各フィールドに対して.clone()を呼びます。
impl Clone for Order {
fn clone(&self) -> Self {
Order {
item: self.item.clone(), // ItemがCloneを実装していないと呼べない
count: self.count.clone(),
}
}
}self.item.clone()が呼べるためには、ItemがCloneを実装していなければなりません。つまりderiveはフィールドの型に同じトレイトの実装を要求します。u32やStringのような標準ライブラリの型はたいてい実装済みなので意識せずに済みますが、フィールドが自作の型になったとたん、この条件が表に出てきます。
修正は、内側のItemにも#[derive(Clone)]を付けることです。これでOrderの生成コードが成立し、.clone()が呼べるようになります。
この条件はCloneに限らず、問題01の表にあるどのトレイトでも同じです。Orderに#[derive(Debug)]を付けるならItemにもDebugが、#[derive(PartialEq)]を付けるならItemにもPartialEqが必要です。列挙型の場合は、全バリアントが抱えるデータの型すべてが対象になります。
05 - PartialOrdで大小比較
比較のトレイトとderive属性と比較演算子に関する問題です。
次のコードはコンパイルエラー(E0369)になります。Versionを<や>で比較できるように定義に1行足し、期待する出力と一致させてください。大小はmajorを先に比べ、同じならminorで決めます。
1.9 < 2.0: true
2.0 > 2.1: false
1.9 < 1.10: true// <や>で比較できるようにせよ
struct Version {
major: u32,
minor: u32,
}
fn main() {
let v1 = Version { major: 1, minor: 9 };
let v2 = Version { major: 2, minor: 0 };
let v3 = Version { major: 2, minor: 1 };
let v4 = Version { major: 1, minor: 10 };
println!("1.9 < 2.0: {}", v1 < v2);
println!("2.0 > 2.1: {}", v2 > v3);
println!("1.9 < 1.10: {}", v1 < v4);
}Playgroundで開く解答例と解説
// <や>で比較できるようにせよ
#[derive(PartialEq, PartialOrd)]
struct Version {
major: u32,
minor: u32,
}
fn main() {
let v1 = Version { major: 1, minor: 9 };
let v2 = Version { major: 2, minor: 0 };
let v3 = Version { major: 2, minor: 1 };
let v4 = Version { major: 1, minor: 10 };
println!("1.9 < 2.0: {}", v1 < v2);
println!("2.0 > 2.1: {}", v2 > v3);
println!("1.9 < 1.10: {}", v1 < v4);
}Playgroundで開く<・<=・>・>=の4つの比較演算子はPartialOrdトレイトが担当します。==と同じく、a < bはPartialOrdのメソッド呼び出しに展開されるため、実装のない型には使えません(付けずに実行するとE0369です)。
#[derive(PartialOrd)]が生成するのは、フィールドを宣言順に比べ、最初に差がついたフィールドで大小を決める実装です。辞書の並び順と同じなので辞書式順序と呼びます。Versionではmajorが先に宣言されているのでmajorで決まり、同じときだけminorで決まります。1.9 < 1.10がtrueになるのは、minor同士を9 < 10と数値で比べているからです。
つまりフィールドの宣言順が比較の優先順位になります。minorを先に宣言すると結果が変わるので、deriveで順序を導出するときはフィールドの並びに意味があると意識してください。
PartialOrdは単独では付けられず、PartialEqとセットで書く必要があります。PartialOrdはPartialEqを前提とするトレイト(スーパートレイト)として定義されているためで、PartialEqを省くとE0277になります。
Partialが付く理由
PartialOrdの「部分的」という名前は、「比較しても大小が決まらない組み合わせがあってもよい」という意味です。代表例が浮動小数点型のNaN(非数。0.0 / 0.0などの結果)で、NaNはどの値と比べても<も>も==もfalseになります。NaN == NaNすらfalseです。
そのためf64やf32はPartialEqとPartialOrdだけを実装し、「必ずa == aが成り立つ」と宣言するEq、「どの2つを比べても必ず決着が付く」と宣言するOrdは実装していません。ここから、自作の型でも**f64のフィールドを持つ型にはEqやOrdを導出できない**という制約が生まれます。
#[derive(PartialEq, Eq)] // エラー: E0277(f64はEqを実装していない)
struct Weight {
kg: f64,
}Versionのように整数だけでできた型ならEqとOrdも導出できます。VecのsortメソッドのようにOrdを要求する場面があるので、整数だけの型には#[derive(PartialEq, Eq, PartialOrd, Ord)]と4つまとめて付けておくのが定番です。
06 - Displayトレイトを実装する
Displayトレイトとトレイトとマクロに関する問題です。
次のコードはコンパイルエラー(E0277)になります。Itemを{}で出力できるようにしてください。deriveでは解決できません。
コーヒー(500円)struct Item {
name: String,
price: u32,
}
// Itemを「〇〇(△△円)」の形で{}出力できるようにせよ
fn main() {
let item = Item {
name: String::from("コーヒー"),
price: 500,
};
println!("{}", item);
}Playgroundで開く解答例と解説
use std::fmt;
struct Item {
name: String,
price: u32,
}
// Itemを「〇〇(△△円)」の形で{}出力できるようにせよ
impl fmt::Display for Item {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}({}円)", self.name, self.price)
}
}
fn main() {
let item = Item {
name: String::from("コーヒー"),
price: 500,
};
println!("{}", item);
}Playgroundで開くエラーメッセージは'Item' doesn't implement 'std::fmt::Display'(E0277)です。第9章の問題04で「#[derive(Debug)]を付けても{}では出力できない」と触れましたが、その理由がここで明らかになります。{:?}がDebugトレイトを呼ぶのに対し、{}はDisplayトレイトという別のトレイトを呼ぶからです。
Displayはderiveできません。エラーメッセージのnoteにもin format strings you may be able to use '{:?}' (or {:#?} for pretty-print) insteadとあり、deriveを勧める文言はありません。Debugは「型名とフィールドを機械的に並べる」という決まった形があるので自動生成できますが、Displayが担うのは利用者に見せる表示で、priceを「500」と出すか「500円」と出すか、nameを先にするか後にするかは型の意味によって変わります。コンパイラには決められないので、手で書きます。
実装の形
書くのは、第18章で学んだimpl トレイト名 for 型名そのものです。ただし見慣れない部品がいくつかあるので、1つずつ見ていきます。
use std::fmt;
impl fmt::Display for Item {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}({}円)", self.name, self.price)
}
}use std::fmt;—Displayは標準ライブラリのstd::fmtモジュールにあります。第14章で学んだとおり、モジュールごとuseしてfmt::Display・fmt::Formatter・fmt::Resultと書くのが慣例ですfn fmt(&self, f: &mut fmt::Formatter)—Displayが要求する唯一のメソッドです。fは出力先で、画面に直接書くのではなく、このfに書き込みます。println!やformat!が、このfを用意してfmtを呼び出しています-> fmt::Result— 戻り値は第12章のResult型です。fmt::ResultはResult<(), fmt::Error>の別名で、書き込みに成功したらOk(())、失敗したらErrを返しますwrite!(f, ...)—fに書き込むマクロです。println!が画面に出力するのに対し、write!は第1引数で指定した出力先に書き込みます。書式文字列の書き方はprintln!と同じです。戻り値がfmt::Resultなので、fmtメソッドの末尾の式としてそのまま返せます
Displayを実装すると、{}だけでなくformat!("{}", item)や、第6章で使った.to_string()もItemに対して使えるようになります。to_stringは「Displayを実装したすべての型」に対して標準ライブラリがまとめて用意しているメソッドだからです。
07 - Defaultトレイト
Defaultトレイトとderive属性と構造体更新記法に関する問題です。
次のコードはコンパイルエラー(E0599・E0063)になります。Settingsに既定値を作る機能を付け、コメントの指示どおりに2つのインスタンスを作って期待する出力と一致させてください。Settingsの既定値は、すべてのフィールドがその型の既定値であるものとします。
Settings { volume: 0, dark_mode: false, nickname: "", coupon: None }
Settings { volume: 0, dark_mode: true, nickname: "", coupon: None }#[derive(Debug)]
struct Settings {
volume: u32,
dark_mode: bool,
nickname: String,
coupon: Option<u32>,
}
fn main() {
// すべて既定値のインスタンスを作れ
let plain = Settings::default();
// dark_modeだけtrueにし、残りは既定値のインスタンスを作れ
let dark = Settings {
dark_mode: true,
};
println!("{:?}", plain);
println!("{:?}", dark);
}Playgroundで開く解答例と解説
#[derive(Debug, Default)]
struct Settings {
volume: u32,
dark_mode: bool,
nickname: String,
coupon: Option<u32>,
}
fn main() {
// すべて既定値のインスタンスを作れ
let plain = Settings::default();
// dark_modeだけtrueにし、残りは既定値のインスタンスを作れ
let dark = Settings {
dark_mode: true,
..Default::default()
};
println!("{:?}", plain);
println!("{:?}", dark);
}Playgroundで開く修正前のコードには2つのエラーがあります。Settings::default()という関数が見つからないこと(E0599)と、darkの初期化でフィールドが足りないこと(E0063)です。どちらもDefaultトレイトで解決します。
Defaultは、その型の既定値を返すdefault()という関連関数を1つだけ要求するトレイトです。標準ライブラリの多くの型が実装済みで、u32なら0、boolならfalse、Stringなら空文字列、OptionならNoneが既定値です。
#[derive(Default)]を付けると、全フィールドをそれぞれの型のdefault()で埋めた値を返す実装が生成されます。問題04で学んだとおり、全フィールドの型がDefaultを実装している必要がありますが、今回はすべて標準ライブラリの型なので条件を満たしています。これでSettings::default()が呼べるようになり、1行目の出力が得られます。
..Default::default()
第9章の問題05で学んだ構造体更新記法は、..baseと書くと指定しなかったフィールドをbaseから引き継ぐ記法でした。..Default::default()はそのbaseの位置に「既定値のインスタンス」を置いたものです。dark_modeだけ明示し、残りの3フィールドは既定値のインスタンスから引き継がれます。
Settings::default()ではなくDefault::default()と型名を省いて書けるのは、..の位置にはSettings型の値しか置けないと決まっているため、コンパイラが型推論で型を決められるからです。もちろん..Settings::default()と書いても構いません。
設定項目のように「たいていは既定値のままで、一部だけ変えたい」という型では、このderive(Default)と..Default::default()の組み合わせが定番です。フィールドが10個あっても、変えたい1個だけ書けば済みます。
08 - オーファンルール
トレイトとDisplayトレイトとタプル構造体に関する問題です。
次のコードはコンパイルエラー(E0117)になります。Vec<String>を{}で出力しようとする方針はあきらめ、自分で定義した型にDisplayを実装する形に書き換えてください。
タグ: rust, trait, deriveuse std::fmt;
impl fmt::Display for Vec<String> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "タグ: {}", self.join(", "))
}
}
fn main() {
let tags = vec![
String::from("rust"),
String::from("trait"),
String::from("derive"),
];
println!("{}", tags);
}Playgroundで開く解答例と解説
use std::fmt;
struct Tags(Vec<String>);
impl fmt::Display for Vec<String> {
impl fmt::Display for Tags {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "タグ: {}", self.join(", "))
write!(f, "タグ: {}", self.0.join(", "))
}
}
fn main() {
let tags = vec![
let tags = Tags(vec![
String::from("rust"),
String::from("trait"),
String::from("derive"),
];
]);
println!("{}", tags);
}Playgroundで開くエラーメッセージはonly traits defined in the current crate can be implemented for types defined outside of the crate(E0117)です。DisplayもVec<String>も標準ライブラリのもので、どちらも自分のクレートで定義した型ではありません。
Rustには、トレイトの実装を書ける場所に関して次の規則があります。
impl トレイト名 for 型名は、トレイトか型の少なくとも一方が自分のクレートで定義されているときだけ書ける
第18章では自作のトレイトをu32のような標準の型に実装しましたが、それはトレイトが自分のものだから許されていました。問題06でDisplayをItemに実装できたのは、型が自分のものだからです。今回はトレイトも型も外部のものなので、どちらの条件も満たしません。この規則をオーファンルール(孤児の規則)と呼びます。トレイトにも型にも「親」がいない実装、という意味です。
なぜこんな規則があるのかというと、実装の衝突を防ぐためです。もしこの規則がなければ、クレートAとクレートBがそれぞれVec<String>にDisplayを実装でき、両方に依存したプログラムでは「どちらの実装を使うのか」が決められなくなります。依存を1つ追加しただけでコンパイルが通らなくなる事態を避けるため、「1つの型に対する1つのトレイトの実装は、プログラム全体で1つに定まる」ことをこの規則が保証しています。
ニュータイプで回避する
それでも外部の型に外部のトレイトの振る舞いを与えたいときは、第9章の問題09で学んだニュータイプパターンを使います。struct Tags(Vec<String>);と、フィールドが1つだけのタプル構造体でVec<String>を包みます。Tagsは自分のクレートの型なので、Displayを実装しても規則に反しません。
中身のVec<String>にはself.0でアクセスします。join(", ")は、ベクタの要素を区切り文字でつないで1つのStringにするメソッドです。
ニュータイプで包むと、Vecのメソッドはtags.0.len()のように.0を経由して呼ぶ必要があります。少し手間ですが、「Tagsは単なる文字列のベクタではなく、タグの一覧という意味を持った型だ」と宣言できる利点もあり、実務でもよく使われる手法です。