1. 程式人生 > 其它 >《Beginning Rust From Novice to Professional》---讀書隨記(借用與生命週期)

《Beginning Rust From Novice to Professional》---讀書隨記(借用與生命週期)

Beginning Rust From Novice to Professional

Author: Carlo Milanesi

如果需要電子書的小夥伴,可以留下郵箱,看到了會發送的

Chapter 22 Borrowing and Lifetimes

Ownership and Borrowing

之前已經說過所有權的問題,在賦值的過程或者傳遞引數的過程中,會出現兩種語義:移動語義和複製語義,也即是,要麼移交(移動)所有權到新的變數手中,要麼建立一個與舊的一模一樣資料的交給新變數手中,但始終所有權都只屬於一個變數。接下來要講講引用(借用)的問題

let n = 12;
let ref_to_n = &n;

在第二個語句之後,“ref_to_n”變數擁有一個引用,該引用引用的是由“n”引用的相同數字。

它不能是一種所有權,因為這個數字已經被“n”所擁有,如果它也被這個引用所擁有,它將被銷燬兩次。所以,像這樣的引用永遠不擁有一個物件。

“n”和“*ref_to_n”表示式指同一個物件,但只有“n”變數擁有它。“ref_to_n”變數可以訪問該物件,但它並不擁有它。這種概念被稱為“借用”。我們說“ref_to_n”借用了“n”擁有的相同數字。

Object Lifetimes

需要注意的是scope的概念是在編譯時使用的,而不是在執行時。相對的,在執行時中叫做lifetime。在Rust中,物件的生命週期是在建立它的指令執行和破壞它的指令執行之間的指令執行序列。在此時間間隔內,該物件被稱為“活著”。

當然,在範圍和生命週期之間存在著一種關係,但它們並不是同一個概念。例如:

let a;
a = 12;
print!("{}", a);

一般來說,一個變數的作用域在宣告該變數時開始,而一個物件的生命週期在該物件收到一個值時開始。變數範圍的結束也不總是與其擁有物件的生命週期結束的點一致。

let mut a = "Hello".to_string();
let mut b = a;
print!("{}, ", b);
a = "world".to_string();
print!("{}!", a);
b = a;

第一句:變數a宣告並初始化,a的作用域和生命週期開始
第二句:變數b宣告並初始化,a移動到b,b的作用域開始,但是a的作用域暫停了,因為它變得不可訪問了,但是b所擁有的物件並沒有建立,因為它擁有的是a建立的
第四句:a被賦值了一個新的物件,然後a恢復了作用域,然後一個新的物件建立,所以它的生命週期開始,這裡更像是a的初始化,因為它之前的物件移動了
第六句:a再次移動到b,所以a的作用域再次暫停,b接收了來自a的新物件,然後舊物件會在這裡被銷燬,因為沒有任何變數擁有它,所以它的生命週期結束了
隨著程式結束,先是b再到a,它們的作用域先後結束,然後是b擁有的那個物件,生命週期結束了,而a沒有擁有任何物件,所以沒有任何事情發生

從上面的分析可以看出來,scope作用域是指的變數(識別符號)的範圍,而lifetime指的是真正擁有值的那個物件的範圍。因為物件是隻存在與執行時,而變數是編譯時已知的,所以才有了之前提及的,scope是在編譯時的概念,lifetime是執行時的概念

Errors Regarding Borrowing

let ref_to_n;
{
    let n = 12;
    ref_to_n = &n;
    print!("{} ", *ref_to_n);
}
print!("{}", *ref_to_n);

ref_to_n宣告,但是沒有初始化,然後下一個範圍內,宣告並初始化n,然後是ref_to_n借用n擁有的物件,當超過這個範圍之後,n的作用域結束,它擁有的物件生命週期結束,然後在範圍外,ref_to_n再次使用了這個借用,所以編譯器會報錯"n does not live long enough",這個叫做use after drop錯誤

let mut v = vec![12];
let ref_to_first = &v[0];
v.push(13);
print!("{}", ref_to_first);

上面程式編譯器報錯"cannot borrow v as mutable because it is also borrowed as immutable",錯誤叫做use after change by an alias

此錯誤是由於向集合中插入項或刪除項會“無效”對集合的所有引用造成的。一般來說,此錯誤屬於更廣泛的錯誤類別,其中資料結構可以通過多個路徑或別名訪問,當使用一個別名更改資料結構時,它不能被另一個別名正確使用。

How to Prevent “Use After Drop” Errors

struct X(char);
impl Drop for X {
    fn drop(&mut self) {
        print!("{}", self.0);
    }
}
let _a = X('a');
let _b;
let _c = X('c');
_b = X('b');

在Rust中,變數釋放的順序是宣告的反方向,而不是初始化順序的反方向

所以,為了避免使用被刪除的物件,所有需要借用另一個變數擁有的物件的變數都必須在該變數之後宣告

How to Prevent “Use After Change by an Alias” Errors

首先,它需要考慮任何讀取物件而不寫它的語句,就像對該物件的臨時不可變借用,以及任何更改物件的語句,比如對該物件的臨時可變借用。

然後,需要記住,任何時候對一個物件的引用並分配給一個變數,借用就開始了;借用在這個變數範圍的末端結束

let a = 12;
let mut b = 13;
print!("{} ", a);

{
    let c = &a;
    let d = &mut b;
    print!("{} {} ", c, d);
}

b += 1;
print!("{}", b);

那麼,規則很簡單,任何物件,在程式碼的任何一點上,都不能同時有一個可變的借用和其他一些借用。

具體就是:

  • 沒有借用
  • 只有一個可變借用
  • 只有一個不可變借用
  • 多個不可變借用

Listing the Possible Cases of Multiple Borrowings

六種允許的情況:

// 多個不可變借用
let a = 12;
let _b = &a;
let _c = &a;
// 一個可變借用
let mut a = 12;
let _b = &a;
print!("{}", a);
// 在一個可變結束後,是一個不可變借用
let mut a = 12;
a = 13;
let _b = &a;
// 在一個可變結束後,是一個可變的借用
let mut a = 12;
a = 13;
let _b = &mut a;
// 在一個不可變借用結束,是一個不可變借用
let mut a = 12;
print!("{}", a);
let _b = &a;
// 在一個不可變借用結束後,是一個可變借用
let mut a = 12;
print!("{}", a);
let _b = &mut a;

然後是六種錯誤的用法:

// 在一個可變借用還沒結束的時候,又借用了一個不可變借用
let mut a = 12;
let _b = &mut a;
let _c = &a;
// 在一個不可變借用沒結束的時候,又借用了一個可變借用
let mut a = 12;
let _b = &a;
let _c = &mut a;
// 在一個可變借用沒結束的時候,又借用了一個可變借用
let mut a = 12;
let _b = &mut a;
let _c = &mut a;
// 在一個不可變借用沒結束的時候,又臨時借用了一個可變借用
let mut a = 12;
let _b = &a;
a = 13;
// 在一個可變借用沒結束的時候,又臨時借用了一個可變借用
let mut a = 12;
let _b = &mut a;
a = 13;
// 在一個可變借用沒結束的時候,又臨時借用了一個不可變借用
let mut a = 12;
let _b = &mut a;
print!("{}", a);

上面總結一下,就是在同一時刻,只能存在無數個不可變借用,或者一個可變借用

Using a Block to Restrict Borrowing Scope

let mut a = 12;
{
    let b = &mut a;
    *b += 1;
}
let c = &mut a;
*c += 2;

b變數被限制在一個塊裡面,所以在超出這個塊之後,b的作用域就結束了,所以c可以繼續借用,其實這個塊和一個函式是等價的

let mut a = 12;
fn f(b: &mut i32) {
    *b += 1;
}
f(&mut a);
let c = &mut a;
*c += 2;

The Need of Lifetime Specifiers for Returned References

let v1 = vec![11u8, 22];
let result;
{
    let v2 = vec![33u8];
    result = {
        let _x1: &Vec<u8> = &v1;
        let _x2: &Vec<u8> = &v2;
        _x1
    }
}
print!("{:?}", *result);

再次強調,保證不出現use after drop錯誤,借用變數的宣告應該在擁有者變數的後面

所以上面的程式碼是正確的,因為result在v1的後面宣告,但如果程式碼改成了下面的樣子

let v1 = vec![11u8, 22];
let result;
{
    let v2 = vec![33u8];
    result = {
        let _x1: &Vec<u8> = &v1;
        let _x2: &Vec<u8> = &v2;
        _x2
    }
}
print!("{:?}", *result);

那就會報錯了,因為result的宣告在v2的前面,所以result使用時不能保證v2有效。然後再次修改程式碼

let v1 = vec![11u8, 22];
let result;
{
    let v2 = vec![33u8];
    fn func(_x1: &Vec<u8>, _x2: &Vec<u8>) -> &Vec<u8> {
        _x1
    }
    result = func(&v1, &v2);
}
print!("{:?}", *result);
-----------------------------------------
let v1 = vec![11u8, 22];
let result;
{
    let v2 = vec![33u8];
    fn func(_x1: &Vec<u8>, _x2: &Vec<u8>) -> &Vec<u8> {
        _x2
    }
    result = func(&v1, &v2);
}
print!("{:?}", *result);

如果根據這個關係來檢查函式的有效性,那編譯器的工作時非常巨大的,因此,與泛型函式類似,返回引用的函式也必須在函式簽名處隔離借用檢查。如果需要借用-檢查任何函式,只考慮函式的簽名、主體和內部中任何被呼叫函式的簽名,而不需要考慮這個被呼叫函式的主體

所以,上面兩個程式碼的編譯器錯誤:"missing lifetime specifier",“lifetime specifier”是函式簽名的裝飾,它允許借用檢查器分別檢查該函式的主體以及對該函式的任何呼叫。

Usage and Meaning of Lifetime Specifiers

fn func(v1: Vec<u32>, v2: &Vec<bool>) {
    let s = "Hello".to_string();
}

分析這個函式的關於的變數的情況

  1. 被函式的引數擁有的物件,例如v1
  2. 被本地變數擁有的物件,例如s
  3. 臨時物件"Hello".to_string()表示式
  4. 靜態物件,"Hello"
  5. 函式引數擁有的借用,這個物件存在於這個函式執行前

當一個函式如果需要返回引用的時候,首先不能引用函式引數的物件,本地變數的物件或者臨時物件,因為當函式結束,所有這些都會銷燬,會造成懸空引用

那麼剩下可以返回的,就是靜態物件,或者是函式引數借用的物件

fn func() -> &str {
    "Hello"
}
fn func(v: &Vec<u8>) -> &u8 {
    &v[3]
}

所以,借用檢查器只對返回值中包含的引用感興趣,這種引用可以有兩種:引用靜態物件,或者借用作為引數接收的一個物件。

為了在不分析函式主體的情況下完成工作,借用檢查器需要知道哪些返回的引用引用靜態物件,哪些引用一個物件作為引數;在第二種情況下,如果有幾個物件作為引數接收,其中哪一個被任何非靜態返回引用借用。

trait Tr {
    fn f(flag: bool, b: &i32, c: (char, &i32)) -> (&i32, f64, &i32);
}

這個函式的簽名是非法的,這個函式的入參有兩個引用,同樣的,在返回值中有兩個引用返回,返回值的引用,可以引用靜態物件,也可以引用b,也可以是c,所以返回值的引用物件不確定,需要手動標註,所以就有了lifetime specifier

trait Tr {
    fn f<'a>(flag: bool, b: &'a i32, c: (char, &'a i32)) -> (&'a i32, f64, &'static i32);
}

首先,a只是一種宣告,與泛型引數類似,為了與泛型引數區分,前面加了',然後泛型引數一般是大寫的字母,而生命週期說明符是小寫的字母

簽名中,出現了三個使用了'a的地方,b,c和第一個返回值,第三個返回值是使用了'static

然後此處'a的含義是,返回值的第一個欄位借用了b引數和c引數的第二個欄位已經借用的那個物件,因此它的壽命必須少於該物件

然後'staic的含義是,返回值的第三個欄位是一個靜態物件,因此它可以在任何時間執行

trait Tr {
    fn f<'a>(flag: bool, b: &'a i32, c: (char, &i32)) -> (&'static i32, f64, &'a i32);
}

這樣標註也是可以的,不同的是,第三個返回值的只與b引數借用的物件的生命週期有關,而不是b和c,因為c沒有標註

trait Tr {
    fn f<'a, 'b, T1, T2>(flag: bool, b: &'a T1, c: (char, &'b i32)) -> (&'b i32, f64, &'a T2);
}

這樣也是可以的,這次有兩個生命週期宣告,第一個返回值與c關聯,第三個返回值與b關聯。

但是其實含義都沒有改變,都是表明,返回值的生命週期小於入參的借用的某個物件

Checking the Validity of Lifetime Specifiers

編譯時借用檢查器需要做的

  • 檢查該函式的簽名是否有效,對其本身並對其主體有效。
  • 檢查該函式的主體是否有效,並考慮到在主體中被呼叫的任何函式的簽名。

如果函式返回值沒有引用,那麼借用檢查器什麼都不做

static FOUR: u8 = 4;
fn f() -> (bool, &'static u8, &'static str, &'static f64) {
    (true, &FOUR, "Hello", &3.14)
}

上面程式碼是有效的,因為所有的生命週期都是對的

fn f(n: &u8) -> &'static u8 {
    n
}

這個就是錯的,因為返回的引用不是引用的靜態物件

fn f<'a, 'b>(x: &'a i32, y: &'b i32) -> (&'b i32, bool, &'a i32) {
    (y, true, x)
}
let i = 12;
let j = 13;
let r = f(&i, &j);
print!("{} {} {}", *r.0, r.1, *r.2);

這是正確的,第一個返回值與y關聯,第三個返回值與x關聯,它們代表兩種生命週期,沒有長短之分,注意的是,x和y的返回位置,不可以互換,因為它們的生命週期不一樣

fn f<'a>(n: i32, x: &'a Vec<u8>, y: &Vec<u8>) -> &'a u8 {
    if n == 0 { return &x[0]; }
    if n < 0 { &x[1] } else { &y[2] }
}

y沒有標註生命週期,所以是非法的,編譯器會報錯

Using the Lifetime Specifiers of Invoked Functions

正如我們在前一節開始時所說的,借用檢查器的兩個任務之一是,在編譯一個函式時,檢查該函式的主體是否有效,並考慮到在主體中呼叫的任何函式的簽名。

let v1 = vec![11u8, 22];
let result;
{
    let v2 = vec![33u8];
    fn func<'a>(_x1: &'a Vec<u8>, _x2: &Vec<u8>) -> &'a Vec<u8> {
        _x1
    }
    result = func(&v1, &v2);
}
print!("{:?}", *result);
let v1 = vec![11u8, 22];
let result;
{
    let v2 = vec![33u8];
    fn func<'a>(_x1: &Vec<u8>, _x2: &'a Vec<u8>) -> &'a Vec<u8> {
        _x2
    }
    result = func(&v1, &v2);
}
print!("{:?}", *result);

第一個是合法的,不過第二個是非法的,"v2 does not live long enough"

為什麼第二個還是不通過?從函式簽名和函式主體去分析(上一節),兩個函式都是正確的標註了生命週期的,是合法的

這時候先從main函式開始看第一個函式,當func執行的時候,有v1,result 和v2三個已經宣告的變數,然後是v1和v2已經初始化了,然後func的簽名說明了,result的值有著和第一個引數一樣的生命週期,那就意味著,賦值給result的物件的生命週期必須比v1小,這也是成立的,因為result的宣告在v1之後,所以它會比v1早銷燬

接著看第二個函式,也是從main開始,func的簽名說明,result的值與第二個引數有著一樣的生命週期,這就意味著,result的生命週期必須比v2小,但是這很明顯不對,因為result的宣告在v2之前,也就說,result的銷燬比v2要晚

現在讓我們解釋一下,為什麼在上一節的最後一個示例中,只使用一個生命週期說明符不如對f函式使用兩個生命週期說明符好。

fn f<'a, 'b>(x: &'a i32, y: &'b i32) -> (&'a i32, bool, &'b i32) {
    (x, true, y)
}

let i1 = 12;
let i2;
let j1 = 13;
let j2;
let r = f(&i1, &j1);
i2 = r.0;
j2 = r.2;
print!("{} {} {}", *i2, r.1, *j2);

這是使用兩個生命週期說明符的正確函式

fn f<'a>(x: &'a i32, y: &'a i32) -> (&'a i32, bool, &'a i32) {
    (x, true, y)
}

let i1 = 12;
let i2;
let j1 = 13;
let j2;
let r = f(&i1, &j1);
i2 = r.0;
j2 = r.2;
print!("{} {} {}", *i2, r.1, *j2);

接著是隻使用了一個說明符的函式,這時候就出錯了"j1 does not live long enough"

兩個函式的內部和簽名都是對的,這時候從函式被呼叫處分析,也就是外部環境與函式的簽名的角度進行分析

首先第一個正確版本,第一個返回值與第一個引數關聯,衍生下去,也就是i2與i1關聯,i2必須比i1的生命週期小,然後顯然是對的,i2的宣告在i1之後;以此類推,j2必須比j1的生命週期小,顯然也是對的,j2的宣告在j1之後

接著是第二個錯誤版本,由於只有一個說明符,也就是說,i2和j2必須要比i1和j1小,然後實際是,i2的宣告在j1的前面,所以,j1不符合條件,所以出現了上面的錯誤,j1的生命週期不夠長