Edited at

Borrow and AsRef

More than 3 years have passed since last update.


この資料について

2015/08/30 時点の Borrow と AsRef について調べたことをまとめたものです。Rust of Us - Chapter 3 で話したときに使いました。一部のコードは参照している文書から引用しています。


Borrow と AsRef は似ている

API Doc の Examples を見ても差分がよく分からない。

Borrow の 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);

AsRef の Examples:

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 が追加された。

BorrowAsRef は一体どこからやってきたのか。コミットログを追いかけた先にはそれぞれ別々の RFC があった。


Borrow はどこから

Borrowrust-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> を実装する場合、SelfT の実装する Hash Eq Ord はそれぞれ同じ結果になるよう実装するべきだ (Borrow 導入の目的を思えば納得がいく)。とはいえ Borrow の API は比較結果の一貫性を保証するわけではない。

Blanket implementation として Borrow<T> for T が用意されているため、ほとんどの場合は自分で Borrow を実装する必要はない。


AsRef はどこから

一方の AsRefrust-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 はそのうちのひとつだ。

現在では AsPathAsRef<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_dirnamePath&Path のどちらも受け取れる。


Borrow と AsRef の使い分け

Borrow<T> を実装する場合は SelfT でハッシュ値や比較結果が同じであることが求められる。AsRef<T> にはそのような制限は無い。

HashMapBTreeMap のキーのようにハッシュ値や比較結果を気にするような場面では Borrow を使い、fs::File::open のように様々な型の引数を受け取れる柔軟なインターフェースを実現したい場合には AsRef を使う。