ユーニックス総合研究所

  • home
  • archives
  • rust-trait

Rustのtrait(トレイト)の使い方を解説します【impl, トレイト境界】

  • 作成日: 2022-11-28
  • 更新日: 2023-12-24
  • カテゴリ: Rust

Rustのtrait(トレイト)を使う

Rustのtrait(トレイト)とは他の言語をやって来た方に言うとインターフェースと似たようなものです。
トレイトは構造体に共通のメソッドを定義したい時に使われます。

たとえばCatDogという構造体があってそれぞれ名前を持っているとします。
そうすると名前を取得する共通の関数が欲しくなりますよね。

そういう時はトレイトを定義して、そのトレイトをCatDogについて実装します。
こうするとCatDogという異なる2つの構造体に共通のメソッドを定義することができます。

関数の引数でトレイトを受け取るようにすればCatDogを意識せずに関数内で名前を取得できます。
このようにトレイトは高度なプログラミングでは必須と言っていい概念です。

では具体的に解説していきます。

関連記事
RustでJSONの読み書きを行う
RustでTCPクライアント/サーバーを作る【ソケット通信】
Rustでファイルサイズを取得する方法【std::fs::metadata】
RustのBoxの使い方【ヒープにメモリを確保!初心者向け】
RustのResultでエラーハンドリング処理を行う
RustのThread(スレッド)で並行処理をする方法
RustのVecの使い方【vec!, push, pop】
Rustのassert!の使い方【assert!, assert_eq!, assert_ne!】

普通のトレイトの使い方

トレイトをまずは定義してみましょう。
Rustのトレイトはtraitキーワードを使って定義します。

trait Animal {  
    // 未実装のメソッド  
    fn get_name(&self) -> &String;  

    // デフォルトメソッド  
    fn get_age(&self) -> usize {  
        return 0;  
    }  
}  

このトレイトAnimalget_name()メソッドの未定義の実装と、get_age()メソッドの定義済みの実装が書かれています。
このトレイトを使って構造体のメソッドを実装します。

まずCatという構造体を作っておきます。

struct Cat {  
    name: String,  
    age: usize,  
}  

このCatnameageをフィールドに持っているなんてことはない構造体です。
このCatについて先ほどのAnimalトレイトを実装すると、

impl Animal for Cat {  
    fn get_name(&self) -> &String {  
        return &self.name;  
    }  
}  

↑こういう感じになります。
構造体のメソッドを定義するときは構造体に対して「impl Cat」とやっていました。
構造体についてのトレイトを実装するときは↑のように「impl Animal for Cat」と書きます。
つまり

impl トレイト for 構造体 {  
    メソッド  
    ...  
}  

という感じで定義します。
「構造体についてのトレイトを実装する」と覚えておけば「impl Animal for Cat」はすぐに思い出せると思います。
この実装を行った構造体にはメソッドget_name()が定義されている状態です。
ですので↓のように

    let cat = Cat {  
        name: String::from("Tama"),  
        age: 20,  
    };  
    println!("{}", cat.get_name());  // Tama  

メソッドを呼び出すことができます。
これがトレイトの実装方法ですが果たしてこれが何の役に立つのでしょうか?
これは順を追って見てみたいと思います。

複数の構造体で同一のトレイトを使う

トレイトは複数の構造体に実装することができます。
先ほどのCat構造体の他にDog構造体も作ります。

struct Dog {  
    name: String,  
    age: usize,  
}  

そしてこのDog構造体にもAnimalトレイトを実装します。

impl Animal for Dog {  
    fn get_name(&self) -> &String {  
        return &self.name;  
    }  
}  

そうするとDog構造体にもget_name()メソッドが定義されました。
ですので↓のコードが合法になります。

    let cat = Cat { name: String::from("Tama"), age: 20 };  
    let dog = Dog { name: String::from("Pochi"), age: 30 };  
    println!("{}", cat.get_name());  // Tama  
    println!("{}", dog.get_name());  // Pochi  

🐭 < はやく便利なところを解説してよ

では次からトレイトの便利なところを見ていきましょう。

複数の構造体を同一の関数で処理する

↓のような関数を定義します。

fn show_animal_name(animal: &impl Animal) {  
    println!("{}", animal.get_name());  
}  

このshow_animal_name()関数は引数に&impl Animal型を取ります。
このように引数を書くと構造体に実装されているトレイトを受け取ることができます。
トレイトにはget_name()メソッドが存在しますので、animal.get_name()のようにメソッドを呼び出すことができます。

ここら辺はインターフェースと同じ振る舞いですね。
他言語を知ってる人からすると「なんだインターフェースじゃん」という感じだと思います。

このshow_animal_name()関数を使うと↓のようにコードを書くことができます。

    let cat = Cat { name: String::from("Tama"), age: 20 };  
    let dog = Dog { name: String::from("Pochi"), age: 30 };  
    show_animal_name(&cat);  // Tama  
    show_animal_name(&dog);  // Pochi  

CatDogも同じ関数に渡せて名前を表示できるという中々便利そうな具合になっています。
じっさいこれは便利でこのような振る舞いをする機能は他の言語ではインターフェースとして知られています。

関数とトレイト境界構文

Rustにはトレイト境界構文なるものがあります。
これを使うと先ほどの関数の定義は↓のように書くことができます。

fn show_animal_name_2<T: Animal>(animal: &T) {  
    println!("{}", animal.get_name());  
}  

実現できる処理はshow_animal_name()関数と変わりません。
しかし記法がちょっと変わっていますよね。

このshow_animal_name_2()関数も↓のようなコードで動作可能です。

    let cat = Cat {  
        name: String::from("Tama's Blog"),  
        age: 20,  
    };  
    let dog = Dog {  
        name: String::from("Pochi's Company"),  
        age: 30,  
    };  
    show_animal_name_2(&cat);  // Tama's Blog  
    show_animal_name_2(&dog);  // Pochi's Company  

関数で複数のトレイトを引数に取る

構造体に複数のトレイトが実装されている場合もあります。
たとえばHumanトレイトがあって、

trait Human {  
    fn talk(&self);  
}  

このトレイトについてCatDogを実装すると、

impl Human for Cat {  
    fn talk(&self) {  
        println!("Cat age is {}", self.age);  
    }  
}  

impl Human for Dog {  
    fn talk(&self) {  
        println!("Dog age is {}", self.age);  
    }  
}  

↑こんな感じになりますよね。
そうするとCatDogにはAnimalget_name()Humantalk()が実装されていることになります。
異なるトレイトが2つ実装されています。

関数で複数のトレイトを同時に同じ引数に受け取りたい場合は↓のように関数を書きます。

fn show_animal_info<T: Animal + Human>(animal: &T) {  
    println!("{}", animal.get_name());  
    animal.talk();  
}  

こうすると引数animalからget_name()talk()の両方を呼び出すことができるようになります。
T: Animal + Human」という書き方がミソですね。

    let cat = Cat {  
        name: String::from("Tama"),  
        age: 20,  
    };  
    let dog = Dog {  
        name: String::from("Pochi"),  
        age: 30,  
    };  
    show_animal_info(&cat);  
    // Tama  
    // Cat age is 20  
    show_animal_info(&dog);  
    // Pochi  
    // Dog age is 30  

where句を使ってトレイトを引数に取る

where句の説明のためにもう1つ構造体とトレイトを作っておきます。

trait Food {  
    fn get_name(&self) -> &String;  
}  

struct CatFood {  
    name: String,  
}  

impl Food for CatFood {  
    fn get_name(&self) -> &String {  
        return &self.name;  
    }  
}  

キャットフードですね。
で、where句というものを使うとですね、先ほどのような関数は↓のようにも書くことができます。

fn show_animal_data<T, U>(animal: &T, food: &U)  
    where T: Animal + Human,  
          U: Food {  
    println!("{}", animal.get_name());  
    animal.talk();  
    println!("{}", food.get_name());  
}  

T」には「Animal + Human」が、「U」には「Food」が設定されます。
これをwhere句を使わないで書くと非常に横に伸びた書き方になってしまいますが、where句を使えば↑のように読みやすく書けるというわけですね。

このshow_animal_data()関数を使うと↓のようなコードになります。

    let cat = Cat {  
        name: String::from("Tama"),  
        age: 20,  
    };  
    let dog = Dog {  
        name: String::from("Pochi"),  
        age: 30,  
    };  
    let cat_food = CatFood {  
        name: String::from("Yamitsuki Cat"),  
    };  
    show_animal_data(&cat, &cat_food);  
    // Tama  
    // Cat age is 20  
    // Yamitsuki Cat  
    show_animal_data(&dog, &cat_food);  
    // Pochi  
    // Dog age is 30  
    // Yamitsuki Cat  

関数からトレイトを返す

関数の返り値(戻り値)としてトレイトを返すこともできます。

fn get_cats_animal(cat: &Cat) -> &impl Animal {  
    return cat;  
}  
    let cat = Cat {  
        name: String::from("Tama"),  
        age: 20,  
    };  
    let animal = get_cats_animal(&cat);  
    println!("{}", animal.get_name());  // Tama  

トレイト境界でメソッドの実装を条件分けする

ちょっと難しい話になりますが、トレイト境界を使うとメソッドを実装するかしないかの分岐を書くことができます。
たとえば↓のような感じです。

struct Point<T> {  
    x: T,  
    y: T,  
}  

impl<T> Point<T> {  
    fn new(x: T, y: T) -> Self {  
        Self {  
            x,  
            y,  
        }  
    }  
}  

impl<T: std::fmt::Display> Point<T> {  
    fn show_data(&self) {  
        println!("{} {}", self.x, self.y);  
    }  
}  

↓のように書くとstd::fmt::Displayが実装されているPointにだけshow_data()メソッドが実装されます。

impl<T: std::fmt::Display> Point<T> {  
    fn show_data(&self) {  
        println!("{} {}", self.x, self.y);  
    }  
}  
    let point = Point::new(1, 2);  
    point.show_data();  

省エネって感じですね。
無駄な実装はしなくて済むという低コストなRustの思想が現れていると思います。

おわりに

今回はRustのtrait(トレイト)を使う方法について解説しました。
トレイトは非常に便利な機能でこれを知っているといないとではプログラムの質も変わってくるかと思います。
ぜひマスターしておきたいところです。

🦝 < 僕の名前はトレイト

🐭 < いろんな構造に実装されるのさ