はじめに
Advanced Traitsへようこそ。この実験は、Rust Bookの一部です。LabEx で Rust のスキルを練習することができます。
この実験では、Rust をより深く理解した今、以前「Traits: Defining Shared Behavior」で扱ったトレイトのより高度な詳細について掘り下げます。
This tutorial is from open-source community. Access the source code
💡 このチュートリアルは英語版からAIによって翻訳されています。原文を確認するには、 ここをクリックしてください
Advanced Traitsへようこそ。この実験は、Rust Bookの一部です。LabEx で Rust のスキルを練習することができます。
この実験では、Rust をより深く理解した今、以前「Traits: Defining Shared Behavior」で扱ったトレイトのより高度な詳細について掘り下げます。
私たちは最初に「Traits: Defining Shared Behavior」でトレイトについて学びましたが、より高度な詳細については議論しませんでした。今、あなたは Rust についてもっと知っているので、細かいところに入ることができます。
関連型は、型のプレースホルダをトレイトと結び付けるもので、トレイトメソッドの定義ではこれらのプレースホルダ型をシグネチャに使用できます。トレイトの実装者は、特定の実装においてプレースホルダ型の代わりに使用する具体的な型を指定します。このようにして、トレイトが実装されるまで、それがどのような型であるかを正確に知る必要なく、いくつかの型を使用するトレイトを定義することができます。
この章で説明したほとんどの高度な機能は、ほとんど必要ないと説明してきました。関連型は中間的な位置にあります:この本の残りの部分で説明される機能よりも頻繁には使用されませんが、この章で議論される他の多くの機能よりも一般的に使用されます。
関連型を持つトレイトの 1 つの例は、標準ライブラリが提供するIterator
トレイトです。関連型はItem
と呼ばれ、Iterator
トレイトを実装する型が反復処理する値の型を表します。Iterator
トレイトの定義は、リスト 19-12 に示すとおりです。
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
リスト 19-12:関連型Item
を持つIterator
トレイトの定義
型Item
はプレースホルダであり、next
メソッドの定義は、それがOption<Self::Item>
型の値を返すことを示しています。Iterator
トレイトの実装者は、Item
の具体的な型を指定し、next
メソッドはその具体的な型の値を含むOption
を返します。
関連型は、ジェネリクスと似た概念のように見えるかもしれません。後者は、どのような型を処理できるかを指定することなく関数を定義できるためです。この 2 つの概念の違いを調べるために、Item
型がu32
であることを指定するCounter
という名前の型に対するIterator
トレイトの実装を見てみましょう:
ファイル名:src/lib.rs
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
--snip--
この構文はジェネリクスのそれと似ています。では、なぜリスト 19-13 に示すようにジェネリクスを使ってIterator
トレイトを定義しないのでしょうか?
pub trait Iterator<T> {
fn next(&mut self) -> Option<T>;
}
リスト 19-13:ジェネリクスを使ったIterator
トレイトの仮想的な定義
違いは、リスト 19-13 のようにジェネリクスを使用する場合、各実装で型を注釈する必要があることです。なぜなら、Counter
に対してIterator<String>
を実装することもできるし、他の任意の型に対しても実装できるため、Counter
に対するIterator
の複数の実装が可能になるからです。言い換えると、トレイトにジェネリックパラメータがある場合、それはある型に対して複数回実装でき、そのたびにジェネリック型パラメータの具体的な型を変更できます。Counter
でnext
メソッドを使用する場合、使用したいIterator
の実装を示すために型注釈を提供する必要があります。
関連型を使用すると、型を注釈する必要がなくなります。なぜなら、ある型に対してトレイトを複数回実装することはできないからです。関連型を使用する定義のリスト 19-12 では、Item
の型をどのようにするかを一度だけ選ぶことができます。なぜなら、Counter
に対するimpl Iterator
は 1 つだけであるからです。Counter
でnext
を呼び出す場所すべてで、u32
値の反復子を必要とすることを指定する必要はありません。
関連型はまた、トレイトの契約の一部にもなります。トレイトの実装者は、関連型のプレースホルダに代わる型を提供する必要があります。関連型は、その型がどのように使用されるかを表す名前を持つことが多く、API ドキュメントにおいて関連型を文書化するのは良い慣例です。
ジェネリック型パラメータを使用する場合、ジェネリック型に対してデフォルトの具体的な型を指定することができます。これにより、デフォルトの型が機能する場合、トレイトの実装者が具体的な型を指定する必要がなくなります。<
プレースホルダ型=
具体的な型>
の構文を使ってジェネリック型を宣言する際に、デフォルトの型を指定します。
この技術が役立つ状況の良い例は、演算子のオーバーロードです。これは、特定の状況下で演算子(たとえば+
)の動作をカスタマイズするものです。
Rust では、独自の演算子を作成したり、任意の演算子をオーバーロードしたりすることはできません。ただし、std::ops
に列挙されている演算と対応するトレイトを、その演算子に関連付けられたトレイトを実装することでオーバーロードすることができます。たとえば、リスト 19-14 では、2 つのPoint
インスタンスを加算するために+
演算子をオーバーロードしています。これは、Point
構造体に対してAdd
トレイトを実装することで行っています。
ファイル名:src/main.rs
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 }
);
}
リスト 19-14:Point
インスタンス用に+
演算子をオーバーロードするためにAdd
トレイトを実装する
add
メソッドは、2 つのPoint
インスタンスのx
値と 2 つのPoint
インスタンスのy
値を加算して、新しいPoint
を作成します。Add
トレイトには、add
メソッドから返される型を決定するOutput
という名前の関連型があります。
このコードのデフォルトのジェネリック型は、Add
トレイトの中にあります。以下はその定義です:
trait Add<Rhs=Self> {
type Output;
fn add(self, rhs: Rhs) -> Self::Output;
}
このコードは一般的におなじみのものに見えるはずです:1 つのメソッドと関連型を持つトレイトです。新しい部分はRhs=Self
です。この構文はデフォルト型パラメータと呼ばれます。Rhs
ジェネリック型パラメータ(「右手側」の略)は、add
メソッドのrhs
パラメータの型を定義します。Add
トレイトを実装する際にRhs
に具体的な型を指定しない場合、Rhs
の型はデフォルトでSelf
になります。これは、Add
を実装している型になります。
Point
に対してAdd
を実装する際、2 つのPoint
インスタンスを加えたかったので、Rhs
のデフォルトを使用しました。Rhs
型をカスタマイズしたい場合、デフォルトを使用しないAdd
トレイトの実装の例を見てみましょう。
異なる単位の値を保持する 2 つの構造体Millimeters
とMeters
があります。既存の型を別の構造体で薄くラップすることは、ニュータイプパターンと呼ばれ、「外部の型に対して外部のトレイトを実装するためのニュータイプパターンの使用」で詳しく説明しています。ミリメートルの値とメートルの値を加え、Add
の実装が正しく変換するようにしたいと思います。Rhs
としてMeters
を使ってMillimeters
に対してAdd
を実装することができます。リスト 19-15 を参照してください。
ファイル名:src/lib.rs
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))
}
}
リスト 19-15:Millimeters
とMeters
を加算するためにMillimeters
に対してAdd
トレイトを実装する
Millimeters
とMeters
を加えるには、Self
のデフォルトを使用する代わりに、Rhs
型パラメータの値を設定するためにimpl Add<Meters>
を指定します。
デフォルト型パラメータは主に 2 つの方法で使用します。
標準ライブラリのAdd
トレイトは、2 番目の目的の例です。通常、同じ型の 2 つを加えますが、Add
トレイトはそれ以上のカスタマイズ機能を提供します。Add
トレイトの定義でデフォルト型パラメータを使用することで、ほとんどの場合に余分なパラメータを指定する必要がなくなります。言い換えると、実装のボイラープレートが不要になり、トレイトの使用が容易になります。
最初の目的は、2 番目と似ていますが逆です。既存のトレイトに型パラメータを追加したい場合、既存の実装コードを破壊することなくトレイトの機能を拡張できるように、デフォルトを与えることができます。
Rust では、あるトレイトが別のトレイトのメソッドと同じ名前のメソッドを持つことを妨げるものはありませんし、Rust はまた、ある型に対して両方のトレイトを実装することも妨げません。また、トレイトのメソッドと同じ名前のメソッドを型に直接実装することも可能です。
同名のメソッドを呼び出す際、どのメソッドを使用したいかを Rust に伝える必要があります。リスト 19-16 のコードを考えてみましょう。ここでは、Pilot
とWizard
の 2 つのトレイトを定義しており、両方ともfly
という名前のメソッドを持っています。そして、既にfly
という名前のメソッドが実装されているHuman
型に対して、両方のトレイトを実装しています。各fly
メソッドは異なることを行います。
ファイル名:src/main.rs
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*");
}
}
リスト 19-16:fly
メソッドを持つ 2 つのトレイトを定義し、Human
型に実装し、Human
に直接fly
メソッドを実装する
Human
のインスタンスでfly
を呼び出すと、コンパイラはデフォルトで型に直接実装されているメソッドを呼び出します。リスト 19-17 を参照してください。
ファイル名:src/main.rs
fn main() {
let person = Human;
person.fly();
}
リスト 19-17:Human
のインスタンスでfly
を呼び出す
このコードを実行すると、*waving arms furiously*
が表示され、Rust がHuman
に直接実装されているfly
メソッドを呼び出したことがわかります。
Pilot
トレイトまたはWizard
トレイトのfly
メソッドを呼び出すには、どのfly
メソッドを意味するのかを指定するために、より明示的な構文を使用する必要があります。リスト 19-18 にこの構文を示します。
ファイル名:src/main.rs
fn main() {
let person = Human;
Pilot::fly(&person);
Wizard::fly(&person);
person.fly();
}
リスト 19-18:どのトレイトのfly
メソッドを呼び出したいかを指定する
メソッド名の前にトレイト名を指定することで、Rust にどのfly
の実装を呼び出したいのかを明確にします。また、Human::fly(&person)
と書くこともできます。これは、リスト 19-18 で使用したperson.fly()
に相当しますが、曖昧さを解消する必要がない場合は、こちらの方が書くのが少し長くなります。
このコードを実行すると、以下が表示されます。
This is your captain speaking.
Up!
*waving arms furiously*
fly
メソッドはself
パラメータを取るため、2 つの型が両方とも 1 つのトレイトを実装している場合、Rust はself
の型に基づいてどのトレイトの実装を使用するかを判断することができます。
ただし、メソッドではない関連関数にはself
パラメータがありません。同じ関数名を持つ非メソッド関数を定義する複数の型またはトレイトがある場合、Rust は完全修飾構文を使用しない限り、どの型を意味するのかを常に把握できません。たとえば、リスト 19-19 では、すべての子犬に Spot という名前を付ける動物愛護施設用のトレイトを作成しています。Animal
という名前のトレイトを作成し、関連する非メソッド関数baby_name
を定義しています。Animal
トレイトは、baby_name
という関連非メソッド関数も直接提供するDog
構造体に対して実装されています。
ファイル名:src/main.rs
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());
}
リスト 19-19:関連関数を持つトレイトと、同じ名前の関連関数を持ち、トレイトも実装する型
子犬すべてに Spot という名前を付けるコードは、Dog
に定義されているbaby_name
関連関数で実装されています。Dog
型はまた、すべての動物が持つ特性を表すAnimal
トレイトを実装しています。子犬は子犬と呼ばれ、これはAnimal
トレイトのbaby_name
関数におけるDog
に対するAnimal
トレイトの実装で表現されています。
main
では、Dog::baby_name
関数を呼び出しており、これはDog
に定義されている関連関数を直接呼び出します。このコードは以下を表示します。
A baby dog is called a Spot
この出力は私たちが望んだものではありません。私たちは、Dog
に対して実装したAnimal
トレイトの一部であるbaby_name
関数を呼び出したいので、コードはA baby dog is called a puppy
を表示するようにしたいと思います。リスト 19-18 で使用したトレイト名を指定する手法はここでは役に立ちません。main
をリスト 19-20 のコードに変更すると、コンパイルエラーが発生します。
ファイル名:src/main.rs
fn main() {
println!("A baby dog is called a {}", Animal::baby_name());
}
リスト 19-20:Animal
トレイトのbaby_name
関数を呼び出そうとしますが、Rust はどの実装を使用するかを判断できません
Animal::baby_name
にはself
パラメータがないため、Animal
トレイトを実装する他の型がある可能性があり、Rust はAnimal::baby_name
のどの実装を私たちが望んでいるのかを判断することができません。このコンパイラエラーが表示されます。
error[E0283]: type annotations needed
--> src/main.rs:20:43
|
20 | println!("A baby dog is called a {}", Animal::baby_name());
| ^^^^^^^^^^^^^^^^^ cannot infer
type
|
= note: cannot satisfy `_: Animal`
曖昧さを解消し、Rust に、他の型に対するAnimal
の実装とは対照的に、Dog
に対するAnimal
の実装を使用したいことを伝えるには、完全修飾構文を使用する必要があります。リスト 19-21 に、完全修飾構文の使い方を示します。
ファイル名:src/main.rs
fn main() {
println!(
"A baby dog is called a {}",
<Dog as Animal>::baby_name()
);
}
リスト 19-21:Dog
に対して実装されたAnimal
トレイトのbaby_name
関数を呼び出したいことを指定するための完全修飾構文の使用
角括弧内に型注釈を提供することで、Rust に対して、この関数呼び出しにおいてDog
型をAnimal
として扱うことで、Dog
に対して実装されたAnimal
トレイトのbaby_name
メソッドを呼び出したいことを伝えています。このコードは、私たちが望む出力を表示します。
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
と指定することで行うことができます。この技術は、トレイトにトレイト境界を追加するのと似ています。リスト 19-22 にOutlinePrint
トレイトの実装を示します。
ファイル名:src/main.rs
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));
}
}
リスト 19-22:Display
からの機能を必要とするOutlinePrint
トレイトの実装
OutlinePrint
がDisplay
トレイトを必要とするように指定したので、Display
を実装している任意の型に対して自動的に実装されるto_string
関数を使用することができます。トレイト名の後にコロンを付けてDisplay
トレイトを指定せずにto_string
を使用しようとすると、現在のスコープ内の型&Self
に対してto_string
という名前のメソッドが見つからないというエラーが表示されます。
Display
を実装していない型(たとえばPoint
構造体)に対してOutlinePrint
を実装しようとするとどうなるか見てみましょう。
ファイル名:src/main.rs
struct Point {
x: i32,
y: i32,
}
impl OutlinePrint for Point {}
Display
が必要であるが実装されていないというエラーが表示されます。
error[E0277]: `Point` doesn't implement `std::fmt::Display`
--> src/main.rs:20:6
|
20 | impl OutlinePrint for Point {}
| ^^^^^^^^^^^^ `Point` cannot be formatted with the default formatter
|
= help: the trait `std::fmt::Display` is not implemented for `Point`
= note: in format strings you may be able to use `{:?}` (or {:#?} for
pretty-print) instead
note: required by a bound in `OutlinePrint`
--> src/main.rs:3:21
|
3 | trait OutlinePrint: fmt::Display {
| ^^^^^^^^^^^^ required by this bound in `OutlinePrint`
これを修正するには、Point
にDisplay
を実装して、OutlinePrint
が必要とする制約を満たします。次のようになります。
ファイル名:src/main.rs
use std::fmt;
impl fmt::Display for Point {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "({}, {})", self.x, self.y)
}
}
その後、Point
に対してOutlinePrint
トレイトを実装するとコンパイルが成功し、Point
インスタンスでoutline_print
を呼び出して、アスタリスクの枠内に表示することができます。
「型に対するトレイトの実装」の項で、トレイトまたは型のどちらか一方、または両方が私たちのクレートにローカルである場合にのみ、型に対してトレイトを実装することが許されるという孤立則に言及しました。この制限を回避する方法として、ニュータイプパターンを使用することができます。これは、タプル構造体で新しい型を作成することを含みます。(「名前付きフィールドなしのタプル構造体を使用して異なる型を作成する」の項でタプル構造体について説明しました。)タプル構造体は 1 つのフィールドを持ち、トレイトを実装したい型を薄くラップします。そして、ラッパー型は私たちのクレートにローカルであり、ラッパーに対してトレイトを実装することができます。「ニュータイプ」は、Haskell プログラミング言語に由来する用語です。このパターンを使用することによる実行時のパフォーマンスペナルティはありませんし、ラッパー型はコンパイル時に省略されます。
例として、Display
をVec<T>
に実装したいとしましょう。孤立則により、直接実装することができません。なぜなら、Display
トレイトとVec<T>
型は私たちのクレートの外で定義されているからです。Vec<T>
のインスタンスを保持するWrapper
構造体を作成することができます。そして、Wrapper
に対してDisplay
を実装して、Vec<T>
値を使用することができます。リスト 19-23 を参照してください。
ファイル名:src/main.rs
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}");
}
リスト 19-23:Display
を実装するためのVec<String>
の周りにWrapper
型を作成する
Display
の実装では、Wrapper
はタプル構造体であり、Vec<T>
はタプルのインデックス 0 の項目であるため、self.0
を使用して内部のVec<T>
にアクセスします。そして、Wrapper
に対してDisplay
型の機能を使用することができます。
この技術の欠点は、Wrapper
は新しい型であるため、保持している値のメソッドを持っていないことです。Wrapper
に直接Vec<T>
のすべてのメソッドを実装して、メソッドがself.0
に委譲するようにする必要があります。これにより、Wrapper
をまるでVec<T>
のように扱うことができます。新しい型が内部型が持つすべてのメソッドを持つことを望む場合、内部型を返すためにWrapper
にDeref
トレイトを実装するのが解決策になります(「Deref を使ってスマートポインタを通常の参照のように扱う」の項でDeref
トレイトの実装について説明しました)。Wrapper
型が内部型のすべてのメソッドを持つことを望まない場合、たとえばWrapper
型の動作を制限する場合、必要なメソッドのみを手動で実装する必要があります。
このニュータイプパターンは、トレイトが関係しない場合でも役立ちます。焦点を切り替えて、Rust の型システムと対話するいくつかの高度な方法を見てみましょう。
おめでとうございます!Advanced Traits の実験を完了しました。LabEx でさらに実験を行って、技術力を向上させることができます。