Syntax and Semantics
Traits
Traitは、Haskellでいう型クラスであり、 C++でいうコンセプト(C++14時点では提案のみっぽいですが)です。 ジェネリクスに制限をくわえ、あるメソッドをもった型しか認めないように束縛するのが、traitです。 これによって、ジェネリクス関数やstructがうけとる型に、 そのメソッドが実行できることを保証できます。
Trait bounds on generic functions
例えば、面積を表示する、以下のジェネリックな関数を作りたいとします。
fn print_area<T>(shape: T) {
println!("This shape has an area of {}", shape.area());
}これはコンパイルエラーになります。
print_area()関数が受け取る型Tに、area()メソッドが定義されていないからです。
現行のC++では、実際にprint_area()関数をある型について使ったとき、
それがarea()メソッドをもたないならエラーになりましたが、
Rustではジェネリクス関数を定義しただけでエラーをおこします。
そこで登場するのがtraitです。
area()メソッドをもつことを保証するtraitをつくり、
型Tを、そのtraitをもつものに制限します。
Syntaxは次のとおりです。
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)
}
}
fn print_area<T: HasArea>(shape: T) {
println!("This shape has an area of {}", shape.area());
}HasAreatraitをつくり、それにarea()メソッドを含めます。
print_area()関数がCirclestructに適用するため、
CircleにHasAreatraitを実装します。
そしてprint_area()が受け取る型Tを、HasAreatraitをもつという条件で束縛します。
ほかのstrctにHasAreatraitをimplすることもできます。
struct Square {
x: f64,
y: f64,
side: f64,
}
impl HasArea for Square {
fn area(&self) -> f64 {
self.side * self.side
}
}Trait bounds on generic structs
ジェネリックなstructについても、traitが使えます。
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
}
}
let mut r = Rectangle {
x: 0,
y: 0,
width: 47,
height: 47,
};PartialEqは標準ライブラリにあるtraitで、
対称律と推移律を満たす関係をもつ型にimplされているようです。
i32はこのtraitをもっているので、Rectangle<i32>がつくれます。
しかし、PartialEqをもっている型に限定しないと、==がつかえないのでエラーになります。
Rules for implementing traits
Traitの作成・使用には、いくつかルールがあります。
まず、他のスコープでつくられたtraitは、明示的にuseしないと使えません。
let mut f = std::fs::File::open("foo.txt").ok().expect("Couldn’t open foo.txt");
let buf = b"whatever"; // byte string literal. buf: &[u8; 8]
let result = f.write(buf);これはコンパイルエラーです。
f.write()のためには、fがWritetraitをもっている必要があります。
use std::io::Write;と書いておけばOKです。 知らないところで変なtraitが定義されて、それを使ってしまうのを防ぐための仕様です。
もう一つ、Rustのプリミティブな型に、標準で提供されているtraitをimplすることはできません。
例えば、i32にToStringをimplすることはできません
(そもそもすでにimplされています)。
Multiple traits bounds
複数のtraitをもっている型に制限したいときは、+で重ねることができます。
use std::fmt::Debug;
fn foo<T: Clone + Debug>(x: T) {
x.clone();
println!("{:?}", x);
}std::fmt::Debugtraitをもっていると、
{:?}というフォーマットですこしくわしい情報が得られるようです。
Where clause
ジェネリックパラメータが増えたり、multiple traits boundsしたりすると、
<>のなかが長くなりがちです。
use std::fmt::Debug;
fn foo<T: Clone, K: Clone + Debug>(x: T, y: K) {
x.clone();
y.clone();
println!("{:?}", y);
}これくらいだとまだいいですが、これ以上長くなってくると読みにくいですね。
そんなときのためにwhereキーワードがあります。
fn bar<T, K>(x: T, y: K) where T: Clone, K: Clone + Debug {
x.clone();
y.clone();
println!("{:?}", y);
}where節をかくと、型制約を引数リストのあとに書くことができます。
関数名と引数が近くなるのでわかりやすいですね。
また、改行をいれたりもできるので、それによっても可読性が上がります。
実は、where節でしかかけない型制約があります。
それは、型制約が、型パラメータに直接かかる制約ではなく、
型パラメータへの間接的な制約であるときです。
ことばでいってもわかりにくいので、例を見てみます。
trait ConvertTo<Output> {
fn convert(&self) -> Output;
}
impl ConvertTo<i64> for i32 {
fn convert(&self) -> i64 { *self as i64 }
}
// can be called with T == i32
fn normal<T: ConvertTo<i64>>(x: &T) -> i64 {
x.convert()
}
// can be called with T == i64
fn inverse<T>() -> T
// this is using ConvertTo as if it were "ConvertTo<i64>"
where i32: ConvertTo<T> {
42.convert()
}normal()関数は、TがConvertTo<i64>traitをもっていることが型制約ですが、
inverse()関数は、TにConvertTo<T>できるという、間接的な型制約になっています。
Default method
Traitがもつメソッドにデフォルト実装を書くことができます。 そのtraitをimplする型でoverrideしない場合、デフォルト実装がつかわれることになります。
fn main() {
trait Validation {
fn is_valid(&self) -> bool { !self.is_invalid() }
fn is_invalid(&self) -> bool { !self.is_valid() }
}
impl Validation for i32 {
fn is_valid(&self) -> bool {
*self >= 0
}
}
impl Validation for f64 {
fn is_invalid(&self) -> bool {
*self < 0.0
}
}
assert!(1.is_valid());
assert!((-1).is_invalid());
assert!((1.1).is_valid());
assert!((-1.1).is_invalid());
}この例では、Valiadationtraitのもつメソッドは、お互いの否定をとるような実装になっています。
したがって、i32などにimplするとき、
is_valid()かis_invalid()のどちらか一方のみ実装すれば十分です。
Inheritance
あるtraitをimplするとき、他のtraitのメソッドが必要になる場合があります。 AというtraitがBというtraitを継承すると、Aをimplする型はBもimplされていることが要請できます。
fn main() {
trait Printable : std::fmt::Display {
fn print(&self) {
println!("{}", self)
}
}
impl Printable for i32 {}
1.print();
struct S;
impl Printable for S {}
let s = S;
s.print();
}この例では、Printabletraitがstd::fmt::Displayを継承しているので、
println!("{}", self)が実行できることが保証されます。
i32はstd::fmt::Displayをimplされているので、問題なくPrintableもimplできます。
一方、structSはstd::fmt::Displayをimplしていないので、Printableもimplできません。
Deriving
基本的には、traitは型ごとにimplしないと使えませんが、
DebugとかDefaultとかのよく使う標準ライブラリのtraitをいちいちimplするのは面倒ですね。
そこで、#[derive(Debug)]と書くと、自動的にそれがimplされるようになっています。
#[derive(Debug)]
struct Foo;
fn main() {
println!("{:?}", Foo);
}ただし、このattributeがつかえるのは、以下のtraitに限られています。
- Clone
- Copy
- Debug
- Default
- Eq
- Hash
- Ord
- PartialEq
- PartialOrd
Drop
最後に、標準ライブラリにある便利なtraitを紹介しておきます。
Droptraitは、C++でいうデストラクタです。
Dropをimplした型のVariable bindingがスコープを抜けて破棄されるとき、
drop()メソッドが呼ばれます。
struct HasDrop;
impl Drop for HasDrop {
fn drop(&mut self) {
println!("Dropping!");
}
}
fn main() {
let x = HasDrop;
// do stuff
} // x goes out of scope hereこれを実行すると、プログラムが終了するときに、Dropping!と出力されます。
Variable bindingは、宣言と逆順に破棄されていきます。 StackのLIFOということですね。
struct Firework {
strength: i32,
}
impl Drop for Firework {
fn drop(&mut self) {
println!("BOOM times {}!!!", self.strength);
}
}
fn main() {
let firecracker = Firework { strength: 1 };
let tnt = Firework { strength: 100 };
}tntがfirecrackerより後に宣言されているので、出力は逆順の、
BOOM times 100!!!
BOOM times 1!!!となります。