Rust核心設(shè)計(jì)之Ownership

Ownership in Rust

背景

目前主流編程語言管理內(nèi)存的方式不外乎兩種--gc或者手動. ownership是rust最獨(dú)特的特性, 屬于第三種解決方案. 它被用來管理內(nèi)存以及跟蹤代碼使用的堆上數(shù)據(jù), 最大化地減少堆上的重復(fù)數(shù)據(jù). 由于這種方式在編譯期間進(jìn)行, 因此它的任何特性均不會拖慢程序運(yùn)行時的性能.


owner的規(guī)則

  1. 每個值都有一個變量, 稱其為owner
  2. 他們同一時間只有一個owner
  3. 當(dāng)owner走出scope時, 值將被釋放

簡單的機(jī)制

在owner走出scope時, rust會調(diào)用一個特殊的drop函數(shù), 來釋放該owner.


實(shí)現(xiàn)該機(jī)制遇到的復(fù)雜場景

ownership受到C++的RAII機(jī)制的啟發(fā). 看上去原理簡單, 但是實(shí)現(xiàn)起來還是相當(dāng)復(fù)雜的.
以下是一些具體的場景:

  1. move, 類似淺拷貝
let x = String.from("hello");
let y = x;
// 編譯錯誤
println!("{}", x);

這里類似淺拷貝但又有所區(qū)別, 拷貝的是變量本身, 在棧中入了一份一樣的變量, 但是指向的值在堆中, 是同一份. 所以問題來了, 假設(shè)此時有2個owner, 那么在退出該scope時, 需要釋放一個內(nèi)存兩次, 這是不行的. 所以, 回到規(guī)則的第二條, owner只能有一個, 這就是move和淺拷貝的區(qū)別, 因?yàn)樗屧醋兞康膐wnership傳遞到新的變量, 使源變量失效. 假使在上面兩行后面再加一行對x的訪問, 那么會在編譯時報錯, 提示borrow of moved value: x. (rust永遠(yuǎn)不會自動對數(shù)據(jù)使用深拷貝, 這種情況下的拷貝被認(rèn)為是沒什么代價的)

  1. 使用深拷貝
let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1 = {}, s2 = {}", s1, s2);

既然是深拷貝, 那么值自然就是2個, 因此也就不存在違反規(guī)則的情況.
在非手動的情況下, Rust避免使用深拷貝是出于對性能的考慮.

  1. 具有Copy特征(其他語言叫接口)的情況
let x = 5;
let y = x;
println!("x = {}, y = {}", x, y);

這里x仍然可用. 所有的整形, 浮點(diǎn), 布爾, 字符類型以及元組都是有Copy特征的.
為什么這么做呢? 因?yàn)檫@些變量size固定, 在編譯期間被存入棧中, 這樣做代價很低, 移動一下棧頂指針就可以了, 所以干脆copy一下值.

  1. 函數(shù)
let s = String::from("hello");
some_function(s);
let x = 5;
another_function(x);

在語義上, 等同于賦值給變量, 使用move或者copy.

  1. 函數(shù)返回值
let s1 = String::from("hello");
let s2 = some_function(s1);
let s3 = another_function(s2);

函數(shù)返回值同樣可以將ownership傳遞到賦值的變量.

總結(jié)起來其實(shí)遵循的規(guī)律是一樣的, 當(dāng)值由一個變量轉(zhuǎn)到另一個變量時, 使用move. 指向堆中數(shù)據(jù)的變量出scope時, 值將會被清除, 除非此值已被move.


引用&借用

當(dāng)變量傳入函數(shù), 如果是move, 則該變量已失效, 那么如何獲取原變量的值呢?

使用元組獲取原ownership

fn main() {
    let s1 = String::from("hello");
    let (s2, len) = calculate_length(s1);
    println!("The length of '{}' is {}.", s2, len);
}

fn calculate_length(s: String) -> (String, usize) {
    let length = s.len();
    (s, length)
}

這種方式有點(diǎn)麻煩, 寫多了肯定會吐
于是有了下面這種:

fn main() {
    let s1 = String::from("hello");
    let len = calculate_length(&s1);
    println!("The length of '{}' is {}.", s1, len);
}

fn calculate_length(s: &String) -> usize {
    s.len()
}

這里創(chuàng)建了一個引用指向s1, 結(jié)構(gòu)看上去是這樣的&s1->s1->data, 因?yàn)橐貌]有獲取這個值的ownership, 因此在引用退出scope時, 它的值不會被drop. 這種引用作為函數(shù)參數(shù)的方式稱為borrowing. 這個名字非常形象, 因?yàn)檫@表示這樣?xùn)|西的所有權(quán)并不是我們的, 并且有借就有還.

引用也是有可變和不可變的, 可變就加關(guān)鍵字mut. 這里有一個約束, 同一個scope中, 同一個值, 只能有一個可變引用, 這是為了規(guī)避數(shù)據(jù)競爭(它的條件: 1.有多個指針同時訪問相同變量 2.其中至少有一個可以寫數(shù)據(jù) 3.沒有同步機(jī)制).

let mut s = String::from("hello");
{
    let r1 = &mut s;
}
let r2 = &mut s;

這是可以的, 因?yàn)閞1已經(jīng)退出scope.

let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
let r3 = &mut s;
println!("{}, {}, and {}", r1, r2, r3);

錯誤, s已被借為不可變量, 不能同時被借為可變量.

let r1 = &s;
let r2 = &s;
println!("{} and {}", r1, r2);
let r3 = &mut s;
println!("{}", r3);

可以, 因?yàn)閞1, r2已經(jīng)不再被使用, 他們的scope沒有交集.


懸掛指針

編譯期間會杜絕這種情況的發(fā)生,保證了引用指向的變量一定在scope內(nèi)。

fn main() {
    let dangling = dangle();
}

fn dangle() -> &String {
    let s = String::from("hello");
    &s
}

因?yàn)閟已經(jīng)退出scope,返回s的引用是無法通過編譯的。


切片

切片沒有ownership,因?yàn)榧僭O(shè)它有,那么這個ownership將被2個owner擁有,即slice與原集合,違反了owner的基本原則。編譯器會保證切片引用的變量一定不會退出scope,看個例子。

fn main() {
    let mut s = String::from("hello world");
    let a_slice = slice_of(&s); // 省略函數(shù)定義
    s.clear(); // error
    println!("the slice is: {}", a_slice);
}

這里會報出一個編譯錯誤,rustc --explain E0502看一下原因,

This error indicates that you are trying to borrow a variable as mutable when it
has already been borrowed as immutable.

哪里有mutable的借用呢?
看下clear()函數(shù)的源碼:

#[inline]
#[stable(feature = "rust1", since = "1.0.0")]
pub fn clear(&mut self) {
    self.vec.clear()
}

可以看到入?yún)⑹亲陨淼目勺兘栌?。之前提到過,可變與不變引用不能同時出現(xiàn)的同樣的scope中,或者這么說,它們的scope有交集,因?yàn)檫@樣會滿足數(shù)據(jù)競爭的條件,這是嚴(yán)格禁止的。因此,從編譯層面保證了切片指向的值一定是有效的。

總結(jié)

說到底,其核心思想就是將內(nèi)存占用與變量的生命周期綁定,當(dāng)變量生命周期結(jié)束,內(nèi)存也將釋放。
偉人總是站在偉人的肩膀上,我們總是站在偉人的肩膀上。向偉大的前輩致敬。這種設(shè)計(jì)非常巧妙,即保證的效率,又方便了開發(fā)者。不過凡事都有兩面性,編譯期間搞的這么6,編譯速度比起C++怕是不遑多讓:)

參考文獻(xiàn)

“The Rust Programming Language”, by Steve Klabnik and Carol Nichols

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容