iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 13
0
自我挑戰組

WebAssembly + Rust 的前端應用系列 第 13

[Day 13] Rust Ownership 所有權 (2)

今天要來繼續研究 ownership,這是一個比較有挑戰的章節各位準備好了嗎?我們繼續前進吧!

Ownership Rules

在開始之前我們先來看一下 ownership 的規則,先節錄一段官方的說明,

Each value in Rust has a variable that’s called its owner.
There can only be one owner at a time.
When the owner goes out of scope, the value will be dropped.

翻譯一下,

  • 在 rust 中每個值都有一個叫做 owner 的變數
  • owner 一次只能有一個
  • 如果 owner 離開 scope 他就會把他的值丟掉

以上就是 rust ownership 的概念,其實看起來還蠻簡單的對吧?

Variable Scope

變數的 scope 這個相信如果有寫過其他語言的各位都不陌生,也就是在 scope 裡面的變數是不能與其他 scope 共用的例如,

fn main() {
  let s = "hello";
}
// 在外面就無法存取 s 這個變數了

不過這個概念像是 javascript 也有,所以其實也沒有多特別不是嗎?恩,讓我們繼續看下去。

The String Type

官網使用 String 這個類型來介紹我們上一篇提到過的 stack 以及 heap 的差異,那麼我們就來看看他葫蘆裡面賣什麼藥吧。

各位還記得之前我們使用 String 來讓使用者猜數字嗎?那個時候我們有用到 String 讓使用者輸入數字,

let mut guess = String::new();

那麼為什麼我們不能這樣寫呢?

let mut guess = "";

這種方式是用 stack 的方式在處理記憶體,但是因為我們不知道使用者會輸入的字串會是什麼所以要用 heap 的方式來宣告這個變數,(heap 和 stack 差異)而這兩種宣告方式最大的差異就是 Rust 底層在處理記憶體的方式:兩種截然不同的演算法 heap 跟 stack。

Memory and Allocation

還記得 heap 跟 stack 的差別嗎?如果我們的變數他的值是靜態的、可預測的那麼我們就可以用 stack 來處理反之則用 heap。所以因為這個例子 guess 這個變數是要由使用者所輸入的因此我們無法在編譯期間得知他到底會佔用多少記憶體空間,由於他只能在 runtime (執行期間)分配記憶體所以我們使用 heap 來處理並且返回一個 pointer。

讓我們回到之前的例子,

let mut guess = String::new();

io::stdin().read_line(&mut guess)
  .expect("Failed to read line");

所以這邊為什麼帶進 read_line 裡面的 &mut guess 他有 & 符號,這個符號在 Rust 裡面就是 pointer(這個概念會在後面解釋)。

所以簡單來說使用 String type 宣告代表

  • 這個變數必須在 runtime (執行期間)才能得到他的值
  • 在完成 String 之後我們需要他返回一個一個 pointer 來讓我們操作

也就是說 heap 類型的資料會需要我們去控制他的記憶體,不過這也就導致我們曾提到過人為操控記憶體時可能出現的錯誤,其實就是起因於我們到底該何時回收記憶體?而這也變成了困擾工程師很久的一個問題,如果沒收回就會浪費空間、如果收太快就會拿不到值、如果多回收了就會產生 Bug。

而 Rust 的處理方式不同,一但 owner 離開了 scope 記憶體就會自動釋放,讓我們再看一下例子,

{
    let s = String::from("hello");
}  

當變數在 scope 裡面時你可以對他存取做操作,但是當變數在 scope 外面的時候 Rust 會自動呼叫一支特別的 drop 程式把記憶體釋放掉。

雖然到目前為止都還蠻直觀簡單的但是在某些情況下可能會跟你所想的不太一樣,下面我們接著來看 move vs copy。

Move vs Copy

我們先來看一個例子,

fn move_vs_copy() {
  // 這邊的 y 在記憶體中複製了一整份完整的資料
  let x = 5;
  let y = x;
  println!("{}", x);
  // s1 的指標和長度等資訊轉移給 s2
  let s1 = String::from("hello");
  let s2 = s1;
  println!("{}", s1);
                 ^^ value borrowed here after move
}

各位可以看到這個例子在印出 x 時沒有問題但是怎麼卻沒辦法印出 s1 呢?因為 x 跟 y 用的是 copy 的方式但是 s1 跟 s2 用的是 move 的方式。

Copy

為了要讓各位更好了解筆者直接借用官網的圖來看這個概念 copy 就是複製一份完整的資料像是下圖,

https://ithelp.ithome.com.tw/upload/images/20190929/20119807nlkPM8nPXw.png

如果資料已知符合我們使用 stack 的條件也就是擁有固定的值,那麼因為他可以在編譯期間就處理完所以操作成本比較低,也因為這樣我們可以直接使用 copy 的方式完整的複製,這邊官網也有提到了有哪些資料型態是用 copy 的方式處理。

  • All the integer types, such as u32.
  • The Boolean type, bool, with values true and false.
  • All the floating point types, such as f64.
  • The character type, char.
  • Tuples, if they only contain types that are also Copy. For example, (i32, i32) is Copy, but (i32, String) is not.

Move

但是如果是未知的資料這時我們必須使用 heap 來處理,也因為處理 heap 的成本比較高(先前有提過需要搜尋成本)所以我們不能把資料完整的複製一份,取而代之的就是只複製他的指標和其相關資訊,我們用官網的圖來看這個概念,

https://ithelp.ithome.com.tw/upload/images/20190929/20119807rdziUgsqww.png

不過這樣做有個問題就是當兩個 pointer 都操作同一個資料的時候當兩筆變數要離開 scope 時會 drop 同一個資料而造成 double free 的錯誤,因此 Rust 為了要 memory safe 把 s1 給捨棄了他的圖會像這樣,

https://ithelp.ithome.com.tw/upload/images/20190929/201198078V9MjwUZC4.png

(反灰代表不能使用)

如果你有在其他的語言聽過 shallow copy 和 deep copy 其概念有點類似不過 Rust 是整個轉移所以我們把他稱作 move。

這邊官網有非常詳細的介紹 move 跟 copy 之間的差異我強烈建議各位可以到官網再自己看一遍或許會更清楚。

Clone

如果需要完整地複製 heap 的資料 Rust 也有提供 clone 的方法如下,

let s1 = String::from("hello");
let s2 = s1.clone();

println!("s1 = {}, s2 = {}", s1, s2);

總結

今天和各位再加強、理解了一次 heap 跟 stack 分別用在哪些情況下以及他的記憶體位置的差異,另外就是介紹了 ownership 的規則還有 copy 跟 move 的差異。

那麼我們下一篇會繼續把 ownership 的基本概念給講完,接著會講 References and Borrowing 以及 The Slice Type。ownership 概念完成之後 Rust 的核心概念就理解的差不多了終於要開始來玩一點應用了,讓我們繼續加油~

最後一樣有問題歡迎發問

/images/emoticon/emoticon07.gif

參考網站

ch04-01-what-is-ownership


上一篇
[Day 12] Rust Ownership 所有權 (1)
下一篇
[Day 14] Rust Ownership 所有權 (3)
系列文
WebAssembly + Rust 的前端應用30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
DanSnow
iT邦好手 1 級 ‧ 2021-04-06 12:04:48

這邊想講的是,其實不一定需要用 String 取得輸入,會用 String 是因為 Rust 的函式庫為了使用者的方便,於是加入了使用 String 做為 buffer 的程式碼,這段程式碼中會去要求 String 增加自己的空間,來確保能放進輸入的內容,但這部份完全可以由使用者自己來實作,如果有必要,也可以用固定大小的 buffer 來取得輸入內容

我要留言

立即登入留言