構造体からRustの特徴であるトレイトベースのジェネリクスまでの流れを翻訳します。
前: 5.19. Generics 次: 5.36. Raw Pointers
英語できないので誤訳や改善案等、見つけた場合はご指摘の程宜しくお願いします。
- 元記事 : https://doc.rust-lang.org/stable/book/traits.html のRust1.2時点
- ライセンス : MIT license, Apache License 2.0.
- See LICENSE-APACHE, LICENSE-MIT, and COPYRIGHT for details.
トレイト
あなたはmethod syntaxで関数を呼び出すために用いたimpl
キーワードを覚えていますか?
struct Circle {
x: f64,
y: f64,
radius: f64,
}
impl Circle {
fn area(&self) -> f64 {
std::f64::consts::PI * (self.radius * self.radius)
}
}
トレイトはそれに似ていますが、定義するときにはシグネチャだけを記述し、その後構造体毎に実装します。
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)
}
}
このように、traits
の記述はimpl
とかなり似ているように見えますが、関数の実装は記述せず、シグネチャだけを定義します。トレイトを実装
するときは、ただimpl 実装の対象
とするのではなく、impl トレイト for 実装の対象
とします。
ジェネリクスに制約を持たせるためにトレイトを使用できます。以下の関数について考えてみましょう。
fn print_area<T>(shape: T) {
println!("This shape has an area of {}", shape.area());
}
これはコンパイルできませんが、このようなエラーが発生するはずです。
error: type `T` does not implement any method in scope named `area`
T
はありとあらゆる型であるため、Rustは.area()
メソッドがT
について実装されているか確信を持てません。しかしジェネリック型T
に対して'トレイトによる制約'を加えることで、.area()
メソッドが実装されていることを保証できます。
trait HasArea {
fn area(&self) -> f64;
}
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);
}
このプログラムの出力は、
This shape has an area of 3.141593
This shape has an area of 1
上記の通り、print_area
はジェネリックですが、正しい型が渡されることを保証します。もし不正な型が渡されるとすると、
print_area(5);
コンパイルエラーになります。
error: failed to find an implementation of trait main::HasArea for int
これまでは構造体にのみトレイトの実装を追加していましたが、あなたはあらゆる型に対してトレイトを実装することができます。技術的には、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
はそのメソッドを持っていません。
let mut f = std::fs::File::open("foo.txt").ok().expect("Couldn’t open foo.txt");
let result = f.write("whatever".as_bytes());
result.unwrap(); // ignore the error
エラーは以下のとおりです。
error: type `std::fs::File` does not implement any method in scope named `write`
let result = f.write(b"whatever");
^~~~~~~~~~~~~~~~~~
Write
トレイトのために、初めにuse
が必要となります。
use std::io::Write;
let mut f = std::fs::File::open("foo.txt").ok().expect("Couldn’t open foo.txt");
let result = f.write("whatever".as_bytes());
result.unwrap(); // ignore the error
これはエラー無しでコンパイルされます。
これは、例え誰かがint
へメソッドを追加するような望ましくない何かを行ったとしても、あなたがトレイトのuse
を行わない限り、影響はないことを意味します。
トレイトの実装についての制限はもう1つあります。トレイト、またはあなたが書いているimpl
の対象となる型は、あなた自身によって実装されなければなりません。HasArea
は私たちが記述したコードであるため、i32
型のためのHasArea
を実装することができます。しかし、i32
のためにRustによって提供されているToString
トレイトを実装したいとしても、トレイトと型が共に私たちの記述したコードでないため、それはできません。
最後に1つ。トレイトによって束縛されたジェネリック関数はmonomorphization
(mono:単一の、morph:様相)されるため、静的ディスパッチが行われます。一体どういう意味でしょうか?詳細については、トレイトオブジェクトの章をチェックしてください。
複数のトレイト束縛
ここまで、トレイトによってジェネリックな型パラメータが束縛できることを見てきました。
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
、両方の実装が必要です。
Where 節
ジェネリック型とトレイト束縛が少ないうちは良いのですが、数が増えるといよいよこの構文では不便になってきます。
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", "workd");
}
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
は基本的な構文よりも強力です。例えば、
(訳注: std::convert::From)
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
// これは"ConvertFrom<i32>"であるかのようにConvertToを用いている
where i32: ConvertTo<T> {
42.convert()
}
ここではwhere
節の追加機能を披露しています。この節は型パラメータではない任意の型1に対してトレイト束縛が可能です。
デフォルトメソッド
私たちがカバーしておくべきトレイトの最後の機能がデフォルトメソッドです。これの例を示すのは最も簡単です。
trait Foo {
fn bar(&self);
fn baz(&self) { println!("We called baz."); }
}
Foo
トレイトの実装者はbar()
を実装する必要がありますが、baz()
を実装する必要はありません。baz()
にはデフォルトの動作が与えられているからです。実装者の選択次第ではこのデフォルトの動作をオーバーライドすることも可能です。
trait Foo {
fn bar(&self);
fn baz(&self) { println!("We called baz."); }
}
struct UseDefault;
impl Foo for UseDefault {
fn bar(&self) { println!("We called bar."); }
}
struct OverrideDefault;
impl Foo for OverrideDefault {
fn bar(&self) { println!("We called bar."); }
fn baz(&self) { println!("Override baz!"); }
}
let default = UseDefault;
default.baz(); // prints "We called baz."
let over = OverrideDefault;
over.baz(); // prints "Override baz!"
継承
時々、トレイトを実装するのに他のトレイトの実装が必要なこともあります。
trait Foo {
fn foo(&self);
}
trait FooBar : Foo {
fn foobar(&self);
}
FooBar
の実装者はFoo
もまた実装しなければなりません。以下のようになります。
trait Foo {
fn foo(&self);
}
trait FooBar : Foo {
fn foobar(&self);
}
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]
-
訳注:
T
ではなく、今回はi32
のような型 ↩