この資料について
2015/08/30 時点の Borrow と AsRef について調べたことをまとめたものです。Rust of Us - Chapter 3 で話したときに使いました。一部のコードは参照している文書から引用しています。
Borrow と AsRef は似ている
API Doc の Examples を見ても差分がよく分からない。
use std::borrow::Borrow;
fn check<T: Borrow<str>>(s: T) {
assert_eq!("Hello", s.borrow());
}
let s = "Hello".to_string();
check(s);
let s = "Hello";
check(s);
fn is_hello<T: AsRef<str>>(s: T) {
assert_eq!("hello", s.as_ref());
}
let s = "hello";
is_hello(s);
let s = "hello".to_string();
is_hello(s);
Borrow と AsRef は違うもの
実際同じことができる場面があるし「分かれている必要はないのでは」という Issue もあげられていた。しかし、これらの trait は異なる目的で作られたようだ。この Issue をきっかけにして、TRPL: Borrow and AsRef が追加された。
Borrow
や AsRef
は一体どこからやってきたのか。コミットログを追いかけた先にはそれぞれ別々の RFC があった。
Borrow はどこから
Borrow
は rust-lang/rfcs#235 collections-conventions で登場した。この RFC は std::collections
の設計を改善したり API を安定させるというモチベーションで作られた。ここで挙げられている当時の問題のひとつに、HashMap<String, ...>
オブジェクトに対してキーを渡したいとき、次のように書かなくてはならないというものがあった。
// map.find("literal") としたいところ
map.find(&"literal".to_string())
これは面倒なうえに無駄に heap を使う。当時はこの問題への workaround として、Equiv
という trait を使っていた。が、それも決して使いやすいものではなかった。find
に対して find_equiv
のようなほとんど重複したメソッドを用意しなければならなかったし、TreeMap
のような順序に依存するものには応用できない限定的なものだった。さらに、TreeMap
は別の方法でこの "Equiv" 問題を扱っており、全体として一貫性に欠けていた。
// "literal" で引くには後者を使う必要があった
fn find(&self, k: &K) -> Option<&V>
fn find_equiv<Q: Hash<S> + Equiv<K>>(&self, k: &Q) -> Option<&V>
どちらも文字列なのに同じように扱えない。String
と &str
の間で起きるようなこの "Equiv" 問題をよりうまく解決するために、この RFC では Borrrow
trait が提案された。Borrow
が導入された現在、上記2つのメソッドは廃止され次のメソッドに統一されている。
fn get<Q: ?Sized>(&self, k: &Q) -> Option<&V> where K: Borrow<Q>, Q: Hash + Eq
この変更によって &String
と &str
どちらを渡しても同じメソッドで同じ結果を得られるようになった。
use std::collections::HashMap;
fn main() {
let mut map = HashMap::new();
map.insert("alpha".to_string(), 1);
assert_eq!(map.get("alpha"), Some(&1));
assert_eq!(map.get(&"alpha".to_string()), Some(&1));
}
どちらの型も Borrow<str>
を実装している。あらゆる場面で両者を同じように扱えるようになった。
use std::borrow::Borrow;
fn assert_hello_message<T: Borrow<str> + ?Sized>(src: &T) {
let hello: &str = src.borrow();
assert_eq!("Hello", hello);
}
fn main() {
assert_hello_message("Hello");
assert_hello_message(&"Hello".to_string());
}
Borrow<T>
を実装する場合、Self
と T
の実装する Hash
Eq
Ord
はそれぞれ同じ結果になるよう実装するべきだ (Borrow
導入の目的を思えば納得がいく)。とはいえ Borrow
の API は比較結果の一貫性を保証するわけではない。
Blanket implementation として Borrow<T> for T
が用意されているため、ほとんどの場合は自分で Borrow
を実装する必要はない。
AsRef はどこから
一方の AsRef
は rust-lang/rfcs#529 conversion-traits:で登場した。これは、データを変換する trait をひとつに統一しようという趣旨の内容だ。ここでデータを変換するというのは、例えば &str
を &Path
にするようなことを指す。&str
を &Path
に変換できる trait があると次のようなケースで嬉しい。
// こうではなくて
let new_path = my_path.join(Path::new("fixed_subdir_name"));
// こう書けて嬉しい
let new_path = my_path.join("fixed_subdir_name");
これを実現するために当時は AsPath
という trait が使われていた。AsPath
を実装したオブジェクトの as_path()
を呼ぶと &Path
を得られる。Path
と &str
がこれを実装していれば、どちらも引数として使えて便利だ。他にも AsSlice
FromStr
など、同じ趣旨の様々な trait があった。
上記の trait は確かに便利だが、このやり方には2つの問題があった。第一に、このようなデータの変換が求められる度に新しい trait を定義しなければならないのはおかしいということ。第二に、例えばもし path のような API に AsPath
のような仕組みが含まれていなかったら、path を使う複数の crate が似たような (互換性のない) AsPath
trait を実装してしまう恐れもある。
この RFC ではそれら既存の trait に代わるものとしていくつかの generic な trait を提案している。AsRef
はそのうちのひとつだ。
現在では AsPath
は AsRef<Path>
で置き換えられている。
次のように、様々な型が AsRef<Path>
を実装している。
use std::path::Path;
fn print_dirname<T: AsRef<Path>>(src: T) {
let path: &Path = src.as_ref();
println!("{}", path.parent().unwrap().display());
}
fn main() {
let a_str = "/usr/src/linux";
let a_string = a_str.to_string();
let a_path = Path::new(a_str);
let a_path_buf = a_path.to_path_buf();
print_dirname(a_str);
print_dirname(a_string);
print_dirname(a_path);
print_dirname(a_path_buf);
print_dirname(&a_str);
print_dirname(&a_string);
print_dirname(&a_path);
print_dirname(&a_path_buf);
}
Blanket implementation AsRef<U> for &T
によって柔軟なインターフェースを定義できる。
上記の関数 print_dirname
は Path
と &Path
のどちらも受け取れる。
Borrow と AsRef の使い分け
Borrow<T>
を実装する場合は Self
と T
でハッシュ値や比較結果が同じであることが求められる。AsRef<T>
にはそのような制限は無い。
HashMap
や BTreeMap
のキーのようにハッシュ値や比較結果を気にするような場面では Borrow
を使い、fs::File::open
のように様々な型の引数を受け取れる柔軟なインターフェースを実現したい場合には AsRef
を使う。