【Rust入門】トレイトの基本を分かりやすく解説

Rust のトレイトの基本について初心者にも分かりやすく解説します。
Rust のトレイト
トレイトとは
Rust におけるトレイト(trait)は、型に共通の振る舞いを定義する仕組みです。Haskell の型クラスに非常によく似ており、Java 等のオブジェクト指向言語ではインターフェースに近いものとなっています。
オブジェクト指向では、振る舞いをクラスに内包されたメソッドで定義しますが、Rust のトレイトではデータ構造(struct
や enum
)と振る舞い(trait
)を分離する点で違いがあります。型そのものには基本的にはデータ構造しかなく、振る舞いはトレイトを後から付与して実装するという考え方です。
トレイトを使用することで、例えば「コンソールに出力ができる」「数値計算ができる」のような共通の振る舞いを定義し、型がその振る舞いをどのように実現するのかを実装できるようになります。
この記事では、トレイトの基本について分かりやすく解説します。
トレイトの定義と実装の基本
トレイトの説明をする際によく利用される図形を用いた例でトレイトを説明します。例えば、図形には、Circle
(円)、Rectangle
(矩形)などを色々と考えることができます。これらの図形に共通的なことを考えてみると「面積を計算できる」ということが挙げられます。
領域に関するトレイトを Area
トレイトとして、面積を計算する振る舞いを area
メソッドとして実装してみましょう。
use std::f64::consts::PI; // 領域に関するトレイト trait Area { // 面積を計算するメソッド fn area(&self) -> f64; } // 円の構造体の定義 struct Circle { radius: f64 } // トレイトの実装 (Circle) impl Area for Circle { fn area(&self) -> f64 { PI * self.radius * self.radius } } // 矩形の構造体の定義 struct Rectangle { width: f64, height: f64 } // トレイトの実装 (Rectangle) impl Area for Rectangle { fn area(&self) -> f64 { self.width * self.height } } fn main() { let circle = Circle{ radius: 5.0 }; let circle_area = circle.area(); println!("Circle(radius={:.2}), Area: {:.2}", circle.radius, circle_area); let rect = Rectangle{ width: 5.0, height: 10.5, }; let rect_area = rect.area(); println!("Rectangle(width={:.2}, height={:.2}), Area: {:.2}", rect.width, rect.height, rect_area); }
【実行結果】 Circle(radius=5.00), Area: 78.54 Rectangle(width=5.00, height=10.50), Area: 52.50
Area
トレイトは「trait Area {}
」のように定義し、内部にはそのトレイトが持つべきメソッドの定義を「fn area(&self) -> f64;
」のように型注釈を含めて記載します。なお、今回は area
というメソッドのみですが Area
という領域に関する振る舞いを他に定義したい場合は、複数のメソッドを定義可能です。
今回用意した円(Circle
)や矩形(Rectangle
)という型に Area
トレイトを実装するには「impl Area for 型名 {}
」のように定義し、内部では先ほど型注釈のみで実装はしていなかった area
メソッドの具体的な実装を記載します。
なお、トレイトでは、トレイトが定義するメソッドを実装することを強制します。厳密には、後ほど説明するデフォルト実装がないメソッドの実装が必須です。
そのため、「impl Area for 型名 {}
」と宣言したら、area
メソッドの実装は必須です。もし他にもメソッドがトレイトで定義されていたら全てのメソッドを実装する必要があります。これにより、あるトレイトを持つ型は、そのトレイトで定義されているメソッドが確実に実装されているものとして安心して型を使用することができます。
main
関数では、各型をインスタンス化していますが、それぞれ Area
トレイトを実装しているので area
メソッドを使用して面積を計算できていることが分かります。他の形状の型が出てきた場合にも、それぞれに関する Area トレイトの実装を追加していくことができます。
トレイトのデフォルト実装
トレイトには「デフォルト実装」として基本となる実装を定義しておくことができます。デフォルト実装とは、もし型側で実装が行われなかった場合に使用される実装のことです。
Area
トレイトに describe
という面積を表示するメソッドを追加してデフォルト実装の例を見てみましょう。
use std::f64::consts::PI; // 領域に関するトレイト trait Area { // 面積を計算するメソッド fn area(&self) -> f64; // デフォルト実装 fn describe(&self) { println!("Area: {:.2}", self.area()) } } // 円の構造体の定義 struct Circle { radius: f64 } // トレイトの実装 (Circle) impl Area for Circle { fn area(&self) -> f64 { PI * self.radius * self.radius } // describe はデフォルト実装を使用 } // 矩形の構造体の定義 struct Rectangle { width: f64, height: f64 } // トレイトの実装 (Rectangle) impl Area for Rectangle { fn area(&self) -> f64 { self.width * self.height } // describeのデフォルト実装をオーバーライドしている fn describe(&self) { println!("Rectangle(width={:.2}, height={:.2}), Area: {:.2}", self.width, self.height, self.area()); } } fn main() { let circle = Circle{ radius: 5.0 }; let _circle_area = circle.area(); circle.describe(); let rect = Rectangle{ width: 5.0, height: 10.5, }; let _rect_area = rect.area(); rect.describe(); }
【実行結果】 Area: 78.54 Rectangle(width=5.00, height=10.50), Area: 52.50
上記例では describe
により area
を呼び出した結果の面積の値を println
で表示するようにしています。これにより、型で describe
を実装していなくても「Area: xx.xx
」と表示されるようになります。これがデフォルト実装です。
Circle
型に対する Area
トレイトの実装には、describe
に関する定義をしていませんが main
関数内では問題なく describe
が使用できています。なお、デフォルト実装を用意していないメソッドを実装していない場合は「missing describe in implementation
」というコンパイルエラーとなります。これは、トレイトが定義するメソッドの実装が強制されているためです。
一方で、Rectangle
型についてはdescribe
メソッドを個別に定義してオーバーライドすることで、デフォルト実装とは異なる表示ができていることが分かります。
トレイトの自動実装 #[derive()]
トレイトの自動実装の使い方
Rust のトレイトでは、標準トレイトが用意されており、作成した型に対して #[derive()]
を使用することで、トレイトを自動で実装することができます。
代表的な例としては、作成した型の情報を println
で表示したい場合です。通常は、構造体名.フィールド名
というようにアクセスして表示する必要がありますが、標準トレイトに Debug
というトレイトが用意されており、以下の例のように derive
することで println
の {:?}
で型の値を表示することができるようになります。
// 円の構造体の定義 #[derive(Debug)] struct Circle { radius: f64 } // 矩形の構造体の定義 #[derive(Debug)] struct Rectangle { width: f64, height: f64 } fn main() { let circle = Circle{ radius: 5.0 }; println!("{:?}", circle); let rect = Rectangle{ width: 5.0, height: 10.5, }; println!("{:?}", rect); }
【実行結果】 Circle { radius: 5.0 } Rectangle { width: 5.0, height: 10.5 }
Debug
トレイトの自動実装を行うためには、型(struct
)定義の上に「#[derive(Debug)]
」というように記載します。他にも複数の標準トレイトを自動実装したい場合には「,
」で列挙することが可能です。
これにより、main
関数内での println
で {:?}
とした部分に型のインスタンスを指定すると「Circle { radius: 5.0 }
」や「Rectangle { width: 5.0, height: 10.5 }
」といったように型名とフィールドの値を表現した形で表示することが可能になります。
トレイト自動実装が使える標準ライブラリのトレイト
Rust の標準ライブラリ(std)のドキュメントの Traitsで、標準ライブラリで定義されているトレイトの一覧を確認できます。各 Traits の参照先ドキュメントで、derive
できるかどうかを確認することができます。代表的な derive
できる標準トレイトには以下があります。
標準トレイト | 概要 |
---|---|
Clone #[derive(Clone) | 値の複製するトレイト。clone() を提供する。全てのフィールドが Clone なら derive 可能。 |
Copy #[derive(Copy, Clone)] | ビット単位でコピー可能にするトレイト。全てのフィールドが Copy のときのみ derive 可能。Copy を実装する型は必ず Clone の実装が必要。 |
Debug #[derive(Debug)] | デバッグ用の出力を可能にするトレイト。{:?} フォーマットで使用でき、ログや開発中の確認に使用する。 |
Default #[derive(Default)] | 型のデフォルト値を生成する default() を提供する。全てのフィールドが Default なら derive 可能。 |
PartialEq #[derive(PartialEq)] | == や != で比較できるようにする。 |
Eq #[derive(Eq, PartialEq)] | 数学的に厳密な等価関係を持ち、反射律、対称律、推移律を満たす。f64 などの浮動小数点は、NaN == NaN が false で反射律を満たさないため Eq ではない。Eq を実装する型は必ず PartialEq でもある。 |
PartialOrd #[derive(PartialOrd)] | 大小比較「< , > , <= , >= 」ができるようにする。 |
Ord #[derive(Ord, PartialOrd)] | 全ての要素間で大小が決まる(全順序)。f64 などの浮動小数点は、NaN を含むと比較が成立しないため Ord ではない。Ord を実装する型は必ず PartialOrd でもある。 |
Hash #[derive(Hash, Eq, PartialEq)] | 値をハッシュ可能にする。HashMap や HashSet のキーとして使えるようになる。Eq と整合し、a == b であれば、hash(a) == hash(b) である必要があります。 |
「Copy
を実装する型は Clone
も実装している必要がある」といったようなルールがある型については「#[derive(Copy, Clone)]
」のようにセットで使用する必要があるので注意が必要です。列挙する場合の順序は関係ありません。
また、Debug
が derive
できるので std::fmt::Display
でもできそうな気がしますが、Display
トレイトは使用できません。Debug
が開発者がとりあえず読める形で出せればよいという性質であるのに対して、Display
はユーザー向けの整形出力で、状況に応じた文言やフォーマットを設計する必要があるため、自動実装の対象に含まれていません。
このように標準トレイトでも必ずしも自動実装が使えるわけではないことは覚えておいてください。また、自動実装で求めている機能を提供できるかは十分に確認してください。もし、求めている内容ではない場合には、自分で実装をしてオーバーライドする必要があります。
トレイト境界
トレイトは、共通の振る舞いを型に付与できるものでした。ジェネリックな関数を作成する際には、引数の型があるトレイトを実装している型のみを許容するように制限することができます。このような制約のことを「トレイト境界(Trait Bounds)」と言います。
例えば、以下のように面積を表示するような関数 show_area
を定義してみます。
use std::f64::consts::PI; use std::fmt::Debug; // 領域に関するトレイト trait Area { // 面積を計算するメソッド fn area(&self) -> f64; } // 円の構造体の定義 #[derive(Debug)] struct Circle { radius: f64 } // トレイトの実装 (Circle) impl Area for Circle { fn area(&self) -> f64 { PI * self.radius * self.radius } } // 矩形の構造体の定義 #[derive(Debug)] struct Rectangle { width: f64, height: f64 } // トレイトの実装 (Rectangle) impl Area for Rectangle { fn area(&self) -> f64 { self.width * self.height } } // 面積を表示する関数 (トレイト境界) fn show_area<T: Area + Debug>(shape: &T) { println!("{:?}, Area: {:.2}", shape, shape.area()); } fn main() { let circle = Circle{ radius: 5.0 }; show_area(&circle); let rect = Rectangle{ width: 5.0, height: 10.5, }; show_area(&rect); }
【実行結果】 Circle { radius: 5.0 }, Area: 78.54 Rectangle { width: 5.0, height: 10.5 }, Area: 52.50
show_area
では、型の情報を表示し、面積を計算した結果を表示するので Debug
トレイトと Area
トレイトを持った型を引数に受け取りたくなります。
このような場合でトレイト境界を指定するには、関数名の後ろに <T: Area + Debug>
のように制約のトレイトを記載します。T
はジェネリックの型パラメータで、Area
と Debug
が実装された任意の型ということを示します。複数のトレイトを制約として指定したい場合には「+
」で記載します。Debug
は「use std::fmt::Debug;
」で宣言する必要があるので注意してください。
また、トレイト境界は以下のように where
を使用しても表現可能です。ジェネリックの型パラメータが増えた場合には where
を使用した方が可読性が上がる可能性があります。
// 面積を表示する関数 (トレイト境界を where 指定) fn show_area<T>(shape: &T) where T: Area + Debug { println!("{:?}, Area: {:.2}", shape, shape.area()); }
ジェネリックな関数は、任意の型を受け取れる柔軟性がある一方で、関数の処理に適さない型を受け取ってしまうと予期せぬエラーが発生してしまいます。トレイト境界で、関数の処理の前提となる型の特徴を指定することができるので、安全に処理を実行できる関数を実現できます。
まとめ
Rust のトレイト(trait)の基本について解説しました。トレイトとは、型に共通の振る舞いを定義する仕組みで、Rust では、データ構造と振る舞いを分離して設計する特徴があります。
トレイトは、trait
キーワードで定義し、各型に対して impl
で実装をします。デフォルト実装を用意していないメソッドは必ず実装する必要があり、これにより型の利用者はトレイトのメソッドが実装されているものとして安心して利用できます。
Rust には標準トレイトが色々と用意されていますが、derive
による自動実装が使用できます。便利である一方で Display
のように自動実装できないものもあります。また、自動実装が求めている結果となるかはよく確認が必要です。
トレイトは、ジェネリックな関数に対してトレイト境界として、型が特定のトレイトを実装している必要があるという制約を表現できます。これにより安全で柔軟な関数設計ができます。
このようにトレイトは、Rust の柔軟で強力な型システムを支える重要な仕組みとなっています。単なる共通の振る舞いの定義にとどまらず、抽象化、コード再利用性、安全性の確保といった観点でも役に立ちます。ぜひ、トレイトの基本を理解してもらいたいと思います。