Rustのよく使うトレイト
トレイトのページでは、自分でトレイトを作りました。でも標準ライブラリには、ほとんどすべてのRustプログラムが使うトレイトが最初から用意されています。これらは、ある型についての簡単な質問に答えてくれます。表示できる? コピーできる? 比較できる? 空の値を作れる?
制服についている「バッジ」のようなものだと考えてください。バッジ1つ1つが「この型はXのやり方を知っています」と示しています。バッジの多くは #[derive(...)] を書くだけで手に入り、残りは手で書きます。このページでは、よく出会うトレイトを順番に見ていき、最後に、あとで見返せる表をまとめます。
Debug:開発者向けの表示
Section titled “Debug:開発者向けの表示”Debug を使うと、値を {:?} で表示できます。これは開発者であるあなたのためのもので、コードを書いたり直したりしながら値を確認するのに使います。手で書くことはほとんどなく、deriveするだけで十分です。大きな構造体なら、フィールドを1行ずつ表示する「見やすい」形式の {:#?} を使うと読みやすくなります。
#[derive(Debug)]struct Book { title: String, pages: u32,}
fn main() { let book = Book { title: String::from("Dune"), pages: 412 }; println!("{:?}", book); println!("{:#?}", book);}実行結果:
Book { title: "Dune", pages: 412 }Book { title: "Dune", pages: 412,}ほかの人のコードで見かけるかもしれない dbg! マクロも、Debug を使っています。
Display:ユーザー向けの表示
Section titled “Display:ユーザー向けの表示”Display は {} が使うトレイトです。こちらはユーザー向けのテキスト、つまり画面上で人が読むメッセージのためのものです。それがどんな見た目であるべきかをRustは推測できないので、deriveはできません。fmt::Display を実装して自分で書きます。
use std::fmt;
struct Book { title: String, author: String,}
impl fmt::Display for Book { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{} by {}", self.title, self.author) }}
fn main() { let book = Book { title: String::from("Dune"), author: String::from("Frank Herbert"), };
println!("{}", book);
let text = book.to_string(); println!("このテキストの長さ: {}", text.len());}実行結果:
Dune by Frank Herbertこのテキストの長さ: 21この例では本のタイトルと著者名、"{} by {}" を英語のままにしています。.len() はバイト数を数えるので、日本語(1文字3バイト)にすると結果の数字が変わってしまうからです。
write! は println! と同じように使えますが、画面ではなくフォーマッタ f に書き込みます。うれしいおまけとして、型に Display を実装すると、.to_string() が自動的に使えるようになります。Display は独自のエラー型のページでも、エラーにわかりやすいメッセージを付けるために登場します。
CloneとCopy
Section titled “CloneとCopy”どちらも値の複製を作りますが、そのやり方が違います。
Cloneは.clone()メソッドを提供します。自分で意図して呼ぶもので、Stringの中のテキストを全部コピーするといった、実際の処理を伴うことがあります。Copyは、値を代入したり渡したりするたびに自動的に複製を作ります。ビット単位でそのままコピーできる、小さくて単純な型にしか使えません。数値、bool、char、そしてそれらだけでできた構造体です。
#[derive(Debug, Clone, Copy)]struct Point { x: i32, y: i32,}
#[derive(Debug, Clone)]struct User { name: String,}
fn main() { let p1 = Point { x: 1, y: 2 }; let p2 = p1; // Copy: p1はまだ使える println!("{:?} と {:?}", p1, p2);
let u1 = User { name: String::from("Ana") }; let u2 = u1.clone(); // Clone: 自分からコピーを頼む println!("{:?} と {:?}", u1, u2);}実行結果:
Point { x: 1, y: 2 } と Point { x: 1, y: 2 }User { name: "Ana" } と User { name: "Ana" }Copy がなければ、let p2 = p1; は p1 をムーブしてしまい、その後は p1 を使えなくなります(所有権を参照)。なお、Copy には必ず Clone も必要です。だから2つをいっしょにderiveするのです。
PartialEqとEq:== で比較する
Section titled “PartialEqとEq:== で比較する”PartialEq を使うと、== と != が使えるようになります。deriveした場合、すべてのフィールドが等しければ、2つの値は等しいとみなされます。
#[derive(Debug, PartialEq)]struct Point { x: i32, y: i32,}
fn main() { let a = Point { x: 1, y: 2 }; let b = Point { x: 1, y: 2 }; let c = Point { x: 5, y: 0 };
println!("a == b: {}", a == b); println!("a != c: {}", a != c);}実行結果:
a == b: truea != c: trueでは Eq とは何でしょうか? 新しいメソッドは何も追加しません。Eq は「どの値も、常に自分自身と等しい」という約束です。当たり前に聞こえますが、浮動小数点数はこれを破ります。Rustでは f64::NAN == f64::NAN は false なのです。そのため f64 は PartialEq を持っていますが、Eq は持っていません。自分の型をHashMapのキーにしたり、HashSetに入れたりするには、Eq(と Hash)が必要です。構造体に浮動小数点数のフィールドがなければ、両方deriveしておきましょう。
PartialOrdとOrd:< と > で比較する
Section titled “PartialOrdとOrd:< と > で比較する”PartialOrd を使うと、<、>、<=、>= が使えるようになります。Ord は完全な順序を追加するもので、.sort()、.max()、.min() に必要です。deriveした場合、Rustはフィールドを上から順に比較します。まず最初のフィールドを比べ、それが等しいときだけ次のフィールドを見ます。
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]struct Version { major: u32, minor: u32, patch: u32,}
fn main() { let old = Version { major: 1, minor: 2, patch: 0 }; let new = Version { major: 1, minor: 10, patch: 3 }; println!("old < new: {}", old < new);
let mut versions = vec![ Version { major: 2, minor: 0, patch: 0 }, new, old, ]; versions.sort();
for v in &versions { println!("{}.{}.{}", v.major, v.minor, v.patch); }}実行結果:
old < new: true1.2.01.10.32.0.0フィールドは順番に比較されるので、フィールドを書く順番が重要です。ここでは major がいちばん重要で、バージョン番号としてはまさに正しい順序です。enumの場合は、先に書いたバリアントほど「小さい」とみなされます。Eq と同じく、浮動小数点数には PartialOrd だけがあり、Ord はありません。それでも浮動小数点数を並べ替える方法は、ソートのページで紹介しています。
Default:ちょうどいい初期値
Section titled “Default:ちょうどいい初期値”Default は、型に「初期値」を作る ::default() 関数を与えます。deriveした場合、各フィールドはそれぞれのデフォルト値になります。数値なら 0、bool なら false、空の String、空の Vec です。..Default::default() という構文を使えば、いくつかのフィールドだけを指定して、残りを埋められます。
#[derive(Debug, Default)]struct Settings { volume: u32, muted: bool, username: String,}
fn main() { let basic = Settings::default(); println!("{:?}", basic);
let custom = Settings { volume: 80, ..Default::default() }; println!("{:?}", custom);}実行結果:
Settings { volume: 0, muted: false, username: "" }Settings { volume: 80, muted: false, username: "" }違う初期値(たとえば音量50)にしたい場合は、deriveを使わずに impl Default for Settings を自分で書き、欲しい値を返す fn default() -> Self を用意しましょう。
FromとInto:型どうしの変換
Section titled “FromとInto:型どうしの変換”From は、ある型から別の型を作る方法を表します。実はもう使っています。String::from("hi") がそうです。From を実装すると、逆方向の Into が自動的に手に入ります。
struct Celsius(f64);struct Fahrenheit(f64);
impl From<Celsius> for Fahrenheit { fn from(c: Celsius) -> Self { Fahrenheit(c.0 * 9.0 / 5.0 + 32.0) }}
fn main() { let boiling = Fahrenheit::from(Celsius(100.0)); println!("水が沸騰する温度: {}°F", boiling.0);
let body: Fahrenheit = Celsius(37.0).into(); println!("体温: {:.1}°F", body.0);}実行結果:
水が沸騰する温度: 212°F体温: 98.6°F.into() を使うときは、Rustが変換先の型を知る必要があります。だから let body: Fahrenheit と型を書いているのです。
? は From を使っています。From が本当に活躍するのはここです。? 演算子はエラーに出会うと、From::from を呼んで、そのエラーを関数が返すエラー型に変換します。つまり、自分のエラー型に From を実装しておけば、? がエラーを変換してくれるのです。
use std::num::ParseIntError;
#[derive(Debug)]enum AppError { BadNumber(ParseIntError),}
impl From<ParseIntError> for AppError { fn from(e: ParseIntError) -> Self { AppError::BadNumber(e) }}
fn parse_age(text: &str) -> Result<u32, AppError> { let age: u32 = text.parse()?; // ここでParseIntErrorがAppErrorに変わる Ok(age)}
fn main() { println!("{:?}", parse_age("42")); println!("{:?}", parse_age("abc"));}実行結果:
Ok(42)Err(BadNumber(ParseIntError { kind: InvalidDigit }))独自のエラー型のページでは、この考え方をさらに詳しく掘り下げています。
Addで演算子をオーバーロードする
Section titled “Addで演算子をオーバーロードする”Rustでは、a + b は実は std::ops::Add トレイトの add メソッドの呼び出しです。自分の型にこれを実装すれば、+ がそのまま使えるようになります。type Output は、足し算の結果がどの型になるかを表します。
use std::ops::Add;
#[derive(Debug, Clone, Copy, PartialEq)]struct Point { x: i32, y: i32,}
impl Add for Point { type Output = Point;
fn add(self, other: Point) -> Point { Point { x: self.x + other.x, y: self.y + other.y } }}
fn main() { let a = Point { x: 1, y: 2 }; let b = Point { x: 3, y: 4 }; println!("{:?}", a + b);}実行結果:
Point { x: 4, y: 6 }同じ考え方は、Sub(-)、Mul(*)、Neg(単項の -)、AddAssign(+=)にも使えます。2つの点を足す、2つの金額を足すといった、意味が明らかなときに使いましょう。あなたの型にとって + が何を意味するのか、読む人が推測しなければならないなら、わかりやすい名前を付けた普通のメソッドのほうがよいでしょう。
どのトレイトをderiveすればいい?
Section titled “どのトレイトをderiveすればいい?”よくある構造体のための早見表です。
| トレイト | deriveする? | 得られるもの |
|---|---|---|
Debug |
ほぼ常に | {:?} と {:#?} での表示 |
Clone |
たいてい | .clone() |
Copy |
String、Vec、Box のフィールドを持たない小さな型だけ |
ムーブの代わりに自動でコピー |
PartialEq |
たいてい | == と != |
Eq |
浮動小数点数のフィールドがないとき | HashMap のキーに必要(Hash とともに) |
Hash |
HashMap のキーや HashSet に使うとき |
ハッシュ化 |
PartialOrd、Ord |
値に自然な順序があるとき | <、>、.sort()、.max() |
Default |
「空」の値に意味があるとき | ::default() と ..Default::default() |
Display |
deriveできない。手で書く | {} での表示と .to_string() |
最初の1行としてとてもよく使われるのが #[derive(Debug, Clone, PartialEq)] です。必要になったら追加していきましょう。足りないものがあれば、コンパイラが教えてくれます。
よくある間違い
Section titled “よくある間違い”String を持つ構造体に Copy をderiveする
#[derive(Clone, Copy)]struct User { name: String, age: u32,}error[E0204]: the trait `Copy` cannot be implemented for this type --> src/main.rs:2:8 |1 | #[derive(Clone, Copy)] | ---- in this derive macro expansion2 | struct User { | ^^^^3 | name: String, | ------------ this field does not implement `Copy`「この型には Copy トレイトを実装できません」「このフィールドは Copy を実装していません」という意味です。String はヒープ上のメモリを所有しているので、ビット単位でコピーすることはできません。Copy を外して、複製が必要なときは .clone() を使いましょう。
浮動小数点数を持つ構造体に Eq をderiveする
#[derive(Debug, PartialEq, Eq)]struct Product { name: String, price: f64 }error[E0277]: the trait bound `f64: Eq` is not satisfied --> src/main.rs:2:32 |1 | #[derive(Debug, PartialEq, Eq)] | -- in this derive macro expansion2 | struct Product { name: String, price: f64 } | ^^^^^^^^^^ the trait `Eq` is not implemented for `f64`「f64 は Eq を実装していません」という意味です。PartialEq は残して、Eq を外しましょう。あるいは、価格を u64 で「銭」や「セント」単位の整数として保存する方法もあります。お金を扱うなら、こちらのほうが安全です。
1. ToDoリストを並べ替える
Section titled “1. ToDoリストを並べ替える”priority: u8 と title: String を持つ Task 構造体を作ってください。Vec<Task> を並べ替えられて、各タスクを {:?} で表示できて、二つのタスクを == で比較できるように、必要なトレイトをderiveします。数字が小さいほど急ぎなので、先に来るようにしてください。ヒント:.sort() には Ord が必要で、Ord には Eq、PartialEq、PartialOrd も必要です。priority を最初に置けば、それが最初に比較されます。
解答を見る
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]struct Task { priority: u8, title: String,}
fn main() { let mut tasks = vec![ Task { priority: 3, title: String::from("植物に水をやる") }, Task { priority: 1, title: String::from("家賃を払う") }, Task { priority: 2, title: String::from("サムに返信する") }, ];
let copy = tasks[1].clone(); println!("同じタスク? {}", copy == tasks[1]);
tasks.sort(); for task in &tasks { println!("{:?}", task); }}実行結果:
同じタスク? trueTask { priority: 1, title: "家賃を払う" }Task { priority: 2, title: "サムに返信する" }Task { priority: 3, title: "植物に水をやる" }2. 表示できて足し算できるお金
Section titled “2. 表示できて足し算できるお金”セント単位の整数 cents: u64 を保存する Money 構造体を作ってください。350.into() が使えるように From<u64> を、$3.50 のように表示されるように Display を、二つの金額を + で足せるように Add を実装します。ヒント:ドルは cents / 100、残りは cents % 100 です。5セントが 05 と表示されるように、残りは {:02} で表示します。足し算のあとも金額を表示できるように、Copy もderiveしておきましょう。
解答を見る
use std::fmt;use std::ops::Add;
#[derive(Debug, Clone, Copy, PartialEq)]struct Money { cents: u64,}
impl From<u64> for Money { fn from(cents: u64) -> Self { Money { cents } }}
impl fmt::Display for Money { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "${}.{:02}", self.cents / 100, self.cents % 100) }}
impl Add for Money { type Output = Money;
fn add(self, other: Money) -> Money { Money { cents: self.cents + other.cents } }}
fn main() { let coffee: Money = 350.into(); let cake = Money::from(405); let total = coffee + cake;
println!("コーヒー: {coffee}"); println!("ケーキ: {cake}"); println!("合計: {total}");}実行結果:
コーヒー: $3.50ケーキ: $4.05合計: $7.55Debug({:?})は開発者向けで、ほぼ常にderiveします。Display({})はユーザー向けで、手で書きます。Cloneは.clone()で頼んだときにコピーします。Copyは自動でコピーしますが、単純な型にしか使えません。PartialEq/Eqで==が使えます。PartialOrd/Ordで<や並べ替えが使え、フィールドは上から順に比較されます。Defaultは初期値を作ります。..Default::default()は、指定しなかったフィールドを埋めてくれます。Fromを実装すればIntoは自動でついてきます。?演算子はエラーの変換にFromを使います。std::ops::Add(とその仲間)を実装すると、自分の型で+が使えるようになります。
もっと先へ進みたいですか? 高度なトレイトでは、type Output のような関連型、デフォルト型引数、そして自分が書いていない型にトレイトを実装する方法を説明しています。
理解度チェック
Section titled “理解度チェック”理解度チェック
1. println! の {} が使うトレイトはどれですか?
2. String のフィールドを持つ構造体が Copy をderiveできないのはなぜですか?
3. PartialOrd をderiveすると、2つの構造体はどのように比較されますか?
4. From<ParseIntError> for AppError を実装しました。これで ? は何ができるようになりますか?
5. 自分の型で a + b を使えるようにするには、どのトレイトを実装しますか?
スコア: 0 / 5
さらに詳しく
Section titled “さらに詳しく”- 導出可能なトレイト — The Rust Programming Language 日本語版(付録C):
#[derive]できるすべてのトレイトについて、deriveした実装が何をするのか、いつ使いたくなるのかを説明しています。 std::fmt— フォーマットとDisplay(英語):幅、パディング、{:#?}などすべてのフォーマットオプションと、DisplayとDebugの位置づけ。FromとInto— Rust by Example 日本語版:自分の型どうしを変換する、短くて実行できる例。std::ops— 演算子のトレイト(英語):AddからIndexまで、オーバーロードできる演算子の一覧。std::cmp— 比較のトレイト(英語):PartialEq、Eq、PartialOrd、Ordの詳細と、それぞれが約束するルール。