Rust トレイト境界 (where句) の考え方と使い方
はじめに
この記事では、トレイト境界の概要と、where句の基本的な実装例について解説します。
トレイト境界
トレイト境界(trait bounds)とは、ジェネリック型に対する制約のことをいいます。
トレイト境界を利用することで、ジェネリック型パラメータに特定のtraitが実装されていることを保証することができます。
次の例のように、Tに対しDisplay traitの型制約を課すことによって、Display traitが実装された引数として扱うことができます。
fn some_function<T: Display>(arg: T) {
// ここで`arg`は`Display`トレイトのメソッドを使用できる
}
このように、トレイト境界は型制約のことでもあるため、「境界」という言葉が少しイメージしづらいかもしれません。
Rust By Example[1]によると、「境界」という言葉は以下のようなイメージで捉えるのが良さそうです。
- トレイト境界が指定されていない場合、ジェネリック型が
Any
に相当するような、あらゆる型の値を受け取ることができます。 - トレイト境界(Trait1) が指定された場合は、Trait1を実装している型のみ受け入れられます。
- トレイト境界(Trait1 + Trait2) が指定された場合は、Trait1とTrait2どちらも実装している型のみ受け入れられます。
where 句
where 句は、トレイト境界を指定する際に使用される構文です。
where 句を使用することで、複雑なトレイト境界を持つジェネリック型に対して、より読みやすく、柔軟な方法でトレイト境界を表現できます。
- 関数のシグネチャ内に直接記述する場合
fn example<T: Display + Clone, U: Debug + PartialEq>(t: T, u: U) {
// 関数の中身
}
- where句を使用する場合
fn example<T, U>(t: T, u: U)
where
T: Display + Clone,
U: Debug + PartialEq,
{
// 関数の中身
}
実装例
ここでは構造体のメソッドでトレイト境界が使用されるような実装例を、where句を用いて紹介します。
ジェネリック構造体のメソッドとして使用されるトレイト境界
次のコードでは、Hoge structの display
メソッドにwhere句が使用されています。
この例では、Hoge構造体の display
メソッドは T: std::fmt::Display
を満たす場合のみ使用可能です。
struct Hoge<T> {
value: T,
}
impl<T> Hoge<T> {
fn new(value: T) -> Self {
Hoge { value }
}
fn display(&self)
where
T: std::fmt::Display,
{
println!("Value: {}", self.value);
}
}
そのため、display
メソッドを使用する側では以下のような挙動になります。
fn main() {
let ex1 = Hoge::new(1); // 1というi32 のパラメータが渡されており、これがstd::fmt::Displayトレイトを実装している
ex1.display(); // 正常に動作する
let ex2 = Hoge::new(MyStruct { value: 1 }); // std::fmt::Display を継承しない MyStruct を引数にとる
ex2.display(); // ここでコンパイルエラーになる
}
struct MyStruct {
value: i32,
}
つまり、 std::fmt::Display
が実装されたパラメータ(ここでは 1 )を渡された場合のみdisplay メソッドを使用できるようになっており、
結果として where句によってジェネリック型に制約を加えられたことになります。
複数のジェネリック型パラメータ間のトレイト境界
次のコードでは、Hoge structの compare
メソッドにwhere句が使用されています。
この例では、 Hoge structの compare
メソッドが、TとUで比較可能( PartialEq<U>
を実装している)である場合のみ使用可能です。
struct Hoge<T, U> {
a: T,
b: U,
}
impl<T, U> Hoge<T, U> {
fn new(a: T, b: U) -> Self {
Hoge { a, b }
}
fn compare(&self)
where
T: PartialEq<U>,
{
if self.a == self.b {
println!("equal.");
} else {
println!("not equal.");
}
}
}
そのため、compare
メソッドを使用する側では以下のような挙動になります。
fn main() {
let ex1 = Hoge::new(1, 2); // 1, 2という相互に比較可能なパラメータを渡している. (i32同士は比較可能)
ex1.compare(); // 正常に動作する
let ex2 = Hoge::new(MyStruct { value: 1 }, 2); // MyStruct と 2 (i32) は比較不可
ex2.compare(); // ここでコンパイルエラーになる
}
struct MyStruct {
value: i32,
}
つまり、 PartialEq
が実装されたパラメータを渡された場合のみ compare
メソッドを使用できるようになります。
逆に言えば、MyStruct structがi32と比較可能であれば、このコンパイルエラーは解消されます。
すなわち、次のように MyStructに PartialEq<i32>
traitを実装することによって、MyStructとi32が比較可能になり、 compare
メソッドを呼び出すことができるようになります。
impl PartialEq<i32> for MyStruct {
fn eq(&self, other: &i32) -> bool {
self.value == *other
}
}
Self に対するトレイト境界
次のコードでは、Hoge structの compare
メソッドにwhere句が使用されています。
struct Hoge {
a: i32,
}
impl Hoge {
fn new(a: i32 ) -> Self {
Hoge { a }
}
fn compare<T>(&self, c: T) -> bool
where
Self: PartialEq<T>,
{
self.eq(&c)
}
}
このトレイト境界 は、2つの意味合いを持っています。
-
Hoge struct に対する制約
Hoge structがPartialEq<T>
を実装している必要があります。 -
引数に対する制約
compareメソッドに渡される型Tは、HogeがTに対してPartialEq
を実装している型である必要があります。
2つめの制約について具体例を示します。
例えばHoge structが以下の2つの型(i8, i32)の PartialEq
を実装していた場合
impl PartialEq<i8> for Hoge {
fn eq(&self, other: &i8) -> bool {
self.a == *other as i32
}
}
impl PartialEq<i32> for Hoge {
fn eq(&self, other: &i32) -> bool {
self.a == *other
}
}
使用する側では次のような挙動になります
fn main() {
let ex1 = Hoge::new(1); // 1, 2という相互に比較可能なパラメータを渡している. i32はPartialEqトレイトを実装している.
ex1.compare(1 as i8); // 正常に動作する
ex1.compare(1 as i32); // 正常に動作する
ex1.compare(1 as i16); // コンパイルエラー i16に対する PartialEq が実装されていない
}
このwhere句により、Hoge型のインスタンスが特定の型Tのインスタンスと比較可能であることを保証し、型安全性を維持できます。
さいごに
これ以外にも、ライフタイムパラメータを使ったりする場合や、関連型[3]による記述などいろいろなパターンがあります。
トレイト境界を使いこなすことで、Rustの強力な型推論のメリットを最大限享受しつつ安全に開発を進めることができます。
また、非常に汎用的なプログラムを記述することができますので、ぜひ参考にしてみてください。
Discussion