高度なトレイト
トレイトについては、第10章の「トレイトによる共有動作の定義」節で最初に取り上げましたが、より高度な詳細については説明しませんでした。ここまでで Rust についてより多くを学んだので、今度は細かなところまで踏み込めます。
関連型を持つトレイトの定義
関連型 は、型のプレースホルダーをトレイトに結び付けることで、トレイトのメソッド定義がシグネチャの中でそれらのプレースホルダー型を使えるようにするものです。トレイトの実装者は、その実装においてプレースホルダー型の代わりに使う具体的な型を指定します。これにより、トレイトが実装されるまでそれらの型が正確には何であるかを知らなくても、いくつかの型を使うトレイトを定義できます。
この章で取り上げる高度な機能の大半は、必要になることがまれだと説明してきました。関連型はその中間にあります。つまり、本書の他の部分で説明した機能より使われる頻度は低い一方で、この章で扱う他の多くの機能よりは一般的に使われます。
関連型を持つトレイトの一例は、標準ライブラリが提供する Iterator トレイトです。関連型の名前は Item で、Iterator トレイトを実装する型が反復処理する値の型を表します。Iterator トレイトの定義は、リスト 20-13 に示すとおりです。
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
型 Item はプレースホルダーであり、next メソッドの定義は、それが Option<Self::Item> 型の値を返すことを示しています。Iterator トレイトの実装者は Item に対する具体的な型を指定し、next メソッドはその具体的な型の値を含む Option を返します。
関連型はジェネリクスと似た概念に見えるかもしれません。後者は、どの型を扱えるかを指定せずに関数を定義できるからです。この 2 つの概念の違いを確認するために、Item 型が u32 であることを指定した、Counter という名前の型に対する Iterator トレイトの実装を見てみましょう。
struct Counter {
count: u32,
}
impl Counter {
fn new() -> Counter {
Counter { count: 0 }
}
}
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
// --snip--
if self.count < 5 {
self.count += 1;
Some(self.count)
} else {
None
}
}
}
この構文は、ジェネリクスの構文と似ているように見えます。では、なぜリスト 20-14 に示すように、単に Iterator トレイトをジェネリクスで定義しないのでしょうか。
pub trait Iterator<T> {
fn next(&mut self) -> Option<T>;
}
違いは、リスト 20-14 のようにジェネリクスを使う場合、各実装で型注釈を付けなければならないことです。Iterator<String> for Counter やそのほかの型についても実装できるため、Counter に対して Iterator の複数の実装を持てる可能性があります。言い換えると、トレイトがジェネリックパラメータを持つ場合、そのジェネリック型パラメータの具体的な型を毎回変えながら、1 つの型に対してそのトレイトを複数回実装できます。Counter に対して next メソッドを使うときは、どの Iterator の実装を使いたいのかを示すために型注釈を指定しなければなりません。
関連型を使う場合は、型注釈は必要ありません。というのも、1 つの型に対して同じトレイトを複数回実装することはできないからです。関連型を使う定義であるリスト 20-13 では、impl Iterator for Counter は 1 つしか存在できないため、Item の型として何を使うかを選べるのは 1 回だけです。Counter に対して next を呼び出すたびに、u32 値のイテレータが欲しいことを指定する必要はありません。
関連型は、トレイトの契約の一部にもなります。トレイトの実装者は、関連型プレースホルダーの代わりとなる型を提供しなければなりません。関連型には、その型がどのように使われるかを説明する名前が付いていることが多く、API ドキュメントでその関連型を説明するのはよい実践です。
デフォルトのジェネリック型パラメータと演算子のオーバーロードの使用
ジェネリック型パラメータを使うときは、そのジェネリック型に対してデフォルトの具体的な型を指定できます。これにより、デフォルトの型で問題ない場合、トレイトの実装者は具体的な型を指定する必要がなくなります。デフォルト型は、<PlaceholderType=ConcreteType> 構文を使ってジェネリック型を宣言するときに指定します。
この手法が役立つ状況の好例が、演算子のオーバーロード です。これは、特定の状況で演算子(+ など)の振る舞いをカスタマイズするものです。
Rust では、独自の演算子を作成したり、任意の演算子をオーバーロードしたりすることはできません。しかし、演算子に対応するトレイトを実装することで、std::ops に列挙されている演算と対応トレイトをオーバーロードできます。たとえばリスト 20-15 では、2 つの Point インスタンスを加算するように + 演算子をオーバーロードしています。これは、Point 構造体に対して Add トレイトを実装することで行います。
use std::ops::Add;
#[derive(Debug, Copy, Clone, 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() {
assert_eq!(
Point { x: 1, y: 0 } + Point { x: 2, y: 3 },
Point { x: 3, y: 3 }
);
}
add メソッドは、2 つの Point インスタンスの x 値と y 値をそれぞれ加算して、新しい Point を生成します。Add トレイトには Output という関連型があり、add メソッドから返される型を決定します。
このコードにおけるデフォルトのジェネリック型は、Add トレイトの中にあります。定義は次のとおりです。
#![allow(unused)]
fn main() {
trait Add<Rhs=Self> {
type Output;
fn add(self, rhs: Rhs) -> Self::Output;
}
}
このコードは、全体として見れば見慣れたもののはずです。1 つのメソッドと 1 つの関連型を持つトレイトです。新しい部分は Rhs=Self です。この構文は デフォルト型パラメータ と呼ばれます。Rhs ジェネリック型パラメータ(“right-hand side” の略)は、add メソッドの rhs パラメータの型を定義します。Add トレイトを実装するときに Rhs に具体的な型を指定しない場合、Rhs の型はデフォルトで Self になります。これは、Add を実装している対象の型です。
Point に対して Add を実装したとき、2 つの Point インスタンスを加算したかったので、Rhs にはデフォルト値を使いました。次に、デフォルトを使うのではなく Rhs 型をカスタマイズしたい場合の Add トレイト実装例を見てみましょう。
異なる単位の値を保持する 2 つの構造体 Millimeters と Meters があり
ます。既存の型を別の構造体で薄くラップするこの手法は newtype パター
ン と呼ばれ、これについては 「newtype パターンで外部トレイトを実装
する」 節でより詳しく説明します。ミリメート
ル単位の値とメートル単位の値を加算し、その際に Add の実装で正しく変
換が行われるようにしたいと考えています。リスト 20-16 に示すように、
Meters を Rhs として Millimeters に対して Add を実装できます。
use std::ops::Add;
struct Millimeters(u32);
struct Meters(u32);
impl Add<Meters> for Millimeters {
type Output = Millimeters;
fn add(self, other: Meters) -> Millimeters {
Millimeters(self.0 + (other.0 * 1000))
}
}
Millimeters と Meters を加算するには、Self というデフォルトを使
う代わりに、Rhs 型パラメータの値を設定するために impl Add<Meters>
を指定します。
デフォルト型パラメータの主な使い方は 2 つあります。
- 既存のコードを壊さずに型を拡張するため
- ほとんどのユーザーには不要な特定のケースでカスタマイズを可能にするため
標準ライブラリの Add トレイトは、2 つ目の目的の例です。
通常は同じ種類の 2 つの型を加算しますが、Add トレイトはそれを超えた
カスタマイズを可能にします。Add トレイトの定義でデフォルト型パラメー
タを使うことで、ほとんどの場合に追加のパラメータを指定する必要がなくな
ります。つまり、少しばかりの実装用ボイラープレートが不要になり、その分
トレイトを使いやすくなります。
1 つ目の目的は 2 つ目と似ていますが逆方向です。既存のトレイトに型パラ メータを追加したい場合、それにデフォルト値を与えることで、既存の実装 コードを壊さずにトレイトの機能を拡張できます。
同名のメソッド間の曖昧さを解消する
Rust では、あるトレイトが別のトレイトのメソッドと同じ名前のメソッドを 持つことを妨げるものは何もありませんし、1 つの型に対して両方のトレイト を実装することも妨げられません。さらに、トレイトのメソッドと同じ名前の メソッドを、その型自体に直接実装することもできます。
同じ名前のメソッドを呼び出すときは、どれを使いたいのかを Rust に伝える
必要があります。リスト 20-17 のコードを考えてみましょう。ここでは、どち
らも fly というメソッドを持つ 2 つのトレイト、Pilot と Wizard を
定義しています。次に、すでに fly という名前のメソッドが実装されている
型 Human に対して、その両方のトレイトを実装します。それぞれの fly
メソッドは異なることを行います。
trait Pilot {
fn fly(&self);
}
trait Wizard {
fn fly(&self);
}
struct Human;
impl Pilot for Human {
fn fly(&self) {
println!("This is your captain speaking.");
}
}
impl Wizard for Human {
fn fly(&self) {
println!("Up!");
}
}
impl Human {
fn fly(&self) {
println!("*waving arms furiously*");
}
}
fn main() {}
リスト 20-18 に示すように、Human のインスタンスに対して fly を呼び
出すと、コンパイラはデフォルトでその型に直接実装されたメソッドを呼び出
します。
trait Pilot {
fn fly(&self);
}
trait Wizard {
fn fly(&self);
}
struct Human;
impl Pilot for Human {
fn fly(&self) {
println!("This is your captain speaking.");
}
}
impl Wizard for Human {
fn fly(&self) {
println!("Up!");
}
}
impl Human {
fn fly(&self) {
println!("*waving arms furiously*");
}
}
fn main() {
let person = Human;
person.fly();
}
このコードを実行すると *waving arms furiously* と出力され、Rust が
Human に直接実装された fly メソッドを呼び出したことがわかります。
Pilot トレイトまたは Wizard トレイトの fly メソッドを呼び出すに
は、どの fly メソッドを意味しているのかを指定する、より明示的な構文を
使う必要があります。リスト 20-19 はこの構文を示しています。
trait Pilot {
fn fly(&self);
}
trait Wizard {
fn fly(&self);
}
struct Human;
impl Pilot for Human {
fn fly(&self) {
println!("This is your captain speaking.");
}
}
impl Wizard for Human {
fn fly(&self) {
println!("Up!");
}
}
impl Human {
fn fly(&self) {
println!("*waving arms furiously*");
}
}
fn main() {
let person = Human;
Pilot::fly(&person);
Wizard::fly(&person);
person.fly();
}
メソッド名の前にトレイト名を指定することで、fly のどの実装を呼び出し
たいのかが Rust に明確に伝わります。また、リスト 20-19 で使った
person.fly() と等価な Human::fly(&person) と書くこともできますが、
曖昧さを解消する必要がないのであれば、こちらは少し長くなります。
このコードを実行すると、次のように出力されます。
$ cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.46s
Running `target/debug/traits-example`
This is your captain speaking.
Up!
*waving arms furiously*
fly メソッドは self パラメータを取るため、1 つの トレイト を実装
する 2 つの 型 があったとしても、Rust は self の型に基づいて、どの
トレイト実装を使うべきかを判断できます。
しかし、メソッドではない関連関数には self パラメータがありません。
複数の型またはトレイトが同じ関数名の非メソッド関数を定義している場合、
完全修飾構文を使わない限り、Rust にはどの型を意味しているのかが常にわか
るとは限りません。たとえば、リスト 20-20 では、すべての子犬に Spot と
名付けたい動物保護施設のためのトレイトを作成します。関連する非メソッド
関数 baby_name を持つ Animal トレイトを作成します。Animal トレイ
トは構造体 Dog に対して実装されており、Dog 自体にも baby_name と
いう関連する非メソッド関数を直接定義しています。
trait Animal {
fn baby_name() -> String;
}
struct Dog;
impl Dog {
fn baby_name() -> String {
String::from("Spot")
}
}
impl Animal for Dog {
fn baby_name() -> String {
String::from("puppy")
}
}
fn main() {
println!("A baby dog is called a {}", Dog::baby_name());
}
Dog に定義された baby_name 関連関数には、すべての子犬を Spot と名付
けるためのコードを実装しています。Dog 型は、すべての動物が持つ特性を
記述する Animal トレイトも実装しています。犬の赤ちゃんは子犬と呼ばれ
ます。そのことは、Dog に対する Animal トレイト実装において、
Animal トレイトに関連付けられた baby_name 関数で表現されています。
main では Dog::baby_name 関数を呼び出します。これは Dog に定義さ
れた関連関数を直接呼び出します。このコードは次のように出力します。
$ cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.54s
Running `target/debug/traits-example`
A baby dog is called a Spot
この出力は望んでいたものではありません。コードが
A baby dog is called a puppy と出力されるように、Dog に実装した
Animal トレイトの一部である baby_name 関数を呼び出したいのです。
ここでは、リスト 20-19 で使ったトレイト名を指定する手法は役に立ちませ
ん。main をリスト 20-21 のコードに変更すると、コンパイルエラーになり
ます。
trait Animal {
fn baby_name() -> String;
}
struct Dog;
impl Dog {
fn baby_name() -> String {
String::from("Spot")
}
}
impl Animal for Dog {
fn baby_name() -> String {
String::from("puppy")
}
}
fn main() {
println!("A baby dog is called a {}", Animal::baby_name());
}
Animal::baby_name には self パラメータがなく、さらに Animal トレ
イトを実装するほかの型が存在する可能性もあるため、Rust には
Animal::baby_name のどの実装を使いたいのか判断できません。すると、次
のようなコンパイラエラーが発生します。
$ cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
error[E0790]: cannot call associated function on trait without specifying the corresponding `impl` type
--> src/main.rs:20:43
|
2 | fn baby_name() -> String;
| ------------------------- `Animal::baby_name` defined here
...
20 | println!("A baby dog is called a {}", Animal::baby_name());
| ^^^^^^^^^^^^^^^^^^^ cannot call associated function of trait
|
help: use the fully-qualified path to the only available implementation
|
20 | println!("A baby dog is called a {}", <Dog as Animal>::baby_name());
| +++++++ +
For more information about this error, try `rustc --explain E0790`.
error: could not compile `traits-example` (bin "traits-example") due to 1 previous error
曖昧さをなくし、他の型に対する Animal の実装ではなく Dog に対する
Animal の実装を使いたいことを Rust に伝えるには、完全修飾構文を使う必要が
あります。リスト20-22は、完全修飾構文の使い方を示しています。
trait Animal {
fn baby_name() -> String;
}
struct Dog;
impl Dog {
fn baby_name() -> String {
String::from("Spot")
}
}
impl Animal for Dog {
fn baby_name() -> String {
String::from("puppy")
}
}
fn main() {
println!("A baby dog is called a {}", <Dog as Animal>::baby_name());
}
山かっこ内で型注釈を Rust に与えることで、この関数呼び出しでは Dog 型を
Animal として扱いたい、つまり Dog に実装された Animal トレイトの
baby_name メソッドを呼び出したいことを示しています。このコードはこれで、
意図した内容を表示します。
$ cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.48s
Running `target/debug/traits-example`
A baby dog is called a puppy
一般に、完全修飾構文は次のように定義されます。
<Type as Trait>::function(receiver_if_method, next_arg, ...);
メソッドではない関連関数では、receiver はありません。
あるのは他の引数のリストだけです。関数やメソッドを呼び出すすべての場所で
完全修飾構文を使うことができます。しかし、Rust がプログラム内のほかの情報から
判断できる部分については、この構文のその部分を省略してかまいません。同じ名前を
使う実装が複数あり、どの実装を呼び出したいのかを Rust が特定するのに助けが
必要な場合にだけ、このより冗長な構文を使う必要があります。
スーパートレイトを使う
ときには、別のトレイトに依存するトレイト定義を書くことがあります。ある型が 最初のトレイトを実装するには、その型が2つ目のトレイトも実装していることを 要求したい場合です。そうするのは、そのトレイト定義の中で2つ目のトレイトの 関連要素を利用できるようにするためです。あなたのトレイト定義が依存している そのトレイトは、あなたのトレイトの スーパートレイト と呼ばれます。
たとえば、アスタリスクで囲まれるように整形して与えられた値を表示する
outline_print メソッドを持つ OutlinePrint トレイトを作りたいとします。
つまり、標準ライブラリのトレイト Display を実装して (x, y) という形式で
表示される Point 構造体があるとして、x が 1 で y が 3 の Point
インスタンスに対して outline_print を呼び出すと、次のように表示されるはず
です。
**********
* *
* (1, 3) *
* *
**********
outline_print メソッドの実装では、Display トレイトの機能を使いたいと
考えています。したがって、OutlinePrint トレイトが機能するのは、Display
も実装していて OutlinePrint が必要とする機能を提供する型だけであることを
指定する必要があります。これは、トレイト定義で OutlinePrint: Display と
指定することで行えます。このテクニックは、トレイトにトレイト境界を追加する
のに似ています。リスト20-23は、OutlinePrint トレイトの実装を示しています。
use std::fmt;
trait OutlinePrint: fmt::Display {
fn outline_print(&self) {
let output = self.to_string();
let len = output.len();
println!("{}", "*".repeat(len + 4));
println!("*{}*", " ".repeat(len + 2));
println!("* {output} *");
println!("*{}*", " ".repeat(len + 2));
println!("{}", "*".repeat(len + 4));
}
}
fn main() {}
OutlinePrint が Display トレイトを必要とすると指定したため、Display を
実装している任意の型に自動的に実装される to_string 関数を使えます。コロンを
追加してトレイト名の後に Display トレイトを指定せずに to_string を
使おうとすると、現在のスコープでは型 &Self に対して to_string という名前の
メソッドは見つからない、というエラーになります。
Point 構造体のように Display を実装していない型に対して OutlinePrint を
実装しようとすると、どうなるか見てみましょう。
use std::fmt;
trait OutlinePrint: fmt::Display {
fn outline_print(&self) {
let output = self.to_string();
let len = output.len();
println!("{}", "*".repeat(len + 4));
println!("*{}*", " ".repeat(len + 2));
println!("* {output} *");
println!("*{}*", " ".repeat(len + 2));
println!("{}", "*".repeat(len + 4));
}
}
struct Point {
x: i32,
y: i32,
}
impl OutlinePrint for Point {}
fn main() {
let p = Point { x: 1, y: 3 };
p.outline_print();
}
Display が必要だが実装されていない、というエラーになります。
$ cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
error[E0277]: `Point` doesn't implement `std::fmt::Display`
--> src/main.rs:20:23
|
20 | impl OutlinePrint for Point {}
| ^^^^^ the trait `std::fmt::Display` is not implemented for `Point`
|
note: required by a bound in `OutlinePrint`
--> src/main.rs:3:21
|
3 | trait OutlinePrint: fmt::Display {
| ^^^^^^^^^^^^ required by this bound in `OutlinePrint`
error[E0277]: `Point` doesn't implement `std::fmt::Display`
--> src/main.rs:24:7
|
24 | p.outline_print();
| ^^^^^^^^^^^^^ the trait `std::fmt::Display` is not implemented for `Point`
|
note: required by a bound in `OutlinePrint::outline_print`
--> src/main.rs:3:21
|
3 | trait OutlinePrint: fmt::Display {
| ^^^^^^^^^^^^ required by this bound in `OutlinePrint::outline_print`
4 | fn outline_print(&self) {
| ------------- required by a bound in this associated function
For more information about this error, try `rustc --explain E0277`.
error: could not compile `traits-example` (bin "traits-example") due to 2 previous errors
これを修正するには、次のように Point に Display を実装して、
OutlinePrint が要求する制約を満たします。
trait OutlinePrint: fmt::Display {
fn outline_print(&self) {
let output = self.to_string();
let len = output.len();
println!("{}", "*".repeat(len + 4));
println!("*{}*", " ".repeat(len + 2));
println!("* {output} *");
println!("*{}*", " ".repeat(len + 2));
println!("{}", "*".repeat(len + 4));
}
}
struct Point {
x: i32,
y: i32,
}
impl OutlinePrint for Point {}
use std::fmt;
impl fmt::Display for Point {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "({}, {})", self.x, self.y)
}
}
fn main() {
let p = Point { x: 1, y: 3 };
p.outline_print();
}
そうすれば、Point に対する OutlinePrint トレイトの実装は正常にコンパイル
され、Point インスタンスに対して outline_print を呼び出して、アスタリスクの
枠内に表示できます。
Newtype パターンで外部トレイトを実装する
第10章の「型にトレイトを実装する」節では、トレイトか型のいずれか、あるいはその両方が自分たちの クレートにローカルである場合にのみ、型にトレイトを実装できるという孤児規則に ついて説明しました。この制約は、タプル構造体で新しい型を作る newtype パターンを使うことで回避できます。(タプル構造体については、第5章の 「タプル構造体で別の型を作る」節で説明しました。) このタプル構造体は1つのフィールドを持ち、トレイトを実装したい型を薄く ラップするものになります。すると、このラッパー型は自分たちのクレートに ローカルになるので、そのラッパーに対してトレイトを実装できます。Newtype は Haskell プログラミング言語に由来する用語です。このパターンを使っても 実行時の性能ペナルティはなく、ラッパー型はコンパイル時に消去されます。
例として、Vec<T> に Display を実装したいとします。しかし、Display
トレイトも Vec<T> 型も自分たちのクレートの外で定義されているため、孤児規則に
よってこれを直接行うことはできません。そこで、Vec<T> のインスタンスを保持する
Wrapper 構造体を作ります。すると、リスト20-24に示すように、Wrapper に
Display を実装し、その Vec<T> の値を利用できます。
use std::fmt;
struct Wrapper(Vec<String>);
impl fmt::Display for Wrapper {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "[{}]", self.0.join(", "))
}
}
fn main() {
let w = Wrapper(vec![String::from("hello"), String::from("world")]);
println!("w = {w}");
}
Display の実装では、内部の Vec<T> にアクセスするために self.0 を
使っています。これは、Wrapper がタプル構造体であり、Vec<T> がその
タプルのインデックス0の要素だからです。これで、Wrapper に対して Display
トレイトの機能を使えるようになります。
この手法を使う欠点は、Wrapper は新しい型であるため、保持している
値のメソッドを持っていないことです。Vec<T> のすべてのメソッドを
Wrapper に直接実装し、それらのメソッドが self.0 に処理を委譲する
ようにする必要があります。そうすれば、Wrapper を Vec<T> とまったく
同じように扱えるようになります。新しい型に内部の型が持つすべての
メソッドを持たせたいのであれば、Wrapper に Deref トレイトを実装して
内部の型を返すようにするのが解決策になります(Deref トレイトの実装に
ついては、第15章の 「スマートポインタを通常の参照のように扱う」
節で説明しました)。内部の型のすべてのメソッドを Wrapper 型に持たせたく
ない場合、たとえば Wrapper 型の振る舞いを制限したい場合には、必要な
メソッドだけを手作業で実装しなければなりません。
この newtype パターンは、トレイトが関係しない場合でも役に立ちます。ここで 話題を変えて、Rust の型システムとやり取りするいくつかの高度な方法を見て いきましょう。