Rustのtrait(トレイト)の使い方を解説します【impl, トレイト境界】
- 作成日: 2022-11-28
- 更新日: 2023-12-24
- カテゴリ: Rust
Rustのtrait(トレイト)を使う
Rustのtrait(トレイト)とは他の言語をやって来た方に言うとインターフェースと似たようなものです。
トレイトは構造体に共通のメソッドを定義したい時に使われます。
たとえばCat
とDog
という構造体があってそれぞれ名前を持っているとします。
そうすると名前を取得する共通の関数が欲しくなりますよね。
そういう時はトレイトを定義して、そのトレイトをCat
やDog
について実装します。
こうするとCat
とDog
という異なる2つの構造体に共通のメソッドを定義することができます。
関数の引数でトレイトを受け取るようにすればCat
とDog
を意識せずに関数内で名前を取得できます。
このようにトレイトは高度なプログラミングでは必須と言っていい概念です。
では具体的に解説していきます。
関連記事
RustでJSONの読み書きを行う
RustでTCPクライアント/サーバーを作る【ソケット通信】
Rustでファイルサイズを取得する方法【std::fs::metadata】
RustのBoxの使い方【ヒープにメモリを確保!初心者向け】
RustのResultでエラーハンドリング処理を行う
RustのThread(スレッド)で並行処理をする方法
RustのVecの使い方【vec!, push, pop】
Rustのassert!の使い方【assert!, assert_eq!, assert_ne!】
普通のトレイトの使い方
トレイトをまずは定義してみましょう。
Rustのトレイトはtrait
キーワードを使って定義します。
trait Animal {
// 未実装のメソッド
fn get_name(&self) -> &String;
// デフォルトメソッド
fn get_age(&self) -> usize {
return 0;
}
}
このトレイトAnimal
はget_name()
メソッドの未定義の実装と、get_age()
メソッドの定義済みの実装が書かれています。
このトレイトを使って構造体のメソッドを実装します。
まずCat
という構造体を作っておきます。
struct Cat {
name: String,
age: usize,
}
このCat
はname
とage
をフィールドに持っているなんてことはない構造体です。
このCat
について先ほどのAnimal
トレイトを実装すると、
impl Animal for Cat {
fn get_name(&self) -> &String {
return &self.name;
}
}
↑こういう感じになります。
構造体のメソッドを定義するときは構造体に対して「impl Cat
」とやっていました。
構造体についてのトレイトを実装するときは↑のように「impl Animal for Cat
」と書きます。
つまり
impl トレイト for 構造体 {
メソッド
...
}
という感じで定義します。
「構造体についてのトレイトを実装する」と覚えておけば「impl Animal for Cat
」はすぐに思い出せると思います。
この実装を行った構造体にはメソッドget_name()
が定義されている状態です。
ですので↓のように
let cat = Cat {
name: String::from("Tama"),
age: 20,
};
println!("{}", cat.get_name()); // Tama
メソッドを呼び出すことができます。
これがトレイトの実装方法ですが果たしてこれが何の役に立つのでしょうか?
これは順を追って見てみたいと思います。
複数の構造体で同一のトレイトを使う
トレイトは複数の構造体に実装することができます。
先ほどのCat
構造体の他にDog
構造体も作ります。
struct Dog {
name: String,
age: usize,
}
そしてこのDog
構造体にもAnimal
トレイトを実装します。
impl Animal for Dog {
fn get_name(&self) -> &String {
return &self.name;
}
}
そうするとDog
構造体にもget_name()
メソッドが定義されました。
ですので↓のコードが合法になります。
let cat = Cat { name: String::from("Tama"), age: 20 };
let dog = Dog { name: String::from("Pochi"), age: 30 };
println!("{}", cat.get_name()); // Tama
println!("{}", dog.get_name()); // Pochi
🐭 < はやく便利なところを解説してよ
では次からトレイトの便利なところを見ていきましょう。
複数の構造体を同一の関数で処理する
↓のような関数を定義します。
fn show_animal_name(animal: &impl Animal) {
println!("{}", animal.get_name());
}
このshow_animal_name()
関数は引数に&impl Animal
型を取ります。
このように引数を書くと構造体に実装されているトレイトを受け取ることができます。
トレイトにはget_name()
メソッドが存在しますので、animal.get_name()
のようにメソッドを呼び出すことができます。
ここら辺はインターフェースと同じ振る舞いですね。
他言語を知ってる人からすると「なんだインターフェースじゃん」という感じだと思います。
このshow_animal_name()
関数を使うと↓のようにコードを書くことができます。
let cat = Cat { name: String::from("Tama"), age: 20 };
let dog = Dog { name: String::from("Pochi"), age: 30 };
show_animal_name(&cat); // Tama
show_animal_name(&dog); // Pochi
Cat
もDog
も同じ関数に渡せて名前を表示できるという中々便利そうな具合になっています。
じっさいこれは便利でこのような振る舞いをする機能は他の言語ではインターフェースとして知られています。
関数とトレイト境界構文
Rustにはトレイト境界構文なるものがあります。
これを使うと先ほどの関数の定義は↓のように書くことができます。
fn show_animal_name_2<T: Animal>(animal: &T) {
println!("{}", animal.get_name());
}
実現できる処理はshow_animal_name()
関数と変わりません。
しかし記法がちょっと変わっていますよね。
このshow_animal_name_2()
関数も↓のようなコードで動作可能です。
let cat = Cat {
name: String::from("Tama's Blog"),
age: 20,
};
let dog = Dog {
name: String::from("Pochi's Company"),
age: 30,
};
show_animal_name_2(&cat); // Tama's Blog
show_animal_name_2(&dog); // Pochi's Company
関数で複数のトレイトを引数に取る
構造体に複数のトレイトが実装されている場合もあります。
たとえばHuman
トレイトがあって、
trait Human {
fn talk(&self);
}
このトレイトについてCat
とDog
を実装すると、
impl Human for Cat {
fn talk(&self) {
println!("Cat age is {}", self.age);
}
}
impl Human for Dog {
fn talk(&self) {
println!("Dog age is {}", self.age);
}
}
↑こんな感じになりますよね。
そうするとCat
とDog
にはAnimal
のget_name()
とHuman
のtalk()
が実装されていることになります。
異なるトレイトが2つ実装されています。
関数で複数のトレイトを同時に同じ引数に受け取りたい場合は↓のように関数を書きます。
fn show_animal_info<T: Animal + Human>(animal: &T) {
println!("{}", animal.get_name());
animal.talk();
}
こうすると引数animal
からget_name()
とtalk()
の両方を呼び出すことができるようになります。
「T: Animal + Human
」という書き方がミソですね。
let cat = Cat {
name: String::from("Tama"),
age: 20,
};
let dog = Dog {
name: String::from("Pochi"),
age: 30,
};
show_animal_info(&cat);
// Tama
// Cat age is 20
show_animal_info(&dog);
// Pochi
// Dog age is 30
where句を使ってトレイトを引数に取る
where
句の説明のためにもう1つ構造体とトレイトを作っておきます。
trait Food {
fn get_name(&self) -> &String;
}
struct CatFood {
name: String,
}
impl Food for CatFood {
fn get_name(&self) -> &String {
return &self.name;
}
}
キャットフードですね。
で、where
句というものを使うとですね、先ほどのような関数は↓のようにも書くことができます。
fn show_animal_data<T, U>(animal: &T, food: &U)
where T: Animal + Human,
U: Food {
println!("{}", animal.get_name());
animal.talk();
println!("{}", food.get_name());
}
「T
」には「Animal + Human
」が、「U
」には「Food
」が設定されます。
これをwhere
句を使わないで書くと非常に横に伸びた書き方になってしまいますが、where
句を使えば↑のように読みやすく書けるというわけですね。
このshow_animal_data()
関数を使うと↓のようなコードになります。
let cat = Cat {
name: String::from("Tama"),
age: 20,
};
let dog = Dog {
name: String::from("Pochi"),
age: 30,
};
let cat_food = CatFood {
name: String::from("Yamitsuki Cat"),
};
show_animal_data(&cat, &cat_food);
// Tama
// Cat age is 20
// Yamitsuki Cat
show_animal_data(&dog, &cat_food);
// Pochi
// Dog age is 30
// Yamitsuki Cat
関数からトレイトを返す
関数の返り値(戻り値)としてトレイトを返すこともできます。
fn get_cats_animal(cat: &Cat) -> &impl Animal {
return cat;
}
let cat = Cat {
name: String::from("Tama"),
age: 20,
};
let animal = get_cats_animal(&cat);
println!("{}", animal.get_name()); // Tama
トレイト境界でメソッドの実装を条件分けする
ちょっと難しい話になりますが、トレイト境界を使うとメソッドを実装するかしないかの分岐を書くことができます。
たとえば↓のような感じです。
struct Point<T> {
x: T,
y: T,
}
impl<T> Point<T> {
fn new(x: T, y: T) -> Self {
Self {
x,
y,
}
}
}
impl<T: std::fmt::Display> Point<T> {
fn show_data(&self) {
println!("{} {}", self.x, self.y);
}
}
↓のように書くとstd::fmt::Display
が実装されているPoint
にだけshow_data()
メソッドが実装されます。
impl<T: std::fmt::Display> Point<T> {
fn show_data(&self) {
println!("{} {}", self.x, self.y);
}
}
let point = Point::new(1, 2);
point.show_data();
省エネって感じですね。
無駄な実装はしなくて済むという低コストなRustの思想が現れていると思います。
おわりに
今回はRustのtrait(トレイト)を使う方法について解説しました。
トレイトは非常に便利な機能でこれを知っているといないとではプログラムの質も変わってくるかと思います。
ぜひマスターしておきたいところです。
🦝 < 僕の名前はトレイト
🐭 < いろんな構造に実装されるのさ