Rustにおける左辺値選好と可変性調停
概要: Rustのミュータビリティー推論に使われる左辺値選好と可変性調停について説明する。
左辺値選好
Rustの型推論では、期待型のほかに、左辺値選好 (lvalue preference) という状態もトップダウンに渡される。
LvaluePreference
は以下のどちらかの値をとる。
PreferMutLvalue
… 可変な左辺値を優先する。NoPreference
… 選好はない。不変な左辺値や、右辺値などでよい。
左辺値選好はほぼ構文的に決定される。具体的には以下のように決定・伝搬される。
&mut
の内側の式はPreferMutLvalue
- 代入の左辺は
PreferMutLvalue
- メソッド呼び出しとフィールド参照のレシーバーは左辺値選好を継承する。
x[i]
の左辺は左辺値選好を継承する。- 参照外し演算子は左辺値選好を継承する。
左辺値選好は次の用途に用いられる。
x[i]
は原則としてIndex
に脱糖されるが、PreferMutLvalue
ならば先にIndexMut
を試す。*x
は原則としてDeref
に脱糖されるが、PreferMutLvalue
ならば先にDerefMut
を試す。- メソッド以外の自動参照外しに対しても同じ処理が行われる。
- メソッドのレシーバの自動参照外しは
NoPreference
が仮定される。 (後述)
左辺値選好が誤推論するケース
さて、この左辺値選好は多くの場合に正しく可変性を推論するが、誤った推論をするケースも考えられる。以下がこの左辺値選好の仮定である。
x[i]
の左辺は可変性を継承する→正しい。 (Index
/IndexMut
の型がそうなっているので)*x
は可変性を継承する→正しい。 (Deref
/DerefMut
の型がそうなっている。また、&T
/&mut T
/Box<T>
の参照外しの動作も同様である。)- フィールド参照は可変性を継承する→正しい。
- メソッド呼び出しは可変性を継承する→正しくない。
&mut self
を受け取り&T
を返したり、&self
を受け取り&mut T
を返したりする可能性がある。
これにより、メソッド呼び出しのレシーバの位置に Deref
や Index
が来ると、可変性の推論で誤推論を起こす可能性がある。これを実験すると以下のようになる。
use std::fmt::Debug; use std::ops::{Deref, DerefMut}; // deref/deref_mut 時にメッセージを出すラッパー #[derive(Copy, Clone, Debug)] pub struct Wrap<X: Debug + ?Sized>(X); impl<X: Debug + ?Sized> Deref for Wrap<X> { type Target = X; fn deref(&self) -> &Self::Target { println!("deref({:?})", self); &self.0 } } impl<X: Debug + ?Sized> DerefMut for Wrap<X> { fn deref_mut(&mut self) -> &mut Self::Target { println!("deref_mut({:?})", self); &mut self.0 } } // 左辺値選好による推論が失敗する例 #[derive(Copy, Clone, Debug)] pub struct A; impl A { pub fn f<'a, 'b>(&'a self, p: &'b mut i32) -> &'b mut i32 { p } pub fn g<'a, 'b>(&'a mut self, p: &'b i32) -> &'b i32 { p } } fn main() { let mut x = Wrap(A); let mut y : i32 = 0; // レシーバが&Tでかまわないが、構造上&mut Tが推論される例 &mut *(*x).f(&mut y); // deref_mut(Wrap(A)); &mut *x.f(&mut y); // deref(Wrap(A)); // レシーバが&mut Tを必要とするが、構造上&Tが推論される例 &*x.g(&y); // deref_mut(Wrap(A)); }
これを見ると、一番最初の例だけ、 Deref::deref
でよいはずの位置で DerefMut::deref_mut
が呼ばれているが、他の例では問題なく動作している。
これは次のような理由による。
- この例には書いていないが、
&T
で十分だが&mut T
が推論され、しかし実際にはDerefMut
が実装されていない場合には、Deref
にフォールバックされるため問題ない。 - メソッドのレシーバの自動参照外しでははじめ
DerefMut
ではなくDeref
が仮定される。 - メソッド解決後に発生する可変性調停のため、推論と異なり
&mut T
が必要な場合は、そのように修正される。
可変性調停
可変性調停 (reconciliation) はメソッドの解決後に行われる処理で、レシーバが &mut self
だった場合に Deref
/Index
を再解決する処理である。
これは次のような手順を踏む。
- レシーバを指定している式を調べ、影響を受ける
Deref
/Index
呼び出しを列挙する。 (例えばx[i][j].some_method()
でsome_method
のレシーバが&mut self
の場合、x[i]
とx[i][j]
を補正する必要がある。 - それぞれの
Deref
/Index
をPreferMutLvalue
で再推論する。これによりこの部分がDerefMut
/IndexMut
に昇格する。
これにより、&mut T
が要求される部分で &T
を誤推論したことにより発生しえたエラーを回避している。
まとめ
*x
やx[i]
などではイミュータブルなDeref
/Index
とミュータブルなDerefMut
/IndexMut
のいずれかが選択される。- これらは色々な仕組みによりだいたいいい感じに推論される。それには以下のような動作が関わってくる。
- 構文的に決まる「左辺値選好」をトップダウンに伝搬する。
DerefMut
が駄目ならDeref
にフォールバックする。- メソッド解決でははじめイミュータブルを仮定し、
&mut self
が要求されていたらミュータブルとして再推論する。