引用與借用

2018-08-12 22:03 更新

引用與借用

這篇指南是 Rust 已經(jīng)存在的三個所有權(quán)制度之一。這是 Rust 最獨特和最令人信服的一個特點,其中 Rust 開發(fā)人員應(yīng)該相當(dāng)熟悉。所有權(quán)即 Rust 如何實現(xiàn)其最大目標(biāo)和內(nèi)存安全。這里有幾個不同的概念,每一個概念都有它自己的章節(jié):

  • 所有權(quán),即正在讀的這篇文章。

  • 借用,和與它們相關(guān)的功能‘引用’

  • 生存期,借用的先進理念

這三篇文章相關(guān)且有序。如果你想完全的理解所有權(quán)制度,你將需要這三篇文章。

在我們了解細節(jié)之前,這里有關(guān)于所有權(quán)制度的兩個重要說明需要知道。

Rust 注重安全和速度。它通過許多‘零成本抽象’來完成這些目標(biāo),這意味著在 Rust 中,用盡可能少的抽象成本來保證它們正常工作。所有權(quán)制度是一個零成本抽象概念的一個主要例子。我們將在這篇指南中提到的所有分析都是在編譯時完成的。你不用為了任何功能花費任何運行成本。

然而,這一制度確實需要一定的成本:學(xué)習(xí)曲線。許多新用戶使用我們喜歡稱之為‘與借檢查器的人斗爭’,即 Rust 編譯器拒絕編譯那些作者認為有效的程序的 Rust 經(jīng)驗。這往往因為程序員對于所有權(quán)的怎樣工作與 Rust 實現(xiàn)的規(guī)則不相符的心理模型而經(jīng)常出現(xiàn)。你可能在第一次時會經(jīng)歷類似的事情。然而有個好消息:更有經(jīng)驗的 Rust 開發(fā)者報告稱,一旦他們遵從所有權(quán)制度的規(guī)則工作一段時間后,他們會越來越少的與借檢查器的行為斗爭。

學(xué)習(xí)了這些后,讓我們來了解借用。

借用

所有權(quán)章節(jié)的末尾部分,我們有一個令人討厭的功能,如下所示:

fn foo(v1: Vec<i32>, v2: Vec<i32>) -> (Vec<i32>, Vec<i32>, i32) {
// do stuff with v1 and v2

// hand back ownership, and the result of our function
(v1, v2, 42)
}

let v1 = vec![1, 2, 3];
let v2 = vec![1, 2, 3];

let (v1, v2, answer) = foo(v1, v2);

這不是慣用的 Rust,然而,由于它不能利用借出,以下是第一步:

fn foo(v1: &Vec<i32>, v2: &Vec<i32>) -> i32 {
// do stuff with v1 and v2

// return the answer
42
}

let v1 = vec![1, 2, 3];
let v2 = vec![1, 2, 3];

let answer = foo(&v1, &v2);

// we can use v1 and v2 here!

我們使用了一個引用:&Vec<i32>,而不是將 Vec<i32> 作為我們的參數(shù)。同時,我們不是直接傳遞 V1V2,我們傳遞 &V1&V2。我們稱 &T 為一個‘引用’,它借用了所有權(quán),而不是擁有了資源。借用東西的綁定當(dāng)它超出作用域時,它不解除分配給它的資源。這意味著在調(diào)用 foo() 函數(shù)后,我們原始的綁定可以再次被使用。

引用與綁定類似,它們都是不可變的。這意味著,在 foo() 函數(shù)中,向量根本不能改變:

fn foo(v: &Vec<i32>) {
 v.push(5);
}

let v = vec![];

foo(&v);

錯誤:

error: cannot borrow immutable borrowed content `*v` as mutable
v.push(5);
^

推入一個值改變了這個變量,所以我們不允許這樣做。

&mut引用

這里有第二種引用:&mut T。一個‘可變引用’允許你改變你借用的資源。例如:

let mut x = 5;
{
let y = &mut x;
*y += 1;
}
println!("{}", x);

以上代碼將輸出 6。我們將 y 標(biāo)記為 x 的一個可變引用,然后將 y 指向的內(nèi)容加 1。你將會注意到 x 也不得不被標(biāo)記為 mut,如果不被標(biāo)記,我們不能將一個可變值借用給一個不可變值。

其他方面,&mut 引用與其他引用一樣。盡管它們之間以及它們?nèi)绾蜗嗷プ饔糜?em>一個很大的區(qū)別。你可以在上面的例子中看到一些可疑的東西,因為我們需要那個用 {} 包圍的額外空間。如果我們刪除它們,我們將會得到一個錯誤:

error: cannot borrow `x` as immutable because it is also borrowed as mutable
println!("{}", x);
   ^
note: previous borrow of `x` occurs here; the mutable borrow prevents
subsequent moves, borrows, or modification of `x` until the borrow ends
let y = &mut x;
 ^
note: previous borrow ends here
fn main() {

}
^

事實證明,這里是有規(guī)則的。

規(guī)則

這里有關(guān)于借用在 Rust 中的規(guī)則:

首先,任何借用必須持續(xù)比所有者的作用域小。其次,你可能有一個或者兩種其他的借用,但是兩種不能在同一時間同時使用:

  • 一個資源的從 0 到 N 的引用 ( &T )。

  • 一個可變的引用 ( &mut T

你可能會注意到這與數(shù)據(jù)競爭的定義非常相似,盡管并不是完全相同:

There is a ‘data race’ when two or more pointers access the same memory location at the same time, where at least one of them is writing, and the operations are not synchronized.

使用引用,由于它們都不編寫,所以你喜歡用多少用多少。如果你想要編寫,你需要兩個或者更多的指針指向同一內(nèi)存,你每次僅僅可以使用一個 &mut。這就是 Rust 如何在編譯時避免數(shù)據(jù)競爭:如果我們違反了規(guī)則,我們將會得到錯誤。

學(xué)習(xí)了以上內(nèi)容后,讓我們再次重新考慮我們的例子。

在作用域中的思考

以下為相關(guān)代碼:

let mut x = 5;
let y = &mut x;

*y += 1;

println!("{}", x);

這些代碼給出我們?nèi)缦洛e誤:

error: cannot borrow `x` as immutable because it is also borrowed as mutable
println!("{}", x);
   ^

這是因為我們違反了規(guī)則,我們有一個 &mut T 指向 x ,所以我們不允許創(chuàng)建任何 &T。反之亦然。以下注釋暗示我們?nèi)绾嗡伎歼@個問題:

note: previous borrow ends here
fn main() {

}
^

換句話說,這個可變的借用貫穿了我們剩余的示例。我們想要的是在我們嘗試調(diào)用 printLn!,可變借用結(jié)束,然后使用不變的借用。在 Rust 中,借用被綁定在對于借用有效的作用域內(nèi)。我們的作用域如下所示:

let mut x = 5;

let y = &mut x;// -+ &mut borrow of x starts here
   //  |
*y += 1;   //  |
   //  |
println!("{}", x); // -+ - try to borrow x here
   // -+ &mut borrow of x ends here

作用域沖突:我們不能讓 &xy 在一個作用域內(nèi)。

因此我們添加大括號:

  let mut x = 5;

{   
let y = &mut x; // -+ &mut borrow starts here
*y += 1;//  |
}   // -+ ... and ends here

println!("{}", x);  // <- try to borrow x here

此時沒有問題。我們可變的借用在我們創(chuàng)建不變的借用之前,超出了作用域。但是作用域是我們可以觀察到一個借用可以持續(xù)多久的關(guān)鍵。

借用問題預(yù)防

為什么會有這些預(yù)防性的規(guī)則?正如我們所指出的,這些規(guī)則預(yù)防了數(shù)據(jù)競爭現(xiàn)象。什么樣的問題是引起數(shù)據(jù)競爭現(xiàn)象的原因呢?以下有幾個原因。

迭代器失效

其中一個例子是當(dāng)你嘗試改變你正在循環(huán)的集合時將會出現(xiàn)的‘迭代器失效’。Rust 的借用檢查器防止這種情況的發(fā)生:

let mut v = vec![1, 2, 3];

for i in &v {
println!("{}", i);
}

以上代碼將打印出從 1 到 3 的數(shù)字。當(dāng)我們循環(huán)訪問這些向量時,我們僅僅給出了這些元素的引用。V 本身作為不可變的借用,這意味著,在我們遍歷的過程中我們不能改變它:

let mut v = vec![1, 2, 3];

for i in &v {
println!("{}", i);
v.push(34);
}

以下是出現(xiàn)的錯誤:

error: cannot borrow `v` as mutable because it is also borrowed as immutable
v.push(34);
^
note: previous borrow of `v` occurs here; the immutable borrow prevents
subsequent moves or mutable borrows of `v` until the borrow ends
for i in &v {
  ^
note: previous borrow ends here
for i in &v {
println!(“{}”, i);
v.push(34);
}
^

我們不能修改 V,因為它在循環(huán)中被借用。

釋放內(nèi)存后再使用

引用必須與它們引用到的資源存活時間一樣長。 Rust 會檢查你的引用的作用域來保證它為真。

如果 Rust 不檢查這個屬性,我們可能會無意間使用一個無效的引用。例如:

let y: &i32;
{ 
let x = 5;
y = &x;
}

println!("{}", y);

我們將會得到以下錯誤:

error: `x` does not live long enough
y = &x;
 ^
note: reference must be valid for the block suffix following statement 0 at
2:16...
let y: &i32;
{ 
let x = 5;
y = &x;
}

note: ...but borrowed value is only valid for the block suffix following
statement 0 at 4:18
let x = 5;
y = &x;
}

換句話說,y 只在 X 存在的作用域內(nèi)有效。一旦 x 消失了,它將會變成一個 x 的無效引用。因此,上面代碼中的錯誤中說借用‘活的時間不夠長’,因為它在有效的矢量的時間內(nèi)是無效的。

當(dāng)在引用到的變量之聲明引用時,會發(fā)生同樣的問題:

let y: &i32;
let x = 5;
y = &x;

println!("{}", y);

我們會得到以下錯誤:

error: `x` does not live long enough
y = &x;
 ^
note: reference must be valid for the block suffix following statement 0 at
2:16...
let y: &i32;
let x = 5;
y = &x;

println!("{}", y);
}

note: ...but borrowed value is only valid for the block suffix following
statement 1 at 3:14
let x = 5;
y = &x;

println!("{}", y);
}
以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號