iT邦幫忙

2024 iThome 鐵人賽

DAY 30
3
Modern Web

Rust 的戰國時代:探索網頁前端工具的前世今生系列 第 30

Day 30:Rust 中的所有權 (Ownership) 是什麼?(3)、系列文總結、完賽心得

  • 分享至 

  • xImage
  •  

什麼是借用 (Borrowing)?

昨天的最後看到這段程式碼中,可以在 calculate_length 這個函式中將原本的傳入的 String 變數的所有權,再用 tuple 的方式傳回去,達到能在原本的 main 去取得傳入變數的所有權的做法:

fn main() {
    let s1 = String::from("hello");

    // 拿回原本該 String 變數的所有權
    let (s2, len) = calculate_length(s1);

    println!("'{}' 的長度為 {}。", s2, len);
}

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

    // 將原本傳入的 s 的所有權再傳回去
    (s, length)
}

但這樣寫稍微太囉唆了,因此在 Rust 中有更優雅的處理方式,可以用傳參考的運算符號 & 來實現,來看看下面怎麼改寫:

fn main() {
    let s1 = String::from("hello");

    // 只傳入 s1 的參考值
    let len = calculate_length(&s1);

    println!("'{}' 的長度為 {}。", s1, len);
}

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

在上面的程式中將原本 calculate_length(s1) 這段改成傳入 &s1 的寫法。參考下圖,這個意思是指傳入這個函式的 s 會去建立一個指向 s1 變數的參考指標,藉此來在不同函式中去取得 s1 的數值,但不取得其所有權,這個行為就稱為 borrowing

borrowing

也就是對 main 這個函式而言,在呼叫 calculate_length 時會跟它說「我不給你 s1 這個變數的所有權,但我可以給你。而當 calculate_length 執行完後,這個 s 的指標就會完成任務被釋放,這樣的做法也達到了避免在只是要取一個變數的值時,需要去複製一份記憶體的浪費。

借用的值能不能被修改?

那延續上面的例子,如果今天想要在原本計算長度的函式中對這個借來的 String 去做修改是可行的嗎?

沒錯,這是做的到的,這裡來看看要怎麼修改。就是將原本的 &s1 改成 &mut s1 就能達到此效果,這個操作又稱為傳入可變的參考 (Mutable References):

fn process_string(s: &mut String) -> usize {
    // 因為 s 是可變的參考,所以可以對它修改
    s.push_str(", world");
    let length = s.len();

    length
}

fn main() {
    // 將 s1 改為 mutable
    let mut s1 = String::from("hello");

    // 傳入 mutable 版本的參考
    let len = process_string(&mut s1);

    // 印出 “修改後的 'hello, world' 長度為 12。”
    println!("修改後的 '{}' 長度為 {}。", s1, len);
}

可以一次把某個變數借給很多人嗎?

需要特別注意的是,借用這件事是可以一次借給很多人,像下面這樣是能被正確編譯過的:

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

    let r1 = &s;
    let r2 = &s;
    let r3 = &s;

    println!("{}, {}, {}", r1, r2, r3);
}

但如果是要用可變參考的方式來借用的話會有一些限制在,以下來看幾個例子。

  1. 不能同時有多個可變參考:
fn main() {
    let mut s = String::from("hello");

    // cannot borrow `s` as mutable more than once at a time
    let r1 = &mut s;
    let r2 = &mut s;
    let r3 = &mut s;

    println!("{}, {}, {}", r1, r2, r3);
}
  1. 不能在有不可變參考的同時又借用為可變參考:
fn main() {
    let mut s = String::from("hello");

    let r1 = &s;
    let r2 = &s;

    // cannot borrow `s` as mutable because it is also borrowed as immutable
    let r3 = &mut s;

    println!("{}, {}, {}", r1, r2, r3);
}
  1. 當借用的行為結束後,則不在此限制內:
fn main() {
    let mut s = String::from("hello");

    let r1 = &s;
    let r2 = &s;

    println!("{}, {}", r1, r2);

    // 這裡是可行的,因為前面借用已經完成
    let r3 = &mut s;

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

這主要是 Rust 中為了避免資料在取用時會互相競爭 (被稱為 Data Races) 的防呆,以防借出去的變數在其他人正在使用時,因為在某處被以可變的狀態借出而改值因而造成不可預期的錯誤。

Dangling Reference

最後再來看另一個關於借用的經典問題 —— 懸空參考 (Dangling Reference):

fn main() {
    // 因為此時回傳值的實際記憶體已被釋放
    // 這裡會指向一個無效的 String
    let reference_to_nothing = dangle();
}

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

    // 回傳 String s 的參考
    &s
} // s 在離開這個作用域後就被釋放

當執行上面這段程式碼時,會得到一段訊息說明 this function's return type contains a borrowed value, but there is no value for it to be borrowed from,因為在 dangle 中最後去借用了 s 這個變數,但在這個作用域結束後 s 就被釋放掉,成為一個已經不存在的值,因此這個回傳的參考就會變成一個懸空參考,實際會指向一個無效的 String。而根本的解法也很容易,就是不要借用直接回傳 s 就正確了。

💡 而上面這段程式中其實在編譯時的完整錯誤訊息中,還會提到生命週期相關的資訊,但那會是另一個新戰場這裡就先不深入展開了。

Slice

最後再簡單提一下切片 (Slice) 的概念,從文件上看起來切片的用途是為了讓你不需要一次參考到整段資料,而只去取某一段需要的資料,而這樣的一小段就稱為切片:

fn main() {
    let s = String::from("hello world");

    let hello = &s[0..5];
    let world = &s[6..11];
}

我有個好奇是看起來切片也是可以設定為 &mut 來做可變參考的,但當今天我是借用不同段的資料像下面這樣的話編譯的過嗎:

fn main() {
    let mut s = String::from("hello world");

    let hello = &s[0..5]; // 不可變切片

    // cannot borrow `s` as mutable because it is also borrowed as immutable
    let mut world = &mut s[6..11]; // 可變切片

    world.make_ascii_uppercase();
    println!("{}, {}", hello, world);
}

從以上這個例子來看在編譯前 IDE 就已經提示了一樣不可同時借出為可變與不可變,看來就算是切片也是需要遵守「一次只能有一個可變或多個不可變參考」這件事。

所有權系統的小結

今天算是初步將 Rust 中的整個所有權系統入門的內容走過一遍了,以下來做個總整理:

  • 什麼是所有權系統
    • Rust 實現記憶體安全 (memory safety) 和無懼並行 (fearless concurrency) 特色的核心機制
  • 特色:
    • 不需要像 C/C++ 這種需要手動管理記憶體的語言一樣,要用 mallocfreenewdelete 等方法來操作
    • 在編譯中防止常見的記憶體錯誤像是 dangling references、double free 等
    • 透過所有權鐵則,在不需要仰賴 GC (garbage collector) 的機制自動釋放記憶體
    • 透過所有權機制,明確在程式上能理解誰擁有所有權、誰只是借用參考值

系列文總結

今天也終於來到這個系列的最後一篇了,雖然說這個系列的主題叫做「Rust 的戰國時代:探索網頁前端工具的前世今生」,但嚴格說起來我覺得前後算是可以拆開講的兩個主題,比較理想上是可以在具備 Rust 開發能力後,直接切入「如何用 Rust 來開發前端工具」這件事,但一方面也是因為我是這次才開始初學 Rust,所以最後變成前面在探討關於「網頁前端工具發展史與原理」,以及最後的「為什麼這些工具要被 Rust 化,來入門了解一下 Rust 吧」。

雖跟原本想寫的樣子長得不太一樣,但也不算是完全沒有收穫,過程中更深入地理解了 JS 模組化的歷史脈絡、Vite 工具的原理、Rust 新工具們的介紹與使用、Rust 語言的入門特性。

因為前面是無存稿參賽採用研究到哪寫到哪的方式,這裡也回頭重新編排一下跟補上 Day 1 中的這整個系列文的大綱,方便有興趣參考此系列學習的讀者一個地圖:

前言

網頁前端工具的前世今生

Vite 原理與實驗

Rspack 實驗

Rust 入門學習筆記

關於 Rust 雖然還有許多觀念沒提到,像是生命週期、泛型、智慧指標、巨集、跨執行緒的無懼並行操作等等,有興趣深入學習的讀者,可以再參考中文版的 The book 手冊以及龍哥的《為你自己學 Rust》都很精彩,以及想用影片學習的話也有影片版的 The book 手把手教學,或不知道未來還有沒有繼續更新這個系列,我這裡放個 //TODO 先。


完賽心得

其實我幾年前也參加過 3 屆的鐵人賽,但不得不說這次是最硬也最曲折的一次。

先說說整個系列文最難的部份是前面幾天的前情提要,發現有許多我大概理解,但真的要講的淺顯易懂不知為何總是刪刪改改,也看了不少 JS 模組化、webpack 的經典文章,總覺得「這些東西是不是其實不需要再寫了」因而寫起來不夠順暢,總覺得寫了的話可能也沒辦法超越,看起來也像是模仿。但後來想想其實我也不需要超越,就只是用我理解的話去整理脈絡,可能總有人看了十幾篇文章後與我這篇對頻就有一些幫助,更甚至沒人看也罷,秉持著「為你自己學」的這個概念,學習筆記有輸出就是有學會一些東西,筆記終究是寫給自己看,就不用想太多了。

而這次因為是臨時一股衝動壓線參賽,在無存稿的狀況下每天趕稿,就連後面幾天剛好人在日本獨旅中,原本樂觀地認為可以趕在出國前把 30 篇完成,但果然還是太高估自己的能耐,或說其實也是自己不想以能應付了事的內容勉強達標就好的緣故吧。所以旅遊的前幾天都是吃完午餐就得找咖啡廳寫一些內容、甚至中間轉車時利用一些空檔做收尾等,也算是微體驗所謂的數位遊牧是怎麼一回事,親身體驗到了那種「雖然想繼續在這個景點逛下去,但得先完成工作」、「現在沒有工作的心情,那不如先去看看風景晚上再來用咖啡因趕工吧」這些經驗。

train

而原以為照著這樣的節奏可以順利完賽,但人生就是這個 BUT。

一直到 Day27 當天出了些小意外,一早去海邊看日出時不慎滑倒右手就這樣去急診縫了十幾針,救護車上還在想「鐵人賽什麼的應該也不重要了吧,健康要緊」。但好在後來當天狀況比較穩定後,還是勉強用左手完成了當天的文章 (誰說左手只是輔助)。隨著恢復越來越好現在寫第 30 篇時已經能用上九隻手指頭了,所以才能寫這麼長一篇 😅,對我自己來說可以說是最鐵人的一次了吧,而今天寫完這篇後也終於可以把筆電放在飯店安心旅遊了。順帶一提真心建議獨旅時不管怎樣都要保個保險,關於這次獨旅的心得可能之後沒有冨樫再來另外寫篇文章記錄下吧。

最後最首要感謝的就是我的女友,包容我這樣任性地壓線參賽每天趕稿,希望在這三十天無存稿的試煉後,除了深刻體驗到事前規劃的重要性之外,我們也終於可以一起無憂無慮地一次把黑白大廚給追完了。

另外也感謝在 IG、Threads 上打卡時,偶爾會收到來自大家的不論是回饋、討論、溫暖的支持、同為水深火熱的戰友的互相打氣;也感謝過去寫過關於這系列中文文章的前人們,讓我能搭配著做為參考資料交叉理解。在研究 Vite 的原始碼的過程中,首推去年鐵人賽由菜市場阿龍錄製的這一系列《Vite 原始碼解讀》教學影片,在兩三天內用 1.5 倍速一口氣看完全系列外,讓我回憶起以前大學在期末考前用 2 倍速惡補預錄課程的美好過去。另外他的《為你自己學 Rust》也是比起官方的 The book 用更有脈絡的初學角度敘述非常好讀,在看的過程中都懷疑他去年是不是其實原本打算實作一套 Rust 版的 bundler 所以才會有雙開這樣的主題。

vite

幕後花絮之 Day12 突發奇想的梗圖

總結

說實在的在寫這個系列的過程中,有時候會懷疑我學這個做什麼?為什麼要鑽那麼深?因為其實每過幾年新技術、工具就會推陳出新,但當原本當紅的技術相對過時,身為開發者的我來說能留下的是什麼?我目前想到最好的答案應該是在「Learning How to Learn」吧。

面對不斷更替的新工具、新語言,如果都能有這樣的 flow 對我而言是個不錯的學習系統:

  • 實際弄髒手去寫一個 hello world
  • 照著官方文件去實作一些簡單專案
  • 實驗一些有疑問的問題去 debug
  • 過程中善用 AI 工具幫忙釐清觀念並回頭查文件驗證
  • 最後再去參考文件把專有名詞與觀念補上
  • 心有餘力能深入追原始碼理解原理
  • 甚至能在遇到 tricky issue 時對開源專案發 PR

久而久之就能建立一個適合自己的學習系統,之後學類似的工具或語言也會快上不少。

memory

就像這系列中間有花了些時間去追 Vite 原始碼,過程中也透過一些實驗應證工具的效能與瓶頸,後來再去看另一套類似的 Rsbuild 的原始碼時,看起來也是相當親切,因為概念上大同小異只是底層用上了 Rust 而已。或像最後的 Rust 初學,在寫完前面 20 多篇時,我其實一行 Rust 都還沒寫過。但最後循著許多入門教材、官方教學手冊、穿插著影片教學,最後竟然還能學到把記憶體架構跟所有權系統整理出一個初步的樣子,雖然尚稱不上完美,但至少是一個開始。

廢話不小心太多了,該去門司港吃燒咖哩不然等等又要準備吃 7-11 了,這個系列文就在這劃下一個完美的句點,希望這個系列有讓看到這邊的你多少有一些收穫。有想要做更多討論與交流的讀者們歡迎到 IG 上可以更即時聯絡到我,也感謝你的閱讀!

landscape


上一篇
Day 29:Rust 中的所有權 (Ownership) 是什麼?(2)
系列文
Rust 的戰國時代:探索網頁前端工具的前世今生30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中
0
ysl0628
iT邦新手 3 級 ‧ 2024-10-14 14:25:31

恭喜恭喜!

感謝 Renee!

0
Alex Liu
iT邦新手 4 級 ‧ 2024-10-14 15:00:23

恭喜完賽!
真心覺得這個系列超強,收穫真的非常多,而且對於新資訊追得好快,佩服。

是 Alex 大!感謝你的肯定也恭喜你前幾天完賽,看你整個系列準備了五個月才是真的猛,而且完全是我不擅長的 UI library 領域,看到最後那個 AtomicRating 的細緻度才是跪了

0
Monica
iT邦新手 5 級 ‧ 2024-10-14 21:42:29

恭喜完賽!!!很豐富的內容還正在追文章中~

感謝 Monica 的肯定!/images/emoticon/emoticon41.gif

0
Min
iT邦新手 4 級 ‧ 2024-10-14 23:00:34

恭喜完賽!辛苦了!

感謝 Min 大!你也辛苦了,明天終於可以放假了 /images/emoticon/emoticon42.gif

我要留言

立即登入留言