トレイトを曖昧なまま使っていて、このままではいかんと、理解するためにこのページにまとめます。
参考ページ
Traits: Defining Shared Behavior - The Rust Programming Language
使用時の環境
WSL2 Ubuntu - 20.04
Rust 1.66.1
トレイトとは
構造体として「Human」を定義し、「歩く」、「走る」、「食べる」、「寝る」などの行動をメソッドして実装したとします。
さらに構造体「Cat」を定義するとき、「歩く」、「走る」、「食べる」、「寝る」はHumanと同じくメソッドとして実装できます。
こうした共通した行動(共通した性質)をくくりだし、個別の型の枠組みを越えて利用できるのがトレイトです。
たとえば「Action」という名前のトレイトを作成し、歩く、走るなどを定義しておいて、HumanやCatがこのトレイトを実装すると、走ったり歩いたりする機能が使えるようになる、といったものです。
構文
トレイトの定義
キーワード「trait」を使います。
trait トレイト名 {
fn メソッド名;
}
メソッドが名前だけの定義(戻り値があればそれも定義)であれば、このトレイトを実装する型が本体を定義する必要が出てきます。
メソッド本体を含めて定義すると、デフォルト実装となり、トレイトを実装する型はあらためて定義する必要はありません(上書き可能)。
前の項目で例として出した「Action」をトレイトとして実装します。
「cargo new animal」で新しいプロジェクトを作成。
「src/lib.rs」内に記述しました。
pub trait Action {
fn body(&self) -> &str;
fn walk(&self) {
let moved = ".".repeat(10);
println!("{}{}", moved, self.body());
}
}
このActionトレイトを実装する型は、「body」メソッドを定義しなければいけません。
「walk」メソッドはデフォルト実装になります。
型への実装
キーワード「impl」と「for」を使います。
impl トレイト名 for 型名 {}
Actionトレイトを実装する「Human」を作成します。
pub struct Human {
pub character: String,
}
impl Action for Human {
fn body(&self) -> &str {
&self.character
}
}
Humanはフィールド「character」を持っています。
実装するのはbodyメソッド。walkはデフォルトのまま使用するため未実装です。
「main.rs」に実行文を書きます。
use animal::{Action, Human};
fn main() {
let human = Human {
character: "🙎".to_string(),
};
human.walk();
}
useキーワードで「Human」と合わせて「Action」を持ち込みます。
main内でhumanはActionトレイトのwalkを利用しているため、合わせて導入しないと動かなくなります。
実行結果
..........🙎
人間を歩かせることができました。
Catを追加してみましょう。
pub struct Cat {
pub character: String,
}
impl Action for Cat {
fn body(&self) -> &str {
&self.character
}
}
use animal::{Action, Cat, Human};
fn main() {
let human = Human {
character: "🙎".to_string(),
};
let cat = Cat {
character: "🐱".to_string(),
};
human.walk();
cat.walk();
}
..........🙎 ..........🐱
実装例
下のコードはActionトレイトにrunメソッド、そしてBirdを追加したものです。
use std::io::{stdout, Write};
use std::{thread::sleep, time::Duration};
pub trait Action {
fn body(&self) -> &str;
fn character_move(&self, effect: &str, delay: u64) {
for i in 0..10 {
print!("\r");
let moved = effect.repeat(i);
print!("{}{}", moved, self.body());
stdout().flush().unwrap();
sleep(Duration::from_millis(delay));
}
println!();
}
fn walk(&self) {
println!("Walk");
self.character_move(".", 500);
}
fn run(&self) {
println!("Run");
self.character_move("-", 250);
}
}
pub struct Human {
pub character: String,
}
impl Action for Human {
fn body(&self) -> &str {
&self.character
}
}
pub struct Cat {
pub character: String,
}
impl Action for Cat {
fn body(&self) -> &str {
&self.character
}
}
pub struct Bird {
pub character: String,
}
impl Action for Bird {
fn body(&self) -> &str {
&self.character
}
}
impl Bird {
pub fn fly(&self) {
println!("Fly");
self.character_move("=", 100);
}
}
use std::io::{stdout, Write};
stdoutはStdout構造体のハンドルを返すメソッドで、そのハンドルからflushメソッドを使いますが、Writeトレイトのパスを持ち込まないと動きません。
character_moveメソッド内では、thred::sleepとtime::Durationで一時停止させ、視覚効果を出すように。
またprint!マクロは改行文字が来るまで標準出力に表示されないため、flushメソッドで出力するようにしました。
impl Bird { pub fn fly(&self) { println!("Fly"); self.character_move("=", 100); } }
BirdにActionトレイトを実装後、Bird内部で、selfをつかって、Actionトレイトのcharacter_moveメソッドにパスが通ります。
use animal::{Action, Bird, Cat, Human};
fn main() {
let human = Human {
character: "🙎".to_string(),
};
let cat = Cat {
character: "🐱".to_string(),
};
let bird = Bird {
character: "🦉".to_string(),
};
human.walk();
human.run();
cat.walk();
cat.run();
bird.fly();
}
実行すると、
Walk .........🙎 Run ---------🙎 Walk .........🐱 Run ---------🐱 Fly =========🦉
実際には使用するメソッドによって時間差で移動しています。
トレイトバウンド
引数がトレイトを実装している型に限定
pub trait Action {
fn body(&self) -> &str;
}
pub struct Human {
pub character: String,
}
impl Action for Human {
fn body(&self) -> &str {
&self.character
}
}
pub struct Cat {
pub character: String,
}
impl Action for Cat {
fn body(&self) -> &str {
&self.character
}
}
学習のためトレイト内ではなく、外部に関数を作成しようと思います。
Actionを実装した型を受け取り、挨拶をする、次のような関数にしました。
pub fn greet<T: Action>(animal: &T) {
println!("{}< HELLO!", animal.body());
}
<>と、そのカッコ内で大文字のアルファベット一文字(慣例に従い、ここではT)を使用。
そしてこのアルファベットに対し、トレイトを指定します。
この場合、引数はActionを備えた型に限定されます。
上の書き方のシンタックスシュガー。
pub fn greet(animal: &impl Action) {
println!("{}< HELLO!", animal.body());
}
引数の()内、implキーワードの後でトレイトを指定します。
引数が複数あった場合も同様に型を指定します。
pub fn greet_each_other<T: Action, U: Action>(animal1: &T, animal2: &U) {
println!("{}< Hi! Hi!>{}", animal1.body(), animal2.body());
}
// 上の関数の別の書き方
// pub fn greet_each_other(animal1: &impl Action, animal2: &impl Action) {
// println!("{}< Hi! Hi!>{}", animal1.body(), animal2.body());
// }
// 2つの引数の型が同じであることを強制したいなら
// pub fn greet_each_other<T: Action>(animal1: &T, animal2: &T) {
// println!("{}< Hi! Hi!>{}", animal1.body(), animal2.body());
// }
// この場合、シンタックスシュガーでは書けない。
use animal::{greet, greet_each_other, Cat, Human};
fn main() {
let human = Human {
character: "🙎".to_string(),
};
let cat = Cat {
character: "🐱".to_string(),
};
greet(&human);
greet_each_other(&human, &cat);
}
🙎< HELLO! 🙎< Hi! Hi!>🐱
引数がトレイトを複数実装している型に限定
下準備としてHumanに新しいフィールド「age」を持たせました。
pub trait Action {
fn body(&self) -> &str;
}
pub struct Human {
character: String,
age: u8,
}
impl Action for Human {
fn body(&self) -> &str {
&self.character
}
}
impl Human {
pub fn new(character: String, age: u8) -> Human {
Human { character, age }
}
}
また挨拶する関数を考えます。
こんどはお互いの年齢を比較し、それによってセリフを変える関数にしようと思っています。
ちょっと寄り道になりますが、標準ライブラリのトレイト「PartialOrd」をHumanに実装し、比較演算子「>, >=, <, <=」を使えるようにします。
PartialOrdを自分の型で使用するには、同じくトレイト「PartialEq」も合わせて実装する必要があります。
それぞれの必須メソッド「partial_cmp」と「eq」を定義することで使えるようになります。
「partial_cmp」はOption<Ordering>を返すようにし、「eq」はselfと比較対象を==をつかってboolを返すようにします。
use std::cmp::Ordering;
// コードは続き
impl PartialOrd for Human {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
self.age.partial_cmp(&other.age)
// if self.age > other.age {
// Some(Ordering::Greater)
// } else if self.age < other.age {
// Some(Ordering::Less)
// } else {
// Some(Ordering::Equal)
// }
}
}
impl PartialEq for Human {
fn eq(&self, other: &Self) -> bool {
self.age == other.age
}
}
準備ができたので、ActionとPartialOrdを実装した型を受け取る関数を作成します。
pub fn greet_each_other<T: Action + PartialOrd>(human1: &T, human2: &T) {
let greet1;
let greet2;
if human1 > human2 {
greet1 = "おはよう";
greet2 = "おはようございます";
} else if human1 < human2 {
greet1 = "おはようございます";
greet2 = "おはよう";
} else {
greet1 = "おはよう";
greet2 = "おはよう"
}
println!("{}<{} {}>{}", human1.body(), greet1, greet2, human2.body());
}
// 次のような書き方もできるが…
// pub fn greet_each_other(human1: &(impl Action + PartialOrd), human2: &(impl Action + PartialOrd)) {
// }
// もはやシンタックスシュガーとは言えず、同じ型であることを強制できない。
<>の中でプラス記号を使い、複数のトレイトを指定します。
たくさんのトレイトを指定すると可読性が悪くなってしまいます。
キーワード「where」を使うと、次のように書くこともできます。
pub fn greet_each_other<T>(human1: &T, human2: &T) // -> 戻り値の型
where
T: Action + PartialOrd,
{}
実行します。
use animal::{greet_each_other, Human};
fn main() {
let momo = Human::new("🙎".to_string(), 23);
let steve = Human::new("👱".to_string(), 54);
greet_each_other(&momo, &steve);
greet_each_other(&steve, &momo);
}
🙎<おはようございます おはよう>👱 👱<おはよう おはようございます>🙎
この項目では、関数の引数にトレイトを限定する書き方でしたが、トレイトや構造体のフィールドも同じように制限することができます。
trait Printtwice<T: std::fmt::Display> {
fn getter(&self) -> &T;
fn printw(&self) {
for _ in 0..2 {
println!("{}", self.getter());
}
}
}
impl Printtwice<i32> for i32 {
fn getter(&self) -> &i32 {
self
}
}
fn main() {
3.printw();
}
// 出力
// 3
// 3