疑問
rustcのソースコードを読んでいたら、初見殺しテクニカルだと思う書き方に遭遇しました。例えばこんな感じ。
impl<'a, 'tcx> FnCtxt<'a, 'tcx> {
...
pub fn local_ty(&self, span: Span, nid: hir::HirId) -> LocalTy<'tcx> {
self.locals.borrow().get(&nid).cloned().unwrap_or_else(|| {
span_bug!(span, "no type for local variable {}", self.tcx.hir().node_to_string(nid))
})
}
...
}
self
のメンバであるlocals
にアクセスして、そこからメソッドをつないでいっている普通の書き方に見えますね。でも、ここでself
の型となっている構造体FnCtxt
にはlocals
なんてメンバは存在しないのです。
周辺のソースを探っていると、このlocals
はInherited
という構造体のメンバのようでした。
pub struct Inherited<'a, 'tcx> {
...
pub(super) locals: RefCell<HirIdMap<super::LocalTy<'tcx>>>,
...
}
それを踏まえて構造体FnCtxt
のメンバを見てみると、FnCtxt
はInherited
の参照をメンバとして持っていることが分かります。
pub struct FnCtxt<'a, 'tcx> {
...
pub(super) inh: &'a Inherited<'a, 'tcx>,
}
とはいえ、普通に考えたらFnCtxt
からInherited
のメンバにアクセスするには、self.inh.locals
のようにinh
をかませる必要がある気がします。どうなっているのでしょうか?
回答
実は、FnCtxt
に実装されているトレイトDeref
に秘密があります。
impl<'a, 'tcx> Deref for FnCtxt<'a, 'tcx> {
type Target = Inherited<'a, 'tcx>;
fn deref(&self) -> &Self::Target {
&self.inh
}
}
FnCtxt
の参照を外すと、inh
が返されるように実装されていました。この実装によりinh
の記述は省略され、FnCtxt
から直接Inherited
のメンバにアクセスできるようになっています。
これは見方を変えれば、FnCtxt
がInherited
を継承していると言うこともできるでしょう。(型の名前からして、そのような実装を意図していそうな感じです。)このやり方を使うことで、データ構造の継承機能を持たないRustに、構造体の継承のようなものを適用することができます。
注意
ただし、上記のような実装は推奨されていないようです。実際、Deref
のリファレンスにもDeref
はスマートポインタに対してのみ実装するべきとの記述があります。rustcで使っておいてひどい。
Because of this, Deref should only be implemented for smart pointers to avoid confusion.
ここでは、Deref
はもともとスマートポインタ用に設計されたので、という文脈ではありますが、そうでなくとも今回紹介した書き方は読みづらいかなと思います。
余談
ここまでの説明を読んで、「Deref
でinh
を返すことは分かったけど、参照外しなんてしてなくない?」という疑問を持った方がいるかもしれません。
これにはRustの型強制(Type Coercion)の仕組みが効いています。Rustでは、ある型のメンバやメソッドにアクセスする際、指定された型そのものに実装がない場合には参照を外した型でそれらが見つかるかを調べます。
つまり、冒頭の例は以下のような流れでメンバを探していくことになります。
self.locals... // selfにlocalsは見つからない
(*self).locals... // 参照を外して見つかるか調べる
(self.deref()).locals... // 参照外しはderefに置き換えられる
(self.inh).locals... // inhの中にlocalsが見つかる
違和感を感じるかもしれませんが、このような動作はおそらく普段も目にしていると思います。例えば、絶対値を求める単純なコードを考えてみます。
let a = -3_i32;
let b = &a;
assert_eq!(b.abs(), 3);
b
の型は明らかに&i32
です。にもかかわらずi32
のメソッドであるabs()
を呼び出すことができているのは、自動的に参照を外してメソッドを探しているためで、結果として以下のコードと同じことになります。
let a = -3_i32;
let b = &a;
assert_eq!((*b).abs(), 3);
このような型強制はRustを書きやすくしている一方で、深く考え始めると少しややこしいかもしれませんね。冒頭の例はこの機能をひねった使い方と言えそうです。