[Rust]トレイトを理解する

Rust

トレイトを曖昧なまま使っていて、このままではいかんと、理解するためにこのページにまとめます。

参考ページ
Traits: Defining Shared Behavior - The Rust Programming Language

使用時の環境
WSL2 Ubuntu - 20.04
Rust 1.66.1

 Androidアプリを作成しました。
 感情用のメモ帳です。

スポンサーリンク
スポンサーリンク

トレイトとは

構造体として「Human」を定義し、「歩く」、「走る」、「食べる」、「寝る」などの行動をメソッドして実装したとします。

さらに構造体「Cat」を定義するとき、「歩く」、「走る」、「食べる」、「寝る」はHumanと同じくメソッドとして実装できます。

こうした共通した行動(共通した性質)をくくりだし、個別の型の枠組みを越えて利用できるのがトレイトです。

たとえば「Action」という名前のトレイトを作成し、歩く、走るなどを定義しておいて、HumanやCatがこのトレイトを実装すると、走ったり歩いたりする機能が使えるようになる、といったものです。

構文

トレイトの定義

キーワード「trait」を使います。

trait トレイト名 {
    fn メソッド名;
}

ソッドが名前だけの定義(戻り値があればそれも定義)であれば、このトレイトを実装する型が本体を定義する必要が出てきます。

メソッド本体を含めて定義すると、デフォルト実装となり、トレイトを実装する型はあらためて定義する必要はありません(上書き可能)。

前の項目で例として出した「Action」をトレイトとして実装します。

「cargo new animal」で新しいプロジェクトを作成。
「src/lib.rs」内に記述しました。

pub trait Action {
    fn body(&self) -> &str;

    fn walk(&self) {
        let moved = ".".repeat(10);
        println!("{}{}", moved, self.body());
    }
}

このActionトレイトを実装する型は、「body」メソッドを定義しなければいけません。

「walk」メソッドはデフォルト実装になります。

型への実装

キーワード「impl」と「for」を使います。

impl トレイト名 for 型名 {}

Actionトレイトを実装する「Human」を作成します。

pub struct Human {
    pub character: String,
}

impl Action for Human {
    fn body(&self) -> &str {
        &self.character
    }
}

Humanはフィールド「character」を持っています。

実装するのはbodyメソッド。walkはデフォルトのまま使用するため未実装です。

「main.rs」に実行文を書きます。

use animal::{Action, Human};

fn main() {
    let human = Human {
        character: "🙎".to_string(),
    };
    human.walk();
}

useキーワードで「Human」と合わせて「Action」を持ち込みます。
main内でhumanはActionトレイトのwalkを利用しているため、合わせて導入しないと動かなくなります。

実行結果

..........🙎

人間を歩かせることができました。

Catを追加してみましょう。

pub struct Cat {
    pub character: String,
}

impl Action for Cat {
    fn body(&self) -> &str {
        &self.character
    }
}
use animal::{Action, Cat, Human};

fn main() {
    let human = Human {
        character: "🙎".to_string(),
    };
    let cat = Cat {
        character: "🐱".to_string(),
    };
    human.walk();
    cat.walk();
}
..........🙎
..........🐱

実装例

下のコードはActionトレイトにrunメソッド、そしてBirdを追加したものです。

use std::io::{stdout, Write};
use std::{thread::sleep, time::Duration};

pub trait Action {
    fn body(&self) -> &str;

    fn character_move(&self, effect: &str, delay: u64) {
        for i in 0..10 {
            print!("\r");
            let moved = effect.repeat(i);
            print!("{}{}", moved, self.body());
            stdout().flush().unwrap();
            sleep(Duration::from_millis(delay));
        }
        println!();
    }

    fn walk(&self) {
        println!("Walk");
        self.character_move(".", 500);
    }

    fn run(&self) {
        println!("Run");
        self.character_move("-", 250);
    }
}

pub struct Human {
    pub character: String,
}

impl Action for Human {
    fn body(&self) -> &str {
        &self.character
    }
}

pub struct Cat {
    pub character: String,
}

impl Action for Cat {
    fn body(&self) -> &str {
        &self.character
    }
}

pub struct Bird {
    pub character: String,
}

impl Action for Bird {
    fn body(&self) -> &str {
        &self.character
    }
}

impl Bird {
    pub fn fly(&self) {
        println!("Fly");
        self.character_move("=", 100);
    }
}
use std::io::{stdout, Write};

stdoutはStdout構造体のハンドルを返すメソッドで、そのハンドルからflushメソッドを使いますが、Writeトレイトのパスを持ち込まないと動きません。

character_moveメソッド内では、thred::sleepとtime::Durationで一時停止させ、視覚効果を出すように。

またprint!マクロは改行文字が来るまで標準出力に表示されないため、flushメソッドで出力するようにしました。

impl Bird {
    pub fn fly(&self) {
        println!("Fly");
        self.character_move("=", 100);
    }
}

BirdにActionトレイトを実装後、Bird内部で、selfをつかって、Actionトレイトのcharacter_moveメソッドにパスが通ります。

use animal::{Action, Bird, Cat, Human};

fn main() {
    let human = Human {
        character: "🙎".to_string(),
    };
    let cat = Cat {
        character: "🐱".to_string(),
    };
    let bird = Bird {
        character: "🦉".to_string(),
    };
    human.walk();
    human.run();
    cat.walk();
    cat.run();
    bird.fly();
}

実行すると、

Walk
.........🙎
Run
---------🙎
Walk
.........🐱
Run
---------🐱
Fly
=========🦉

実際には使用するメソッドによって時間差で移動しています。

トレイトバウンド

引数がトレイトを実装している型に限定

pub trait Action {
    fn body(&self) -> &str;
}

pub struct Human {
    pub character: String,
}

impl Action for Human {
    fn body(&self) -> &str {
        &self.character
    }
}

pub struct Cat {
    pub character: String,
}

impl Action for Cat {
    fn body(&self) -> &str {
        &self.character
    }
}

学習のためトレイト内ではなく、外部に関数を作成しようと思います。

Actionを実装した型を受け取り、挨拶をする、次のような関数にしました。

pub fn greet<T: Action>(animal: &T) {
    println!("{}< HELLO!", animal.body());
}

<>と、そのカッコ内で大文字のアルファベット一文字(慣例に従い、ここではT)を使用。
そしてこのアルファベットに対し、トレイトを指定します。

この場合、引数はActionを備えた型に限定されます。

上の書き方のシンタックスシュガー。

pub fn greet(animal: &impl Action) {
    println!("{}< HELLO!", animal.body());
}

引数の()内、implキーワードの後でトレイトを指定します

引数が複数あった場合も同様に型を指定します。

pub fn greet_each_other<T: Action, U: Action>(animal1: &T, animal2: &U) {
    println!("{}< Hi! Hi!>{}", animal1.body(), animal2.body());
}

// 上の関数の別の書き方
// pub fn greet_each_other(animal1: &impl Action, animal2: &impl Action) {
//     println!("{}< Hi! Hi!>{}", animal1.body(), animal2.body());
// }

// 2つの引数の型が同じであることを強制したいなら
// pub fn greet_each_other<T: Action>(animal1: &T, animal2: &T) {
//     println!("{}< Hi! Hi!>{}", animal1.body(), animal2.body());
// }
// この場合、シンタックスシュガーでは書けない。
use animal::{greet, greet_each_other, Cat, Human};

fn main() {
    let human = Human {
        character: "🙎".to_string(),
    };
    let cat = Cat {
        character: "🐱".to_string(),
    };
    greet(&human);
    greet_each_other(&human, &cat);
}
🙎< HELLO!
🙎< Hi! Hi!>🐱

引数がトレイトを複数実装している型に限定

下準備としてHumanに新しいフィールド「age」を持たせました。

pub trait Action {
    fn body(&self) -> &str;
}

pub struct Human {
    character: String,
    age: u8,
}

impl Action for Human {
    fn body(&self) -> &str {
        &self.character
    }
}

impl Human {
    pub fn new(character: String, age: u8) -> Human {
        Human { character, age }
    }
}

また挨拶する関数を考えます。

こんどはお互いの年齢を比較し、それによってセリフを変える関数にしようと思っています。

ちょっと寄り道になりますが、標準ライブラリのトレイト「PartialOrd」をHumanに実装し、比較演算子「>, >=, <, <=」を使えるようにします。

PartialOrdを自分の型で使用するには、同じくトレイト「PartialEq」も合わせて実装する必要があります。

PartialOrd in std::cmp - Rust

PartialEq in std::cmp - Rust

それぞれの必須メソッド「partial_cmp」と「eq」を定義することで使えるようになります。

「partial_cmp」はOption<Ordering>を返すようにし、「eq」はselfと比較対象を==をつかってboolを返すようにします。

use std::cmp::Ordering;

// コードは続き
impl PartialOrd for Human {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        self.age.partial_cmp(&other.age)
        // if self.age > other.age {
        //     Some(Ordering::Greater)
        // } else if self.age < other.age {
        //     Some(Ordering::Less)
        // } else {
        //     Some(Ordering::Equal)
        // }
    }
}

impl PartialEq for Human {
    fn eq(&self, other: &Self) -> bool {
        self.age == other.age
    }
}

準備ができたので、ActionとPartialOrdを実装した型を受け取る関数を作成します。

pub fn greet_each_other<T: Action + PartialOrd>(human1: &T, human2: &T) {
    let greet1;
    let greet2;
    if human1 > human2 {
        greet1 = "おはよう";
        greet2 = "おはようございます";
    } else if human1 < human2 {
        greet1 = "おはようございます";
        greet2 = "おはよう";
    } else {
        greet1 = "おはよう";
        greet2 = "おはよう"
    }
    println!("{}<{} {}>{}", human1.body(), greet1, greet2, human2.body());
}

// 次のような書き方もできるが…
// pub fn greet_each_other(human1: &(impl Action + PartialOrd), human2: &(impl Action + PartialOrd)) {
// }
// もはやシンタックスシュガーとは言えず、同じ型であることを強制できない。

<>の中でプラス記号を使い、複数のトレイトを指定します。

たくさんのトレイトを指定すると可読性が悪くなってしまいます。

キーワード「where」を使うと、次のように書くこともできます。

pub fn greet_each_other<T>(human1: &T, human2: &T) // -> 戻り値の型
where
    T: Action + PartialOrd,
{}

実行します。

use animal::{greet_each_other, Human};

fn main() {
    let momo = Human::new("🙎".to_string(), 23);
    let steve = Human::new("👱".to_string(), 54);
    greet_each_other(&momo, &steve);
    greet_each_other(&steve, &momo);
}
🙎<おはようございます おはよう>👱
👱<おはよう おはようございます>🙎

この項目では、関数の引数にトレイトを限定する書き方でしたが、トレイトや構造体のフィールドも同じように制限することができます。

trait Printtwice<T: std::fmt::Display> {
    fn getter(&self) -> &T;

    fn printw(&self) {
        for _ in 0..2 {
            println!("{}", self.getter());
        }
    }
}

impl Printtwice<i32> for i32 {
    fn getter(&self) -> &i32 {
        self
    }
}

fn main() {
    3.printw();
}
// 出力
// 3
// 3
タイトルとURLをコピーしました