トレイトとは?
他の言語における「インターフェース」に近いもので、構造体や型などにメソッドを追加することができる機能です。トレイトを使うことで、対象のデータ型が特定のメソッドを持っていることを保証することができます。
おさらいですが、関数と違いメソッドは何かしらのオブジェクトのインスタンスに紐づいています。
オブジェクトにメソッドを持たせるだけであればトレイトは不要(impl
で十分)ですが、そのメソッドの集合に名前を付けたり、あるデータ型がそのメソッドの集合を実装していることを保証したりするのであればトレイトが必要になります。
トレイトの宣言
trait トレイト名 { ... }
の形で宣言します。ブロックの中にはfn
でメソッド(か関連関数)のシグネチャを列挙します。
ここで書くのはあくまでシグネチャ(引数名とその型、および戻り値)であるので、メソッドの処理そのものは定義しません。
厳密に言えば、処理を定義する(普通のメソッドのように実装する)と、それがデフォルトの処理になります。すなわち、トレイトの実装時にそのメソッドの実装をしなかった場合はデフォルトの処理のほうが呼ばれます。
// トレイトの定義
trait Greet {
fn greet(&self) -> String;
}
トレイトの実装
impl トレイト名 for データ型 { ... }
で対象のデータ型にトレイトを実装します。ブロックの中には、トレイトで定義されているメソッドを実装します。トレイトの方で要求されているのに実装していないメソッドがあるとコンパイルエラーになります(前述のデフォルト実装されているものを除く)。
// 構造体
struct Person {
name: String,
}
// トレイトの実装
impl Greet for Person {
fn greet(&self) -> String {
format!("Hello, my name is {}!", self.name)
}
}
// 使用例
fn main() {
let person = Person {
name: String::from("Alice"),
};
println!("{}", person.greet());
}
トレイトとジェネリック型
ジェネリック型の条件として、特定のトレイトを実装していることを指定できます。
ジェネリック型は<T>
の構文で定義しますが、<T: トレイト名>
と書くと、その型T
が指定したトレイトを実装していることを強制することができます。
// トレイトの定義
trait Summable {
fn sum(&self) -> i32;
}
// トレイトを実装した構造体
struct Numbers {
values: Vec<i32>,
}
impl Summable for Numbers {
fn sum(&self) -> i32 {
self.values.iter().sum()
}
}
// ジェネリックな関数
fn print_sum<T: Summable>(item: &T) {
println!("The sum is: {}", item.sum());
}
// 使用例
fn main() {
let numbers = Numbers {
values: vec![1, 2, 3, 4],
};
print_sum(&numbers);
}
引数にトレイトを要求するシンタックスシュガー
関数の引数の型があるトレイトを実装していることを強制したい場合、fn 関数名<T: トレイト名>(引数名: &T)
と書くこともできますが、もっと簡潔にかけるよう、fn 関数名(引数名: &impl T)
というシンタックスシュガーが用意されています。
// 簡潔な構文
fn print_sum_simplified(item: &impl Summable) {
println!("The sum is: {}", item.sum());
}
// 使用例
fn main() {
let numbers = Numbers {
values: vec![1, 2, 3, 4],
};
print_sum_simplified(&numbers);
}
条件を増やす
ある型T
が複数のトレイトを実装していることを強制したい場合は、トレイトを+
で連結します。
// トレイトの定義
trait Displayable {
fn display(&self) -> String;
}
// 構造体
struct Data {
value: i32,
}
// 複数のトレイトを実装
impl Summable for Data {
fn sum(&self) -> i32 {
self.value
}
}
impl Displayable for Data {
fn display(&self) -> String {
format!("Value: {}", self.value)
}
}
// 関数で複数トレイトを要求
fn display_sum<T: Summable + Displayable>(item: &T) {
println!("{}", item.display());
println!("The sum is: {}", item.sum());
}
// 使用例
fn main() {
let data = Data { value: 42 };
display_sum(&data);
}
条件がもっと増える場合は、<>
の中に書くのではなく、where
句を使って別の行に切り出すこともできます。
fn display_sum_where<T>(item: &T)
where
T: Summable + Displayable,
{
println!("{}", item.display());
println!("The sum is: {}", item.sum());
}
// 使用例
fn main() {
let data = Data { value: 42 };
display_sum_where(&data);
}
このトレイトの仕組みは、総じて他の言語におけるインターフェースと似ています。個人的によくハマるのが、Copy
やPartialEq
、PartialOrd
、Display
といった標準で用意されていてちょっとした処理にも要求されるトレイトを、独自の構造体に実装し忘れることです。