トレイト
トレイトとは、型が実装しなければならないメソッドや定数を定義するものです。逆に言えば、あるトレイトを実装している型は、そのトレイトが定義しているメソッドや定数を実装していることが保証されます。
型によって振る舞いを変えることをアドホックポリモーフィズムといいます。Rustでアドホックポリモーフィズムを実現する際にトレイトが利用されます。
トレイトの定義
トレイトの定義は以下のように書きます。
trait
トレイト名
{
メソッドや定数
...
}
以下の例では、車を定義しています。
ここではとりあえず、「走って給油するもの」を車とします。
// 車とは、走って給油するもの という定義
trait Car {
// 走行距離が増えて燃料が減る。燃料の消費量を返す。
fn run(&mut self, distance: u32) -> u32;
// 給油する。給油後の燃料の量を返す。
fn refuel(&mut self, fuel: u32) -> u32;
}
トレイトの実装
ある型にトレイトを実装するには、以下のように書きます。
impl
トレイト名
for
型名
{
型における具体的な実装
}
トレイトに定義したものは必ず実装しなければいけません。
以下の例では、トラック構造体を定義し、車トレイトを実装しています。
// トラック
struct Truck {
distance: u32, // 走行距離[km]
consumption: u32, // 燃費[km/L]
fuel: u32, // 燃料[L]
}
// トラックに対するCarの実装
impl Car for Truck {
fn run(&mut self, distance: u32) -> u32 {
self.distance += distance;
self.fuel -= self.distance / self.consumption;
println!(
"トラックで荷物を{} km運びました。燃料は残り{} Lです。",
distance, self.fuel
);
self.distance * self.consumption
}
fn refuel(&mut self, fuel: u32) -> u32 {
self.fuel += fuel;
println!(
"トラックに{} L給油しました。燃料は残り{} Lです。",
fuel, self.fuel
);
self.fuel
}
}
ジェネリクスを使用した関数
ジェネリクスとは具体的な型名を指定せずに処理を記述する機能です。これにより、同じような処理を1つの関数で記述できます。しかし、具体的な型は問わないとはいえ、「○○メソッドを実装している」などの条件が必要な場合があります。Rustではこの条件を「トレイト境界」と呼び、トレイトの実装の有無によって受け入れる型を限定します。
以下の例では、ジェネリクスを使用し、車トレイトを実装していればどんな型でも受け入れる関数を定義しています。
// 借りた車を走らせた後、元の量まで給油して返す。
// Tという型を用意し、TはCarを実装している型に限定する。
// Carを実装している型は、run()とrefuel()を実装していることが保証されるため、具体的な型名は問わない。
fn drive<T>(mut car: T, distance: u32) -> T
where
T: Car, // TはCarを実装した型に限定する → トレイト境界
{
let consumed_fuel = car.run(distance);
car.refuel(consumed_fuel);
car
}
使い方
書いてませんが、トラックの他に、スポーツカーも実装しました。これで、Carトレイトを実装した型が2つ存在することになります。後は、普通に関数を呼び出し、引数にCarトレイトを実装した型のデータを渡すだけです。同じ関数を2度呼び出しているだけに見えますが、実際はトラックとスポーツカーで異なる関数が呼び出されています。
fn main() {
let my_truck = Truck {
distance: 0,
consumption: 5,
fuel: 50,
};
let my_sports_car = SportsCar {
distance: 0,
consumption: 10,
fuel: 30,
};
// 同じ関数名だが、型によって異なる関数を実行している。
drive(my_truck, 100);
drive(my_sports_car, 200);
}
実行結果
トラックで荷物を100 km運びました。燃料は残り30 Lです。
トラックに20 L給油しました。燃料は残り50 Lです。
スポーツカーで200 kmドライブしました。燃料は残り10 Lです。
スポーツカーに20 L給油しました。燃料は残り30 Lです。
コード全体
今回書いたコードの全体です。今後新たに型を追加する場合、型とトレイトを実装するだけでdrive()
関数は修正する必要はありません。このようにトレイトとジェネリクスをうまく使うとソースの継ぎ足しが簡単かつ安全になります。複雑なプログラムほど、この恩恵は大きいと思います。
トレイトとジェネリクスを使ったサンプルコード
// 車とは、走って給油するもの という定義
trait Car {
// 走行距離が増えて燃料が減る。燃料の消費量を返す。
fn run(&mut self, distance: u32) -> u32;
// 給油する。給油後の燃料の量を返す。
fn refuel(&mut self, fuel: u32) -> u32;
}
// トラック
struct Truck {
distance: u32, // 走行距離[km]
consumption: u32, // 燃費[km/L]
fuel: u32, // 燃料[L]
}
// スポーツカー
struct SportsCar {
distance: u32, // 走行距離[km]
consumption: u32, // 燃費[km/L]
fuel: u32, // 燃料[L]
}
// トラックに対すCarの実装
impl Car for Truck {
fn run(&mut self, distance: u32) -> u32 {
self.distance += distance;
self.fuel -= self.distance / self.consumption;
println!(
"トラックで荷物を{} km運びました。燃料は残り{} Lです",
distance, self.fuel
);
self.distance * self.consumption
}
fn refuel(&mut self, fuel: u32) -> u32 {
self.fuel += fuel;
println!(
"トラックに{} L給油しました。燃料は残り{} Lです",
fuel, self.fuel
);
self.fuel
}
}
// スポーツカーに対するCarの実装
impl Car for SportsCar {
fn run(&mut self, distance: u32) -> u32 {
self.distance += distance;
self.fuel -= self.distance / self.consumption;
println!(
"スポーツカーで{} kmドライブしました。燃料は残り{} Lです",
distance, self.fuel
);
self.distance * self.consumption
}
fn refuel(&mut self, fuel: u32) -> u32 {
self.fuel += fuel;
println!(
"スポーツカーに{} L給油しました。燃料は残り{} Lです",
fuel, self.fuel
);
self.fuel
}
}
// 借りた車を走らせた後、元の量まで給油して返す。
// Tという型を用意し、TはCarを実装している型に限定する。
// Carを実装している型は、run()とrefuel()を実装していることが保証されるため、具体的な型名は問わない。
fn drive<T>(mut car: T, distance: u32) -> T
where
T: Car, // TはCarを実装した型に限定する → トレイト境界
{
let consumed_fuel = car.run(distance);
car.refuel(consumed_fuel);
car
}
fn main() {
let my_truck = Truck {
distance: 0,
consumption: 5,
fuel: 50,
};
let my_sports_car = SportsCar {
distance: 0,
consumption: 10,
fuel: 30,
};
// 同じ関数名だが、型によって異なる関数を実行している。
drive(my_truck, 100);
drive(my_sports_car, 200);
}
余談
理解するのに苦労しました。シンプルに難解です。所有権などは概念自体は簡単で、ソースの読み書きが大変でしたが、トレイトは概念を理解するところが壁になりました。
私なりにトレイト(とジェネリクス)の良さがわかるようにサンプルを書きましたが、トレイトの実力はまだまだこんなものではない気がしています。この手の機能はもっと大規模で抽象的な処理で真価を発揮すると思っています。私一人でプログラミングするプログラムの規模では、あまり恩恵はないのかもしれません。