2017-04-04 26 views
10
fn works<'a>(foo: &Option<&'a mut String>, s: &'a mut String) {} 
fn error<'a>(foo: &RefCell<Option<&'a mut String>>, s: &'a mut String) {} 

let mut s = "hi".to_string(); 

let foo = None; 
works(&foo, &mut s); 

// with this, it errors 
// let bar = RefCell::new(None); 
// error(&bar, &mut s); 

s.len(); 

如果我把兩行與評論,會出現以下錯誤:由於編譯器關於RefCell的特殊知識,是否出現此錯誤?

error[E0502]: cannot borrow `s` as immutable because it is also borrowed as mutable 
    --> <anon>:16:5 
    | 
14 |  error(&bar, &mut s); 
    |      - mutable borrow occurs here 
15 |  
16 |  s.len(); 
    | ^immutable borrow occurs here 
17 | } 
    | - mutable borrow ends here 

works()errors()的簽名看起來非常相似。但顯然編譯器知道你可以用RefCell作弊,因爲借用檢查器的行爲不同。

我甚至可以用我自己的另一種類型「隱藏」RefCell,但編譯器仍然總是做正確的事情(在可以使用RefCell的情況下會出錯)。編譯器如何知道所有這些東西,它是如何工作的?編譯器將類型標記爲「內部可變性容器」還是類似的東西?

回答

8

RefCell<T>包含UnsafeCell<T>這是一個特殊的lang item。導致錯誤的是UnsafeCell。你可以用檢查:

fn error<'a>(foo: &UnsafeCell<Option<&'a mut String>>, s: &'a mut String) {} 

... 

let bar = UnsafeCell::new(None); 
error(&bar, &mut s); 

但誤差不會由於編譯器識別的UnsafeCell引入內部可變性,但是,一個UnsafeCellinvariant在T.其實,我們可以使用PhantomData重現錯誤:

struct Contravariant<T>(PhantomData<fn(T)>); 

fn error<'a>(foo: Contravariant<&'a i32>, s: &'a mut String) {} 

... 

let bar = Contravariant(PhantomData); 
error(bar, &mut s); 

甚至只是東西都在一生'a逆變或不變:

fn error<'a>(foo: Option<fn(&'a i32)>, s: &'a mut String) {} 

let bar = None; 
error(bar, &mut s); 

您無法隱藏RefCell的原因是因爲方差是通過結構的字段派生的。一旦你在某處使用了RefCell<T>,無論多深,編譯器都會計算出T是不變的。


現在讓我們看看編譯器如何確定E0502錯誤。首先,重要的是要記住,編譯器必須在這裏選擇兩個特定的生命週期:表達式&mut s'a)類型中的生命週期以及bar(我們稱之爲'x)類型的生命週期。兩者都是受限制的:前者的使用壽命'a必須小於s的範圍,否則我們最終會得到比原始字符串壽命更長的參考。 'x必須大於bar的範圍,否則我們可以通過bar訪問懸掛指針(如果某個類型具有生存期參數,則編譯器假定該類型可以訪問具有該生存期的值)。

有了這兩個基本的限制,編譯器經過以下步驟:

  1. 類型barContravariant<&'x i32>
  2. error函數接受的Contravariant<&'a i32>任何亞型,其中'a&mut s表達的壽命。
  3. 因此bar應的Contravariant<&'a i32>
  4. Contravariant<T>亞型超過T逆變,即如果U <: T,則Contravariant<T> <: Contravariant<U>
  5. 因此,當&'x i32超類型&'a i32時可以滿足子類型關係。
  6. 因此'x'a,即'a活得比'x

類似地,對於不變量類型,所導出的關係是'a == 'x,以及用於convariant,'x會超越'a。現在

,這裏的問題是,壽命在類型bar生命直到範圍的端部(按上面提到的限制):

let bar = Contravariant(PhantomData); // <--- 'x starts here -----+ 
    error(bar,        //       | 
      &mut s);       // <- 'a starts here ---+ | 
    s.len();        //      | | 
              // <--- 'x ends here¹ --+---+ 
              //      | 
              // <--- 'a ends here² --+ 
} 

// ¹ when `bar` goes out of scope 
// ² 'a has to outlive 'x 

在這兩個逆變和不變的情況下,'a會超越(或等於)'x表示語句s.len()必須包含在範圍內,從而導致借位錯誤。

只有在協變的情況下,我們可以做的'a短的射程比'x,讓s.len()被稱爲前的臨時對象&mut s被丟棄(意思是:在s.len()s不再被認爲借):

let bar = Covariant(PhantomData);  // <--- 'x starts here -----+ 
              //       | 
    error(bar,        //       | 
      &mut s);       // <- 'a starts here --+ | 
              //      | | 
              // <- 'a ends here ----+ | 
    s.len();        //       | 
}           // <--- 'x ends here -------+ 
+0

哦,我的!多麼驚人的答案!我已經擔心答案只會是「'RefCell'特別」,但這是一個了不起的見解。謝謝♥有一個問題,但:在'foo:Option '的情況下,我們能否真正打破記憶安全?還是由於編譯器如何在內部認爲它基本上是一個誤報? –

+0

@LukasKalbertodt如果這是額外的解釋,而不是問題,請隨意。 (如果還有其他相關問題,評論框太小,請改爲編輯問題)。 – kennytm

+0

@LukasKalbertodt我相信'Option '案件是一個誤判,可以用非詞彙的生命週期來解決。但我沒有詳細檢查,也許它會與線程有一些不好的交互。 – kennytm

相關問題