トレイトを使うと、任意の振る舞いを抽象化し、それを複数の型に持たせることができる。
この記事では、トレイトの基本的な使い方を見ていく。
Rust のバージョンは1.55.0
、Edition は2018
で動作確認している。
基本的な書き方
例として、IceCream
とEnglishClass
という 2 つの構造体を用意した。
共通するフィールドはひとつもないし、それぞれに独立して存在する異なる型だが、トレイトを使うことで「ある同じ振る舞いを持ったグループ」として抽象化できる。
struct IceCream { unit_price: f64, flavor: String, } struct EnglishClass { hourly_price: f64, hour: f64, difficulty_level: String, }
今回の例では、小計価格を計算する振る舞いを持たせることにする。
まずはトレイトを定義する。
trait Purchasable { fn get_subtotal_price(&self) -> f64; }
トレイトの名前はPurchasable
とした。そしてPurchasable
トレイトを実装している型はget_subtotal_price
メソッドを持ち、このメソッドが小計価格を計算して返す。
上記を見れば分かるが、get_subtotal_price
について定義しているのは引数と返り値の型だけで、実装については書いていない。
実装はトレイトを実装する型(今回の場合はIceCream
とEnglishClass
)が定義する。型さえあっていれば、メソッドの中身は問われない。
早速、IceCream
とEnglishClass
にPurchasable
トレイトを実装する。
impl Purchasable for IceCream { fn get_subtotal_price(&self) -> f64 { self.unit_price } } impl Purchasable for EnglishClass { fn get_subtotal_price(&self) -> f64 { &self.hourly_price * &self.hour } }
impl トレイトの名前 for トレイトを実装する型の名前
と記述し、そのなかでメソッドの定義を行う。
IceCream
とEnglishClass
でget_subtotal_price
の実装は異なるが、(&self) -> f64
という型を満たしているので問題ない。
これで、どちらのインスタンスでもget_subtotal_price
メソッドを使えるようになった。
fn main() { let vanilla_ice = IceCream { unit_price: 400.0, flavor: String::from("vanilla"), }; let some_class = EnglishClass { hourly_price: 2000.0, hour: 3.0, difficulty_level: String::from("hard"), }; println!("{}", vanilla_ice.get_subtotal_price()); // 400 println!("{}", some_class.get_subtotal_price()); // 6000 }
トレイトを使うことで、「IceCream
とEnglishClass
はget_subtotal_price
という共通の振る舞いを持っている」という意味を持たせることができる。たまたま名前が同じで型も同じのメソッドを持っていたわけではない。
例えば、単にget_subtotal_price
を実装したいだけなら以下のように書くこともできる。
struct IceCream { unit_price: f64, flavor: String, } struct EnglishClass { hourly_price: f64, hour: f64, difficulty_level: String, } impl IceCream { fn get_subtotal_price(&self) -> f64 { self.unit_price } } impl EnglishClass { fn get_subtotal_price(&self) -> f64 { &self.hourly_price * &self.hour } }
この場合、同じ名前のメソッドを偶然持っていたというだけで、IceCream
とEnglishClass
には何の共通性もないし、何かグルーピングされているわけでもない。
トレイトを使うことで初めて、抽象化されたある振る舞い(この例だとget_subtotal_price
)を持っている型として意味づけることができる。そしてそのような型のことを、「あるトレイト(この例だとPurchasable
)を実装している型」と表現する。
この機能によって、「特定のトレイトを実装している型のみを引数として受け入れる関数」などを記述することができる。これについては後述する。
デフォルト実装とそのオーバーライド
トレイトに、メソッドの実装を書くこともできる。その場合、その実装がデフォルトとなり、トレイトを実装するそれぞれの型にメソッドの実装を書かなくて済むようになる。
例として、税込価格を計算するget_tax_included_price
メソッドを実装してみる。
const DEFAULT_CONSUMPTION_TAX_RATE: f64 = 10.0; trait Purchasable { fn get_subtotal_price(&self) -> f64; fn get_tax_included_price(&self) -> f64 { let subtotal = &self.get_subtotal_price(); subtotal / DEFAULT_CONSUMPTION_TAX_RATE + subtotal } }
トレイトにget_tax_included_price
の実装を書いたので、Purchasable
を実装している型は、何もしなくてもget_tax_included_price
を使えるようになる。
fn main() { let vanilla_ice = IceCream { unit_price: 400.0, flavor: String::from("vanilla"), }; let some_class = EnglishClass { hourly_price: 2000.0, hour: 3.0, difficulty_level: String::from("hard"), }; println!("{}", vanilla_ice.get_tax_included_price()); // 440 println!("{}", some_class.get_tax_included_price()); // 6600 }
トレイトを実装している型にメソッドの実装を書くと、デフォルトの実装をオーバーライドできる。
アイスクリームは軽減税率の対象なので、8
% をかける必要があったので、以下のように修正する。
こうすると、IceCream
のインスタンスでget_tax_included_price
を実行したときは8
%を使って計算するようになる。EnglishClass
についてはオーバーライドを行っていないので、デフォルトの実装がそのまま使われる。
struct IceCream { unit_price: f64, flavor: String, } struct EnglishClass { hourly_price: f64, hour: f64, difficulty_level: String, } const DEFAULT_CONSUMPTION_TAX_RATE: f64 = 10.0; const REDUCED_CONSUMPTION_TAX_RATE: f64 = 8.0; trait Purchasable { fn get_subtotal_price(&self) -> f64; fn get_tax_included_price(&self) -> f64 { let subtotal = &self.get_subtotal_price(); subtotal / DEFAULT_CONSUMPTION_TAX_RATE + subtotal } } impl Purchasable for IceCream { fn get_subtotal_price(&self) -> f64 { self.unit_price } fn get_tax_included_price(&self) -> f64 { let subtotal = &self.get_subtotal_price(); subtotal * ((100.0 + REDUCED_CONSUMPTION_TAX_RATE) / 100.0) } } impl Purchasable for EnglishClass { fn get_subtotal_price(&self) -> f64 { &self.hourly_price * &self.hour } } fn main() { let vanilla_ice = IceCream { unit_price: 400.0, flavor: String::from("vanilla"), }; let some_class = EnglishClass { hourly_price: 2000.0, hour: 3.0, difficulty_level: String::from("hard"), }; println!("{}", vanilla_ice.get_tax_included_price()); // 432 println!("{}", some_class.get_tax_included_price()); // 6600 }
特定のトレイトを実装している型のみを引数として受け入れる関数
関数を定義するときにfn 関数名(仮引数: &impl トレイト名)
と書くと、指定したトレイトを実装している型のみを引数として受け入れるようになる。
例えば以下のget_tax_amount
は、Purchasable
トレイトを実装している型ならどんな型でも受け入れる。
fn get_tax_amount(arg: &impl Purchasable) -> f64 { let tax_included_price = arg.get_tax_included_price(); let subtotal = arg.get_subtotal_price(); tax_included_price - subtotal } fn main() { let vanilla_ice = IceCream { unit_price: 400.0, flavor: String::from("vanilla"), }; let some_class = EnglishClass { hourly_price: 2000.0, hour: 3.0, difficulty_level: String::from("hard"), }; println!("{}", get_tax_amount(&vanilla_ice)); // 32 println!("{}", get_tax_amount(&some_class)); // 600 }
Point
はPurchasable
トレイトを実装していないので、get_tax_amount
に渡すとコンパイルエラーになる。
fn main() { struct Point { x: i32, y: i32, } let p = Point { x: 1, y: 2 }; println!("{}", get_tax_amount(&p)); // the trait `Purchasable` is not implemented for `main::Point` }
fn 関数名(仮引数: &(impl トレイト名1 + トレイト名2))
と書くと、両方のトレイトを実装している型のみを受け入れる。
以下のshow_purchasable_food_info
に渡す引数はPurchasable
とFood
の両方を満たしている必要があるため、EnglishClass
を渡すとコンパイルエラーになる。
trait Food { fn get_flavor(&self) -> &String; } impl Food for IceCream { fn get_flavor(&self) -> &String { &self.flavor } } fn show_purchasable_food_info(arg: &(impl Purchasable + Food)) { println!( "This is {}. Flavor is {}. ", arg.get_tax_included_price(), arg.get_flavor() ); } fn main() { let vanilla_ice = IceCream { unit_price: 400.0, flavor: String::from("vanilla"), }; let some_class = EnglishClass { hourly_price: 2000.0, hour: 3.0, difficulty_level: String::from("hard"), }; show_purchasable_food_info(&vanilla_ice); // This is 432. Flavor is vanilla. show_purchasable_food_info(&some_class); // the trait `Food` is not implemented for `EnglishClass` }
トレイトを使った引数の制約は、ジェネリックを使って表記することもできる。
その場合、get_tax_amount
とshow_purchasable_food_info
はそれぞれ、以下のようになる。
fn get_tax_amount<T: Purchasable>(arg: &T) -> f64 { let tax_included_price = arg.get_tax_included_price(); let subtotal = arg.get_subtotal_price(); tax_included_price - subtotal } fn show_purchasable_food_info<T: Purchasable + Food>(arg: &T) { println!( "This is {}. Flavor is {}. ", arg.get_tax_included_price(), arg.get_flavor() ); }
特定のトレイトを実装している型を返す関数
引数ではなく返り値に対してトレイトを指定することもできる。
fn get_vanilla_ice() -> impl Purchasable { IceCream { unit_price: 400.0, flavor: String::from("vanilla"), } }
注意しなければならないのは、上記のget_vanilla_ice
の返り値の型はimpl Purchasable
であり、IceCream
ではない。
そのため、show_purchasable_food_info
に渡すとコンパイルエラーになる。vanilla_ice
はFood
トレイトを実装していると見做されないため、このような挙動になる。
fn main() { let vanilla_ice = get_vanilla_ice(); // vanilla_ice の型は impl Purchasable show_purchasable_food_info(&vanilla_ice); // the trait `Food` is not implemented for `impl Purchasable` }
以下のようにすると返り値がimpl Purchasable + Food
になり、その値はshow_purchasable_food_info
に渡せるようになる。
fn get_vanilla_ice() -> impl Purchasable + Food { IceCream { unit_price: 400.0, flavor: String::from("vanilla"), } }
型引数が特定のトレイトを実装をしているときにのみ、特定のメソッドを使えるようにする
ジェネリックとトレイトを組み合わせることで、より複雑なことを表現できるようになる。
以下のMyBox
は要素をひとつ持つタプル構造体で、describe
メソッドを持っている。
T
にはどのような型でも渡すことができる。
struct MyBox<T>(T); impl<T> MyBox<T> { fn describe(&self) { println!("MyBox is tuple structs."); } } fn main() { let int = MyBox(1); // MyBox<i32> int.describe(); // MyBox is tuple structs. let str = MyBox("abc"); // MyBox<&str> str.describe(); // MyBox is tuple structs. let bool = MyBox(true); // MyBox<bool> bool.describe(); // MyBox is tuple structs. }
要素を二乗するsquare
メソッドをMyBox
に実装したい場合、以下のように書くとコンパイルエラーになってしまう。
impl<T> MyBox<T> { fn describe(&self) { println!("MyBox is tuple structs."); } fn square(self) -> T { self.0 * self.0 // cannot multiply `T` by `T` } }
T
にはどのような型が入るか分からず、乗算できるとは限らない。そのためコンパイルエラーとなる。例えばT
に&str
が入った場合、乗算できない。
T
に入る型が特定のトレイトを実装していたときにのみsquare
メソッドを使えるようにすることで、このコンパイルエラーを回避できる。
以下のMyBox
では、T
がMul
トレイトとCopy
トレイトを実装していた場合にのみ、square
メソッドを使えるようにしている。
そのため、問題なくコンパイルできる。describe
は引き続き、T
がどんな型であっても使用できる。
struct MyBox<T>(T); impl<T> MyBox<T> { fn describe(&self) { println!("MyBox is tuple structs."); } } use std::ops::Mul; impl<T: Mul + Copy> MyBox<T> { fn square(self) -> <T as std::ops::Mul>::Output { self.0 * self.0 } }
i32
、u32
、f64
はいずれもMut
トレイトとCopy
トレイトを実装しているため、square
メソッドを実行できる。
&str
は条件を満たしていないためsquare
メソッドを使えず、実行しようとするとコンパイルエラーになる。
fn main() { let x = MyBox(1); // MyBox<i32> println!("{}", x.square()); // 1 let x = MyBox(5u32); // MyBox<u32> println!("{}", x.square()); // 25 let x = MyBox(3.0); // MyBox<f64> println!("{}", x.square()); // 9 let x = MyBox("abc"); // MyBox<&str> println!("{}", x.square()); // method cannot be called on `MyBox<&str>` due to unsatisfied trait bounds }
ブランケット実装
以下のコードをコンパイルしようとすると、エラーになる。
MyStruct
のインスタンスであるa
がbase
メソッドを呼び出そうとしているため。MyStruct
はFoo
トレイトを実装しているがBase
トレイトは実装していないのだから、当然である。
fn main() { struct MyStruct {} trait Base { fn base(&self) { println!("base!"); } } trait Foo { fn foo(&self) { println!("foo!"); } } impl Foo for MyStruct {} let a = MyStruct {}; a.foo(); // foo! a.base(); // method not found in `main::MyStruct` }
a
にbase
メソッドを使わせたい場合、MyStruct
がBase
トレイトを実装する必要がある。
難しいことは何もなくimpl Base for MyStruct {}
と書けばそれで済むのだが、impl<T: Foo> Base for T {}
と書くこともできる。
こうすると、Foo
トレイトを実装している全ての型に対してBase
トレイトも実装することになる。このような記法をブランケット実装と呼ぶ。
fn main() { struct MyStruct {} trait Base { fn base(&self) { println!("base!"); } } trait Foo { fn foo(&self) { println!("foo!"); } } impl Foo for MyStruct {} impl<T: Foo> Base for T {} let a = MyStruct {}; a.foo(); // foo! a.base(); // base! }