前回にて、型の制約で引数の制限をかけていましたが、それだけでは予期せぬ挙動の可能性がありました。
もう少し制約をかけると良さそうです。
トレイト境界という仕組みを使うと機能としての制約をつけ、挙動を想定することができます。
トレイト境界とは
以前説明したとおり、トレイトとは型に依存しない、機能が集まったもの、でした。
機能とは、例えばイテレーションや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
}
}
意外とシンプルに書けました。
呼び出す側の変更
続いて、呼び出し側の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
}
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);
}
今のところ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
}
}
呼び出し側の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
}
// 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);
}
どちらのパターンでも実装可能です。両方とも実装することも可能です。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
}
// convert_to_ascii
let res = to_lines3(bin_data, 8, to_ascii_9);
let res = to_lines4(bin_data, 8, to_hex);
トレイト境界という言葉
トレイト境界という言葉を初めて目にしたとき、さっぱりわかりませんでした。
何かを分け隔てるものだろうと思いましたが、トレイトという機能を深く知ることでわかるようになりました。
実際面としては、この関数やこのメソッドを使いたい!だからこのトレイトを要求するように条件をつけたい。トレイト境界とはそのようなものと理解できるかもしれません。
翻訳の問題なのかと思いましたがこちらの議論を見てみると、原文を尊重した言葉となっているようです。
「トレイト境界」という訳語について公式コミュニティでの見解 · 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 詳細はリンク先をご覧いただければと思いま…
- Rustは素直に書くとオーバーロードできないのですが、トレイトを分けると同じ名前を使うことができました。トレイトで名前空間が違うのでしょうか。オーバーロードのテクニックとして一般的なようです。トレイトで拡張しやすいように設計されているようです。 ↩︎
TOC
GitHubにコードをアップロードしています。
コードのコメントに書かれているfirst_stepなどをcargoコマンドに渡すと実行できます。
# Example
$ cargo run --examples first_step