🦀

Rust トレイト境界 (where句) の考え方と使い方

2023/12/15に公開

はじめに

この記事では、トレイト境界の概要と、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つの意味合いを持っています。

  1. Hoge struct に対する制約
    Hoge structが PartialEq<T> を実装している必要があります。

  2. 引数に対する制約
    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の強力な型推論のメリットを最大限享受しつつ安全に開発を進めることができます。
また、非常に汎用的なプログラムを記述することができますので、ぜひ参考にしてみてください。

脚注
  1. https://doc.rust-jp.rs/rust-by-example-ja/generics/bounds.html ↩︎

  2. https://github.com/rust-lang-ja/the-rust-programming-language-ja/issues/153 ↩︎

  3. https://doc.rust-jp.rs/rust-by-example-ja/generics/assoc_items/types.html ↩︎

Discussion