開発

[binllion] トレイト境界を理解する

前回にて、型の制約で引数の制限をかけていましたが、それだけでは予期せぬ挙動の可能性がありました。

もう少し制約をかけると良さそうです。

トレイト境界という仕組みを使うと機能としての制約をつけ、挙動を想定することができます。

トレイト境界とは

以前説明したとおり、トレイトとは型に依存しない、機能が集まったもの、でした。

機能とは、例えばイテレーションやfrom()などの型変換の機能をよく目にすると思います。

引数に機能としての制約をつけるとは、例えばFromトレイトを実装したものに限定するということになります。

このようにすることでトレイトに実装される関数やメソッドが特定され、その機能を使って処理を行うことができます。

独自の型とトレイトを定義してみる

では、16進数とASCIIの変換処理を型とトレイトを使って書いてみましょう。

型名はわかりやすいようにToHex、ToAsciiとしました。

トレイトは、このようにtraitの中に関数を定義します。変換の機能としてconvert()関数を定義しました。

実装はimplの中で行います。

implの内容は前回までの変換処理を実装しています。

// convert_to_ascii

struct ToHex;
struct ToAscii;

trait ConvertTrait {
    fn convert(buf: &[u8]) -> String;
}

impl ConvertTrait for ToHex {
    fn convert(buf: &[u8]) -> String {
        let sep = String::from(" ");
        let hex = buf
            .iter()
            .map(|x| format!("{:02X}", x))
            .collect::<Vec<_>>()
            .join(&sep);
        hex
    }
}

impl ConvertTrait for ToAscii {
    fn convert(buf: &[u8]) -> String {
        let res: String = buf
            .iter()
            .map(|&x| format!("{}", ToPrintableAscii::as_printable_char(x)))
            .collect();
        res
    }
}Code language: JavaScript (javascript)

意外とシンプルに書けました。

呼び出す側の変更

続いて、呼び出し側のto_lines()です。

トレイト境界を使うように書き直しました。

// convert_to_ascii

// 自作トレイトでトレイト境界を使う 関数バージョン
pub(crate) fn to_lines6<F: ConvertTrait>(buf: &[u8], len: usize) -> Vec<String> {
    let mut vec = Vec::new();
    buf.chunks(len)
        .for_each(|x| vec.push(format!("{:width$} {}", " ", F::convert(x), width = 8)));
    vec
}Code language: JavaScript (javascript)

fn to_lines6<F: ConvertTrait>の部分とF::convert(x)の部分が新しい箇所です。これがトレイト境界です。

この<>で表されているものは型パラメータで、FはConvertTraitを実装している型、ということを表しています。

Fのトレイト、つまりConvertTraitは、convert()の関数を持つことを期待されているのでF::convert(x)でToHex、ToAsciiそれぞれのconvert()関数が呼び出されます。

// convert_to_ascii

    #[test]
    fn test_to_lines_with_ascii_and_hex_3() {
        let bin_data = setup();
        let expect = setup_to_ascii();
        let res = to_lines6::<ToAscii>(bin_data, 8);
        dbg!(&res);
        println!("{}: {:?}", "to_ascii_9", &res);
        assert_eq!(res, expect);

        let expect = setup_to_lines_hex();
        let res = to_lines6::<ToHex>(bin_data, 8);
        dbg!(&res);
        println!("{}: {:?}", "to_hex", &res);
        assert_eq!(res, expect);
    }Code language: PHP (php)

今のところto_lines6()という名前にしていますが、これをconvertとすれば、convert::<ToAscii>()のように書けて保守性の高いコードになりそうです。

関数を呼び出すパターンの他にメソッドを呼ぶパターンも可能です。

定義としてはこのようになります。

// convert_to_ascii

// メソッドバージョン
trait ConvertTraitMethod {
    fn convert(&self, buf: &[u8]) -> String;
}

impl ConvertTraitMethod for ToHex {
    fn convert(&self, buf: &[u8]) -> String {
        let sep = String::from(" ");
        let hex = buf
            .iter()
            .map(|x| format!("{:02X}", x))
            .collect::<Vec<_>>()
            .join(&sep);
        hex
    }
}

impl ConvertTraitMethod for ToAscii {
    fn convert(&self, buf: &[u8]) -> String {
        let res: String = buf
            .iter()
            .map(|&x| format!("{}", ToPrintableAscii::as_printable_char(x)))
            .collect();
        res
    }
}Code language: PHP (php)

呼び出し側のto_lines()はこのようになります。

まず、引数として型やインスタンスを渡す必要があります。

convertの呼び出し方がfunc.convert()とメソッド表記になります。

// convert_to_ascii

// 自作トレイトででトレイト境界を使う メソッドバージョン
pub(crate) fn to_lines7<F: ConvertTraitMethod>(buf: &[u8], len: usize, func: F) -> Vec<String> {
    let mut vec = Vec::new();
    buf.chunks(len)
        .for_each(|x| vec.push(format!("{:width$} {}", " ", func.convert(x), width = 8)));
    vec
}Code language: JavaScript (javascript)
// convert_to_ascii

    #[test]
    fn test_to_lines_with_ascii_and_hex_4() {
        let bin_data = setup();
        let expect = setup_to_ascii();
        let res = to_lines7(bin_data, 8, ToAscii);
        dbg!(&res);
        println!("{}: {:?}", "to_ascii_9", &res);
        assert_eq!(res, expect);

        let expect = setup_to_lines_hex();
        let res = to_lines7(bin_data, 8, ToHex);
        dbg!(&res);
        println!("{}: {:?}", "to_hex", &res);
        assert_eq!(res, expect);
    }Code language: PHP (php)

どちらのパターンでも実装可能です。両方とも実装することも可能です。1

余談:関数を引き渡すパターン

前回、型制約で関数を引き渡すパターンを書きましたが、これのトレイト境界バージョンも書けます。

Rustには型としてfnがありますが、トレイトとしてFnもあります。そのため、同じようなことが可能です。

// convert_to_ascii

// トレイト境界を使う
pub(crate) fn to_lines3<F: Fn(&[u8]) -> String>(buf: &[u8], len: usize, func: F) -> Vec<String> {
    let mut vec = Vec::new();
    buf.chunks(len)
        .for_each(|x| vec.push(format!("{:width$} {}", " ", func(x), width = 8)));
    vec
}

// 別のやり方でトレイト境界を使う
pub(crate) fn to_lines4(buf: &[u8], len: usize, func: impl Fn(&[u8]) -> String) -> Vec<String> {
    let mut vec = Vec::new();
    buf.chunks(len)
        .for_each(|x| vec.push(format!("{:width$} {}", " ", func(x), width = 8)));
    vec
}Code language: JavaScript (javascript)
// convert_to_ascii

let res = to_lines3(bin_data, 8, to_ascii_9);
let res = to_lines4(bin_data, 8, to_hex);Code language: JavaScript (javascript)

トレイト境界という言葉

トレイト境界という言葉を初めて目にしたとき、さっぱりわかりませんでした。

何かを分け隔てるものだろうと思いましたが、トレイトという機能を深く知ることでわかるようになりました。

実際面としては、この関数やこのメソッドを使いたい!だからこのトレイトを要求するように条件をつけたい。トレイト境界とはそのようなものと理解できるかもしれません。

翻訳の問題なのかと思いましたがこちらの議論を見てみると、原文を尊重した言葉となっているようです。

「トレイト境界」という訳語について公式コミュニティでの見解 · Issue #172 · rust-lang-ja/book-ja

初めまして、 #20 / #153@rust-lang-ja/the-rust-programming-language-ja で過去に議論されていた件ですが、私も気になって公式コミュニティで質問してみました。 https://users.rust-lang.org/t/what-is-trait-bounds-exact-meaning/67695 詳細はリンク先をご覧いただければと思いま…


  1. Rustは素直に書くとオーバーロードできないのですが、トレイトを分けると同じ名前を使うことができました。トレイトで名前空間が違うのでしょうか。オーバーロードのテクニックとして一般的なようです。トレイトで拡張しやすいように設計されているようです。 ↩︎

GitHubにコードをアップロードしています。0.1.0バージョン

コードのコメントに書かれているfirst_stepなどをcargoコマンドに渡すと実行できます。

# Example
$ cargo run --example first_stepCode language: Bash (bash)

管理人

Recent Posts

情報セキュリティマネジメント試験取得への道

スキルアップを図るべく情報セキ…

2か月 ago

ファイナンシャルプランナー3級試験取得への道

スキルアップを図るべくファイナ…

2か月 ago

[rust] New Type Patternを使ってみる

DDDの考えを取り入れることで…

5か月 ago

RustでDDDの要素を取り入れてみる

前回SOLID原則というものを…

5か月 ago

期間限定!書籍無料キャンペーン2025

「mdBookではじめるKin…

5か月 ago