概要: 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が要求されていたらミュータブルとして再推論する。