注意: 最新版のドキュメントをご覧ください。この第1版ドキュメントは古くなっており、最新情報が反映されていません。リンク先のドキュメントが現在の Rust の最新のドキュメントです。
トレイトはある型が提供しなければならない機能をRustのコンパイラに伝える言語機能です。
メソッド構文で関数を呼び出すのに用いていた、 impl
キーワードを思い出して下さい。
struct Circle { x: f64, y: f64, radius: f64, } impl Circle { fn area(&self) -> f64 { std::f64::consts::PI * (self.radius * self.radius) } }
始めにトレイトをメソッドのシグネチャと共に定義し、続いてある型のためにトレイトを実装するという流れを除けばトレイトはメソッド構文に似ています。
この例では、 Circle
に HasArea
トレイトを実装しています。
struct Circle { x: f64, y: f64, radius: f64, } trait HasArea { fn area(&self) -> f64; } impl HasArea for Circle { fn area(&self) -> f64 { std::f64::consts::PI * (self.radius * self.radius) } }
このように、 trait
ブロックは impl
ブロックにとても似ているように見えますが、関数本体を定義せず、型シグネチャだけを定義しています。トレイトを impl
するときは、ただ impl Item
とするのではなく、 impl Trait for Item
と記述します。
トレイトはある型の振る舞いを確約できるため有用です。ジェネリック関数は制約、あるいは 境界 が許容する型のみを受け取るためにトレイトを利用できます。以下の関数を考えて下さい、これはコンパイルできません。
fn main() { fn print_area<T>(shape: T) { println!("This shape has an area of {}", shape.area()); } }fn print_area<T>(shape: T) { println!("This shape has an area of {}", shape.area()); }
Rustは以下のエラーを吐きます。
error: no method named `area` found for type `T` in the current scope
T
はあらゆる型になれるため、 area
メソッドが実装されているか確認できません。ですがジェネリックな T
にはトレイト境界を追加でき、境界が実装を保証してくれます。
fn print_area<T: HasArea>(shape: T) { println!("This shape has an area of {}", shape.area()); }
<T: HasArea>
構文は「 HasArea
トレイトを実装するあらゆる型」という意味です。トレイトは関数の型シグネチャを定義しているため、 HasArea
を実装するあらゆる型が .area()
メソッドを持っていることを確認できます。
トレイトの動作を確認するために拡張した例が以下になります。
trait HasArea { fn area(&self) -> f64; } struct Circle { x: f64, y: f64, radius: f64, } impl HasArea for Circle { fn area(&self) -> f64 { std::f64::consts::PI * (self.radius * self.radius) } } struct Square { x: f64, y: f64, side: f64, } impl HasArea for Square { fn area(&self) -> f64 { self.side * self.side } } fn print_area<T: HasArea>(shape: T) { println!("This shape has an area of {}", shape.area()); } fn main() { let c = Circle { x: 0.0f64, y: 0.0f64, radius: 1.0f64, }; let s = Square { x: 0.0f64, y: 0.0f64, side: 1.0f64, }; print_area(c); print_area(s); }trait HasArea { fn area(&self) -> f64; } struct Circle { x: f64, y: f64, radius: f64, } impl HasArea for Circle { fn area(&self) -> f64 { std::f64::consts::PI * (self.radius * self.radius) } } struct Square { x: f64, y: f64, side: f64, } impl HasArea for Square { fn area(&self) -> f64 { self.side * self.side } } fn print_area<T: HasArea>(shape: T) { println!("This shape has an area of {}", shape.area()); } fn main() { let c = Circle { x: 0.0f64, y: 0.0f64, radius: 1.0f64, }; let s = Square { x: 0.0f64, y: 0.0f64, side: 1.0f64, }; print_area(c); print_area(s); }
このプログラムの出力は、
This shape has an area of 3.141593
This shape has an area of 1
見ての通り、上記の print_area
はジェネリックですが、適切な型が渡されることを保証しています。もし不適切な型を渡すと、
print_area(5);
コンパイル時エラーが発生します。
error: the trait `HasArea` is not implemented for the type `_` [E0277]
ジェネリック構造体もトレイト境界による恩恵を受けることができます。型パラメータを宣言する際に境界を追加するだけで良いのです。以下が新しい型 Rectangle<T>
とそのメソッド is_square()
です。
struct Rectangle<T> { x: T, y: T, width: T, height: T, } impl<T: PartialEq> Rectangle<T> { fn is_square(&self) -> bool { self.width == self.height } } fn main() { let mut r = Rectangle { x: 0, y: 0, width: 47, height: 47, }; assert!(r.is_square()); r.height = 42; assert!(!r.is_square()); }
is_square()
は両辺が等しいかチェックする必要があるため、両辺の型は core::cmp::PartialEq
トレイトを実装しなければなりません。
impl<T: PartialEq> Rectangle<T> { ... }
これで、長方形を等値性の比較できる任意の型として定義できました。
上記の例では任意の精度の数値を受け入れる Rectangle
構造体を新たに定義しました-実は、等値性を比較できるほぼ全ての型に対して利用可能なオブジェクトです。同じことを Square
や Circle
のような HasArea
を実装する構造体に対してできるでしょうか?可能では有りますが乗算が必要になるため、それをするには オペレータトレイト についてより詳しく知らなければなりません。
ここまでで、構造体へトレイトの実装を追加することだけを説明してきましたが、あらゆる型についてトレイトを実装することもできます。技術的には、 i32
に HasArea
を実装することも できなくはない です。
trait HasArea { fn area(&self) -> f64; } impl HasArea for i32 { fn area(&self) -> f64 { println!("this is silly"); *self as f64 } } 5.area();
しかし例え可能であったとしても、そのようなプリミティブ型のメソッドを実装するのは適切でない手法だと考えられています。
ここまでくると世紀末感が漂いますが、手に負えなくなることを防ぐためにトレイトの実装周りには2つの制限が設けられています。第1に、あなたのスコープ内で定義されていないトレイトは適用されません。例えば、標準ライブラリは File
にI/O機能を追加するための Write
トレイトを提供しています。デフォルトでは、 File
は Write
で定義されるメソッド群を持っていません。
let mut f = std::fs::File::open("foo.txt").expect("Couldn’t open foo.txt"); let buf = b"whatever"; // buf: &[u8; 8] はバイト文字列リテラルです。 let result = f.write(buf);
エラーは以下のようになります。
error: type `std::fs::File` does not implement any method in scope named `write`
let result = f.write(buf);
^~~~~~~~~~
始めに Write
トレイトを use
する必要があります。
use std::io::Write; let mut f = std::fs::File::open("foo.txt").expect("Couldn’t open foo.txt"); let buf = b"whatever"; let result = f.write(buf);
これはエラー無しでコンパイルされます。
これは、例え誰かが i32
へメソッドを追加するような望ましくない何かを行ったとしても、あなたがトレイトを use
しない限り、影響はないことを意味します。
トレイトの実装における制限はもう1つあります。トレイト、またはあなたが書いている impl
の対象となる型は、あなた自身によって実装されなければなりません。 HasArea
は私たちが記述したコードであるため、 i32
のための HasArea
を実装することができます。しかし、 i32
のためにRustによって提供されている ToString
トレイトを実装したいとしても、トレイトと型が共に私たちの記述したコードでないため、それはできません。
トレイトに関して最後に1つ。トレイト境界が設定されたジェネリック関数は「単相化」(monomorphization)(mono: 単一の、morph: 相)されるため、静的ディスパッチが行われます。一体どういう意味でしょうか?詳細については、 トレイトオブジェクト の章をチェックしてください。
トレイトによってジェネリックな型パラメータに境界が与えられることを見てきました。
fn main() { fn foo<T: Clone>(x: T) { x.clone(); } }fn foo<T: Clone>(x: T) { x.clone(); }
2つ以上の境界を与えたい場合、 +
を使えます。
use std::fmt::Debug; fn foo<T: Clone + Debug>(x: T) { x.clone(); println!("{:?}", x); }
この T
型は Clone
と Debug
両方が必要です。
ジェネリック型もトレイト境界の数も少ない関数を書いているうちは悪く無いのですが、数が増えるとこの構文ではいよいよ不便になってきます。
fn main() { use std::fmt::Debug; fn foo<T: Clone, K: Clone + Debug>(x: T, y: K) { x.clone(); y.clone(); println!("{:?}", y); } }use std::fmt::Debug; fn foo<T: Clone, K: Clone + Debug>(x: T, y: K) { x.clone(); y.clone(); println!("{:?}", y); }
関数名は左端にあり、引数リストは右端にあります。境界を記述する部分が邪魔になっているのです。
Rustは「 where
節」と呼ばれる解決策を持っています。
use std::fmt::Debug; fn foo<T: Clone, K: Clone + Debug>(x: T, y: K) { x.clone(); y.clone(); println!("{:?}", y); } fn bar<T, K>(x: T, y: K) where T: Clone, K: Clone + Debug { x.clone(); y.clone(); println!("{:?}", y); } fn main() { foo("Hello", "world"); bar("Hello", "world"); }
foo()
は先程見せたままの構文で、 bar()
は where
節を用いています。型パラメータを定義する際に境界の設定をせず、引数リストの後ろに where
を追加するだけで良いのです。長いリストであれば、空白を加えることもできます。
use std::fmt::Debug; fn bar<T, K>(x: T, y: K) where T: Clone, K: Clone + Debug { x.clone(); y.clone(); println!("{:?}", y); }
この柔軟性により複雑な状況であっても可読性を改善できます。
また、where
は基本の構文よりも強力です。例えば、
trait ConvertTo<Output> { fn convert(&self) -> Output; } impl ConvertTo<i64> for i32 { fn convert(&self) -> i64 { *self as i64 } } // T == i32の時に呼び出せる fn normal<T: ConvertTo<i64>>(x: &T) -> i64 { x.convert() } // T == i64の時に呼び出せる fn inverse<T>() -> T // これは「ConvertTo<i64>」であるかのようにConvertToを用いている where i32: ConvertTo<T> { 42.convert() }
ここでは where
節の追加機能を披露しています。この節は左辺に型パラメータ T
だけでなく具体的な型(このケースでは i32
)を指定できます。この例だと、 i32
は ConvertTo<T>
を実装していなければなりません。(それは明らかですから)ここの where
節は i32
が何であるか定義しているというよりも、 T
に対して制約を設定しているといえるでしょう。
典型的な実装者がどうメソッドを定義するか既に分かっているならば、トレイトの定義にデフォルトメソッドを加えることができます。例えば、以下の is_invalid()
は is_valid()
の反対として定義されます。
trait Foo { fn is_valid(&self) -> bool; fn is_invalid(&self) -> bool { !self.is_valid() } }
Foo
トレイトの実装者は is_valid()
を実装する必要がありますが、デフォルトの動作が加えられている is_invalid()
には必要ありません。
struct UseDefault; impl Foo for UseDefault { fn is_valid(&self) -> bool { println!("Called UseDefault.is_valid."); true } } struct OverrideDefault; impl Foo for OverrideDefault { fn is_valid(&self) -> bool { println!("Called OverrideDefault.is_valid."); true } fn is_invalid(&self) -> bool { println!("Called OverrideDefault.is_invalid!"); true // 予期されるis_invalid()の値をオーバーライドする } } let default = UseDefault; assert!(!default.is_invalid()); // 「Called UseDefault.is_valid.」を表示 let over = OverrideDefault; assert!(over.is_invalid()); // 「Called OverrideDefault.is_invalid!」を表示
時々、1つのトレイトの実装に他のトレイトの実装が必要になります。
fn main() { trait Foo { fn foo(&self); } trait FooBar : Foo { fn foobar(&self); } }trait Foo { fn foo(&self); } trait FooBar : Foo { fn foobar(&self); }
FooBar
の実装者は Foo
も実装しなければなりません。以下のようになります。
struct Baz; impl Foo for Baz { fn foo(&self) { println!("foo"); } } impl FooBar for Baz { fn foobar(&self) { println!("foobar"); } }
Foo
の実装を忘れると、Rustは以下のように伝えるでしょう。
error: the trait `main::Foo` is not implemented for the type `main::Baz` [E0277]
繰り返しDebug
や Default
のようなトレイトを実装するのは非常にうんざりさせられます。そのような理由から、Rustは自動的にトレイトを実装するための アトリビュート を提供しています。
#[derive(Debug)] struct Foo; fn main() { println!("{:?}", Foo); }
ただし、deriveは以下の特定のトレイトに制限されています。