【Rust】コードで理解するトレイト
はじめに
主に僕は書籍を通じてRustを勉強していたが、はっきり言って全く理解できなかったので生存期間に引き続きコードで記述する事で理解しようと思った。
Rustのモジュールの考え方について
Rustは任意の型に対して通常 impl
を用いて以下のようにモジュールを追加する。
struct Cat { name: String, eyes: String, } impl Cat { fn new(name: &String, eyes: &String) -> Self { Cat { name: name.to_string(), eyes: eyes.to_string(), } } fn cry(&self, cry_str: String) { println!("{} eyes {} say, {}", &self.eyes, &self.name, cry_str) } } fn main() { let cat = Cat::new(&"JiJi".to_string(), &"Black".to_string()); cat.cry("myaw".to_string()); //-> Black eyes JiJi say, myaw }
Goをやっていた人からすると、コードの流れとしては Go
にすごい似ていることに気づくだろう。 Cat
型を定義した後、impl
キーワードを使って fn cry(&self, cry_str: String)
メソッド関数を定義している。
こうする事で、new
または cry
関数は Cat
型専用の関数となるので、保守性と可読性が増すという訳だ。
Rustのモジュールでの課題
こうする事で、
new
またはcry
関数はCat
型専用の関数となるので、保守性と可読性が増すという訳だ。
この通り、"new
または cry
関数は Cat
型専用の関数" となってしまう訳で、例えば他にも声を出して鳴くであろう同様の Dog
や Devil
やPikachu
など定義する時に、コードが冗長化する。
そこで今回の トレイト (trait)が登場する訳だ。
やっていき
ようは、この new またはcryするという機能
というものAnimal
という名前の trait
として定義して、型に一段DIするという運びとなる。
struct Cat { name: String, eyes: String, } struct Dog { name: String, eyes: String, } trait Animal { fn new(name: &String, eyes: &String) -> Self; fn cry(&self, cry_str: String); } fn animal_new<T: Animal>(name: &String, eyes: &String) -> T { Animal::new(&name.to_string(), &eyes.to_string()) } fn animal_cry<T: Animal>(animal: T, cry_str: String) { animal.cry(cry_str); } impl Animal for Cat { fn new(name: &String, eyes: &String) -> Self { Cat { name: name.to_string(), eyes: eyes.to_string(), } } fn cry(&self, cry_str: String) { println!("Cat, {} eyes {} say, {}", &self.eyes, &self.name, cry_str) } } impl Animal for Dog { fn new(name: &String, eyes: &String) -> Self { Dog { name: name.to_string(), eyes: eyes.to_string(), } } fn cry(&self, cry_str: String) { println!("Dog, {} eyes {} say, {}", &self.eyes, &self.name, cry_str) } } fn main() { let cat: Cat = animal_new(&"JiJi".to_string(), &"Black".to_string()); let dog: Dog = animal_new(&"Mugi".to_string(), &"Red".to_string()); animal_cry(cat, "myaw".to_string()); //-> Cat, Black eyes JiJi say, myaw animal_cry(dog, "bowow".to_string()); //-> Dog, Red eyes Mugi say, bowow }
解説はじめ
トレイト単体で言えば、C++でいう仮想関数や、Goでいうinterfaceと言ったものに非常に似ている。
はじめに animal_new
関数により、2つの異なる型を同時に初期化している。
中身が注目で2つの異なる型のnew
メソッドを Animal
トレイトから出せるようになっている事がわかる。
これにより、 Cat
型と Dog
型を同等に扱える。
そして以下に何やら怪しい形の関数を定義していることに注目されたい。
fn animal_new<T: Animal>(name: &String, eyes: &String) -> T { Animal::new(&name.to_string(), &eyes.to_string()) } <snip> let cat: Cat = animal_new(&"JiJi".to_string(), &"Black".to_string()); let dog: Dog = animal_new(&"Mugi".to_string(), &"Black".to_string()); <snip>
型もメソッドもDIしたのであれば、せっかくなので同じ関数で呼び出したいとなるので、満を辞して ジェネリクス
さんの登場してもらうことになる。
Rustのジェネリクスはとても賢いので、定義したtraitを紐づけてやると、制限つきのジェネリクス、つまりAnimalトレイトをimplしている型だけ使えるようにジェネリクスを縛れる。
こうして定義した関数を ジェネリクス関数 と呼ぶらしい。
縛られているか証拠に試しに Flog
型 を定義してみると期待通りコンパイルエラーとなる。
struct Flog { name: String, eyes: String, } <snip> let flog: Flog = animal_new(&"Pyoko".to_string(), &"Black".to_string()); <snip>
Compiling playground v0.0.1 (/playground) error[E0277]: the trait bound `Flog: Animal` is not satisfied --> src/main.rs:57:22 | 57 | let flog: Flog = animal_new(&"Pyoko".to_string(), &"Black".to_string()); | ^^^^^^^^^^ the trait `Animal` is not implemented for `Flog` |
こうして型安全に初期化されたら、実際に鳴くときもジェネリクス関数を使えば、違う型の違う実装の、しかし同じ名前のメソッドが使える
という訳だ。
fn animal_cry<T: Animal>(animal: T, cry_str: String) { animal.cry(cry_str); } <snip> animal_cry(cat, "myaw".to_string()); //-> Cat, Black eyes JiJi say, myaw animal_cry(dog, "bowow".to_string()); //-> Dog, Red eyes Mugi say, bowow <snip>
感想
ジェネリクスとトレイトを使い回せればかなりコードを簡略化してかけそう😝