理解可變引用的排他性

本節內容完全屬於我個人推理,完全用我個人的理解來解釋結論,我不知道官方有沒有相關的術語,如果有,盼請告知。另外,如果結論錯誤,也盼請指正。

不可變引用可以共存,表示允許同時有多個不可變引用來訪問數據,這不難理解。

fn main(){
  let x = String::from("junmajinlong");
  let _x1 = &x;
  let _x2 = &x;
  let _x3 = &x;
}

可變引用具有排他性,某數據在某一時刻只允許有一個可變引用,此時不允許有其他任何引用。這看上去似乎這也不難理解。

例如,下面的代碼會報錯:cannot borrow x as mutable more than once at a time。


#![allow(unused)]
fn main() {
let mut x = String::from("junmajinlong");
let x_mut1 = &mut x;    // (1)
let x_mut2 = &mut x;    // (2)
println!("{}", x_mut1); // (3)
println!("{}", x_mut2); // (4)
}

多數Rust書籍都只是像上面示例一樣對【可變引用具有排他性】的結論粗淺地驗證一遍。

但真相比這要複雜一點。比如,去掉上面的代碼(3)或者同時去掉代碼(3)和(4),又或者將代碼(3)移到代碼(2)之前,得到的代碼都是可以正確執行的代碼:


#![allow(unused)]
fn main() {
// 可以正確執行
let mut x = String::from("junmajinlong");
let x_mut1 = &mut x;
let x_mut2 = &mut x;
println!("{}", x_mut2);

// 也可以正確執行
let mut x = String::from("junmajinlong");
let x_mut1 = &mut x;
let x_mut2 = &mut x;

// 也可以正確執行
let mut x = String::from("junmajinlong");
let x_mut1 = &mut x;
println!("{}", x_mut1);
let x_mut2 = &mut x;
println!("{}", x_mut2);
}

從上面的測試來看,同一份數據的多個可變引用是可以共存的。可見,可變引用具有排他性的【排他性】,其含義體現在更深層次。

可以將可變引用看作是一把獨佔鎖。在當前作用域內,從第一次使用可變引用開始創建這把獨佔鎖,之後無論使用原始變量(即所有權擁有者)、可變引用還是不可變引用都會搶佔這把獨佔鎖,以保證只有一方可以訪問數據,每次搶得獨佔鎖後,都會將之前所有引用變量給鎖住(原始變量依然可用),使它們變成不可用狀態。當離開當前作用域時,當前作用域內的所有獨佔鎖都被釋放。

因此,可變引用是搶佔且排他的,將其稱為搶佔式獨佔鎖更為合適。

換個角度來理解,自從第一次使用可變引用導致獨佔鎖出現後,可以隨時使用原始變量、可變引用或不可變引用來搶獨佔鎖,但搶鎖後以前的引用變量就不能再用,且當前持有的鎖也可以隨時被搶走。一切都由程序員控制,程序員可以在任意代碼位置通過原始變量或引用來搶鎖。

下面通過示例來分析上述規則。

fn main(){
  let mut a = String::from("junmajinlong");

  // 創建兩個不可變引用,不可變引用可以共存
  // 此時還沒有獨佔鎖
  let a_non_ref1 = &a;
  let a_non_ref2 = &a;
  // 可直接使用不可變引用
  println!("{}", a_non_ref1);
  println!("{}", a_non_ref2);

  // 第一次使用可變引用,將出現獨佔鎖,a_ref1擁有獨佔鎖
  let a_ref1 = &mut a;
  // 搶佔獨佔鎖後,前面兩個不可變引用變量將不能使用
  // 因此下面兩行代碼報錯
  //   println!("{}", a_non_ref1);
  //   println!("{}", a_non_ref2);

  // 再次使用不可變引用,a_non_ref3將獲得獨佔鎖
  let a_non_ref3 = &a;
  // 搶佔獨佔鎖後,前面所有引用變量都不能使用
  // 因此下面代碼會報錯
  //   println!("{}", a_ref1);
  //   println!("{}", a_non_ref1);

  // 再次使用可變引用,a_ref2將獲得獨佔鎖
  // 搶佔後前面所有該數據的引用都不可用
  let a_ref2 = &mut a;
  // 但a_ref2是可用的
  println!("{}", a_ref2);

  // 任何時候使用原始變量a,也會搶佔獨佔鎖
  // 原始變量搶得獨佔鎖後,前面所有引用變量將不能使用
  println!("{}", a);
  // 因此下面的代碼會報錯
  //   println!("{}", a_ref2);
}

理解上面的分析後,再分析代碼是否錯誤以及為什麼將非常輕鬆。

例如,下面第一段代碼為什麼不報錯,而第二段代碼是錯誤的:

fn main(){
  let mut x = String::from("junmajinlong");

  // (1).下面這段代碼是正確的
  let x1 = &mut x;     // 獨佔鎖出現,x1擁有獨佔鎖
  println!("{}", x1); // x1是可用的變量
  let x2 = &mut x;    // x2搶佔獨佔鎖,x1不可用
  println!("{}", x2); // x2是可用的變量

  // (2).下面這段代碼是錯誤的
  let x3 = &mut x;    // x3搶佔獨佔鎖
  ff(&x);  // &x搶佔獨佔鎖,參數s獲得鎖,使得x3不可用
  println!("{}", x3); // 使用了x3,導致報錯,註釋本行將正確
}

fn ff(s: &String){
  println!("{}", s);
}

再看下面這段代碼:

fn main(){
  let mut x = 33;
  let y = &mut x; // y獲得獨佔鎖
  x = *y + 1;     // 使用y獲取數據後,x重新搶得獨佔鎖
                  // 賦值之後,x有效,y將失效
  println!("{}", x);     // 正確
  // println!("{}", y);  // 錯誤
}

如果從位置表達式和值的角度來理解引用,會更直觀更容易理解。在通過位置和值理解內存模型中說過,位置具有一些狀態標記,其中之一就是該位置當前是否正在被引用以及如何被引用的狀態標記。

對某個位置每建立一次引用就記錄一次,如果是建立共享引用,則簡單判斷即可,但對該位置進行可變引用之後,從此刻開始的任意時刻,這個位置將只能存在單一使用者,使用者可以是原始變量,可以是新的可變引用或不可變引用,使用者可以隨時更換,但保證任意時刻只能有一個使用者