年末年始にtraitの理解が進んだ私
新年あけましておめでとうございます。年末年始、皆様いかがお過ごしだったでしょうか?忘年会や新年会など半ば強制的なイベントで体調を崩したり、体が重くなったりしてませんでしょうか?
私は平常通りに過ごしました。いや平常よりも本をよく読みました。何せ理解の遅い私ですから、皆様と同じように過ごしていては年齢的にも永遠に追いつけません。年末年始は私にとって、絶好のチャンスです。やらねばなるまい……
なぜtraitが何となく掴みどころがないのか
Rustのtraitは非常に役立つスグレモノですが、ひょっとしたら私のように、Rustのtraitを何となく掴みどころのないもののように感じている方がいるかもしれませんので、まずは使い方をみていこうかと思います。Rustのtraitはまず以下のように使われます。
#[derive(Debug)]
struct A;
fn main() {
let a = A;
dbg!(a);
}
Debug
がtraitで、std::fmt
で定義されています。もしDebug
がなければdbg!
マクロは使えずエラーになります。上記のコードを自身で実装する場合、以下のようなものになります。
use std::fmt;
struct A;
impl fmt::Debug for A {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "A")
}
}
fn main() {
let a = A;
dbg!(a);
}
「長い!」「面倒くさい!」ということで、よく使われるtraitは前者のように属性として使えるようになっています。これらはユーティリティトレイトと呼ばれています。Pythonで言うならば、特殊メソッドのような機能と言えるかもしれません。上記のコードをPythonで書くならば
class A:
def __repr__(self):
return "A"
traitの次なる機能は、抽象基底クラス的なものです。Rustのtraitはデフォルトメソッドを持つことができますし、そのメソッドをオーバーライドすることもできます。
trait Device {
// Default method
fn play<'a>(software: &'a str) {
println!("PLAY: {}", software);
}
// Default method
fn stop() {
println!("STOP");
}
}
#[derive(Debug)]
struct CD;
impl Device for CD {
// Override
fn stop() {
println!("CD STOP");
}
}
#[derive(Debug)]
struct DVD;
impl Device for DVD {
// No override
}
fn main() {
CD::play("Buena Vista Social Club");
CD::stop();
DVD::play("Forrest Gump");
DVD::stop();
}
上記の場合Device
がデフォルトメソッドを持つトレイトになっています。このトレイトを実装した場合、もれなくそれらのメソッドも付いてきます。そしてそのメソッドに変更を加えたい場合のみ、それらを個別で書くという感じになります。またトレイト内で定数を書くことも可能で、これを関連定数と呼びます。
trait A {
const NAME: &'static str = "Foo";
}
struct B;
impl A for B {}
fn main() {
println!("{}", B::NAME);
}
次なる機能は関連型と呼ばれるものです。独自の型にIterator
トレイトを実装したい場合などに使うのですが、簡単な例を書いてみました。
trait Convert {
type Out;
fn convert(&self) -> Self::Out;
}
#[derive(Debug)]
struct A;
#[derive(Debug)]
struct B;
impl Convert for A {
type Out = B;
fn convert(&self) -> Self::Out { B }
}
fn main() {
let a = A;
dbg!(a.convert());
}
独自にConvert
トレイトを作ってみました。type Out;
というところが関連型と呼ばれています。From
やInto
トレイトを使う手もありますが、そちらは公式ドキュメントにもあるので割愛。関連型は、トレイト内では型を具体的に指定せず、実装した側で決めることができるといった感じです。例えばトレイト内のメソッドの戻り値を予め決めてしまった場合、柔軟性に欠ける場合があります。そのような時に有効な機能なのではないかと思います。まとめ
なぜtraitが掴みどころがないように感じるのか?他言語の場合、
interface
やclass
などのように機能別に名前が存在しますが、Rustのtraitは使い方次第で機能が変わるからではないかと思われます。つまり、この場面ではこの使い方、あの場面ではあの使い方といったように、使い分けの判断がこの掴みどころのなさの理由ではないかと思います。この使い分けの判断を身につけるには他言語の知識・経験も必要なような気もしますし、traitについてもっと知る必要があるのかもしれません。奥が深い……いや?深いと勝手に思っているだけかもしれません。実はすごく単純なのかもしれない。当ブログを読んでくださっている読者の方々、今年もどうぞよろしくお願いいたします。皆様にとって良い一年となりますように!!