[Rust] Rust基礎(1)
Adrian Chen

極不建議以Rust來進行程式設計入門,如你所見——它的一些概念是獨有的,適合老人們來進階提升自己,而不是新手來入門。它非常難,恐怕並不會讓新進人士感受到程式設計的美好。
參考書目:《Rust語言聖經》(簡體)《Rust程式設計語言》(繁體)

變數

變數綁定

在Rust中,我們對於變數的宣告不常用“指定”(台灣)或“賦值”(中國大陸)這兩個詞語,而是使用“綁定”。比如我們使用:

1
let a = 1;

為什麼要創造出這樣一個新的詞語呢?這就涉及到Rust中特有的概念和最核心的原則——所有權。簡單來講,任何一個物件都有“主人”,一般情況下,該物件完全屬於它的主人。那麼綁定的過程,就是給這個物件找主人的過程。比如上面的表達式,就是給1這個物件找到一個主人,是一個叫a的變數。

關於所有權的概念,我們後續再討論,這暫時不重要,讀者可以暫時忘記它。

但是接下來的事情,你就不該忘了:Rust的變數預設是不可變的!也就是說,上面的let a = 1;宣告的變數a,其實像一個常數⋯⋯

那有沒有什麼辦法宣告一個可變的變數呢?有,只需要加上mut關鍵字⋯⋯

1
2
3
4
5
// variable
let mut a = 1;

// immutable variable
let b = 2;

所以,使用Rust的煩惱之一,就是你應該如何選擇變數的可變性。不可變變數帶來更高的安全性,但是代價是靈活性。而可變變數最大的優點就是靈活性和性能,但是其安全性大打折扣。後面你會明白的,安全性在Rust中極其重要。

我們可以使用底線來忽略未使用的變數。在Rust中,宣告的變數如果不被使用,會爆出一個warning,但是如果使用底線作為變數名字的開頭,就會自動忽略這個不被使用的變數。

1
let _a = 1;

說完變數,我們再來說一說常數。這時候你就瘋了,不是說變數預設不可變嗎?不是說不可變的變數就像常數嗎?怎麼又冒出一個常數?別急,我們來解釋它們的不同。

首先,常數沒有mut關鍵字。也就是說,常數不是預設不可變,而是永遠、在任何時候都不可變。

其次,常數在宣告的時候必須指名其型別。Rust不會幫助你推斷常數的型別。

再來,常數只能夠透過常數表達式來設置,而不能透過任何運行中產生的可變數來設置。

常數用const關鍵字來宣告,且變數名稱建議使用全大寫字母,單詞中間用底線來連結。下面是一個常見的常數宣告:

1
const CONST_NUM: u32 = 1;

變數遮蔽

我們可以重新宣告一個已經宣告過的變數,原來的變數對應的值就被“遮蔽”,該變數的值變成新值,直到新變數的scope結束。

比如:

1
2
3
4
5
let x = 1;
let x = "Hello, world.";

println!("{}", x);
// Result: Hello, world.

好,你又瘋了,這和改變變數的值有什麼分別?

別急!我們來解釋解釋。使用mut宣告的變數,改變變數的值不能改變變數的類型。而使用這種方法改變的值事實上不是改變值⋯⋯

別繞!我們看這張圖,或許你就明白了:

image2

使用變數遮蔽,事實上是在記憶體中,又開闢了一塊空間,放上我們的新物件,然後將這個變數指向這塊記憶體空間。所以,這種方式不僅可以在形式上改變變數的值,還可以改變變數的型別

資料型別

Rust是強型別語言,儘管我宣告變數的時候好像並沒有特意去指定型別(當然指定也沒有錯誤),那是因為Rust的編譯器會聰明地幫助我們推測型別。Rust中使用:來跟上型別,和TypeScript、Kotlin都類似。

數字型別

整數

數字是最基本的型別,我們有整數型別:

長度 有符號 無符號 有符號範圍
8 i8 u8
16 i16 u16
32 i32 u32
64 i64 u64
128 i128 u128
視CPU而定 isize usize 32位元CPU:;64位元CPU:

Rust預設的整數型別是i32

如果整數有溢出,Rust的處理也非常有意思。在debug模式下,Rust編譯器監測到整數溢出,會導致程式panic,然而在release模式下,不會導致panic,但是會按照“二進位補碼換行”的方式來處理。比如一個u8類型,只能夠儲存從0至255之間的數字,如果儲存256,就會把256變成0,257變成1,依此類推,程式不會panic,但是此類程式碼仍應該被視作錯誤的程式碼。

浮點數

f32f64,分別為單精度和雙精度,預設是f64

關於浮點數陷阱,相信大家學C語言的時候就已經深刻了解到了,這裏不再贅述了。

關於數字的運算,包括加減乘除模,還有位運算,我就不多介紹了。

序列化。Rust提供了一個非常簡單的方法來實現變數序列化。比如:

1
2
3
4
5
1..5 // 1~4(不包括5)的數字序列

1..=5 // 1~5的數字序列

'a'..='z' // a~z的字母序列

字元、布林

字元的型別標識為char。和許多現代程式設計語言一樣,Rust使用Unicode編碼系統,這很方便。

布林的型別標識為bool,取值truefalse

單元

Rust特有的一個型別,只有一個取值,就是()。這東西能作為一個型別我是沒有想到的。所以它有什麼用?

佔位。

對,它的作用基本等同於佔位,佔一個顯式的位置,但是並不耗用任何記憶體位置。比如在Rust中,返回()的函式和沒有返回值的函式(發散函式)是完全不同的概念。這些我們下面再介紹。又比如,你可以使用()作為map的值,代表我們不關注具體的值,只關注key。

元組

1
let tup: (i32, f64, u8) = (500, 6.4, 1);

元組一旦被宣告,長度無法被改變。

元組是一個復合型別,可以透過“元組解構”來取到裡面的值。

1
2
let (x, y, z) = tup;
println!("The value of y is: {y}");

或者透過index來拿到:

1
2
println!("{}", tup.0);
// Result: 500

陣列

陣列(array)在Rust中也是不可以改變長度的,且其中資料的型別必須相同。

1
2
3
let arr = [1, 2, 3, 4, 5];

println!(arr[0]);

如果你想顯式表明array的型別,需要這樣做:

1
2
// 分號前面的i32表示陣列元素的型別,5表示陣列元素的個數
let arr: [i32; 5] = [1, 2, 3, 4, 5];

創造一個所有元素均相同的陣列:

1
2
// 會宣告一個含5個整數3的陣列
let arr = [3; 5];

函式

我們來看一個最簡單的函式,傳說中的add函式:

1
2
3
fn add(i: i32, j: i32) -> i32 {
i + j
}

我們來逐步看:

  • fn關鍵字:表明這是一個函式。
  • add:函式名稱,命名規則基本同其他。
  • 引數:必須顯式宣告引數的型別!去掉任意引數的型別都會出錯。
  • -> i32:回傳值是i32型別。
  • i + j:計算並回傳。

如果你習慣了其餘程式語言,你可能會驚奇地發現,這個函式沒有return陳述式。

這個時候我們就要引出Rust中另外兩個需要區分的概念——陳述式和表達式。

於是你迄今為止第三次瘋了:OMG!為什麼這兩個概念還需要區分😷⋯⋯

我們來看一下:

  • 陳述式:結尾有分號;,只進行計算,而不會回傳任何值。
  • 表達式:結尾沒有分號;,進行計算,並且回傳計算後的結果。

所以,我們上面的函式以一個表達式做結尾,它本身就是有回傳值的⋯⋯

對於表達式,還可以進行這樣的操作:

1
2
3
4
5
6
7
8
fn main() {
let y = {
let x = 3;
x + 1
}; // 括弧裡面的程式碼回傳值作為變數y的綁定值

println!("The value of y is: {}", y);
}

真的,我知道這很逆天,但Rust已經逆天太多次了,不是嗎?

好,我們回到函式的回傳值。

得益於元組的出現,函數可以有“多個回傳值”。

1
2
3
fn com_fun(i: i32, j: i32) -> (i32, i32) {
(i + j, i - j)
}

在其他程式語言中,我們有回傳void型的權利,在Rust中,你當然也有。不過Rust在這方面居然也能玩出一點花樣。

還記得我們上面說的單元()嗎?你可以透過回傳單元,來達到類似的效果。

1
2
3
4
5
6
7
fn void_fun() {
println!("Hello, world."); // 如果函式體內最後一行是陳述式,那麼函式會預設隱式回傳()
}

fn void_fun_2() -> () { // 顯式回傳()
println!("Hello, world.");
}

對於無回傳值的函式,Rust還有一個永不回傳的函式——發散函式,回傳類型為!

1
2
3
fn err -> ! {
panic!("Never again.")
}

那麼發散函式和普通無回傳值的函式有什麼區別?

在Rust中,發散函式通常被用作程式panic的信號。

發散函式一般以panic!()結尾,表示程式出現panic,呼叫該函式之後,程式就panic了,之後的任何程式碼都不會被執行。

1
2
3
4
5
6
7
8
9
fn main() {
println!("start work!");
mydiverging();
println!("unreachable println!");
}

fn mydiverging() -> ! {
panic!("never return");
}

後面的println!("unreachable println!");永遠不會被執行,因為這時候程式已經panic了。

流程控制

條件

說到條件控制,if-else系統當然是繞不開的話題。

1
2
3
4
5
6
let number = 3;
if number < 5 {
println!("條件為真");
} else {
println!("條件為否");
}

還有else if的應用:

1
2
3
4
5
6
7
8
9
10
11
let number = 6;

if number % 4 == 0 {
println!("數字可以被 4 整除");
} else if number % 3 == 0 {
println!("數字可以被 3 整除");
} else if number % 2 == 0 {
println!("數字可以被 2 整除");
} else {
println!("數字無法被 4、3、2 整除");
}

if-else和Kotlin一樣,也可以被視作有回傳值的,用於代替C語言中的三元運算子?:

1
2
let condition = true;
let val = if condition { 5 } else { "六" }

迴圈

在Rust中,除了常規的while、for迴圈之外,還有一個loop迴圈。我們一一來看。

loop迴圈勇於創造一個類似無限迴圈的迴圈,但是你可以在迴圈中要求它停下來:

1
2
3
4
5
6
7
8
let mut i = 0;
loop {
println!("Hello");
i += 1;
if i > 10 {
break
}
}

啊是的,breakcontinue關鍵字仍然在Rust中得到延續。

但是!我又要說但是了。這個break雖然基本的用法沒有什麼差,但是Rust還是给它了一點新的用法。

因為,迴圈在Rust中是可以有回傳值的

喔上帝,我知道這很逆天,但是⋯⋯算了,想來你也已經習慣了。

我們直接來看好了:

1
2
3
4
5
6
7
8
9
10
11
12
13
fn main() {
let mut counter = 0;

let result = loop {
counter += 1;

if counter == 10 {
break counter * 2;
}
};

println!("結果為:{result}");
}

你可以在break後面接上想回傳的數值,你就會在loop被break掉之後,把這個值綁定給外面等待已久的變數。

關於while迴圈,基本用法已經很熟悉了,沒有什麼變化,我就不多說了。

而重頭戲for迴圈,實際上也是for-in迴圈:

1
2
3
4
5
let a = [10, 20, 30, 40, 50];

for element in a {
println!("數值為:{element}");
}

所有權

所有權的概念可能是Rust最具特色的一點,也是一個新的概念,需要花時間去掌握。

從一開始接觸程式設計,我們就被告知,程式的執行離不開和記憶體打交道。比如最基本的變數的概念,我們就可以看作是一塊記憶體空間。

程式從記憶體中申請空間來存放資料,那我們如何在不使用的時候釋放這些記憶體空間,成為了每個程式設計語言的重點和難點。目前流行的程式設計語言在這個問題上主要分為三個流派:

  • 垃圾回收機制(GC):程式運行時不斷尋找不需要的記憶體。典型代表是Java、Go。這種管理模式優點是對程式設計人員十分友好,缺點在於可能犧牲一些靈活性和性能。
  • 手動管理:在程式中透過函式來手動管理記憶體的使用和釋放。典型代表是C++。這種管理模式十分靈活,能夠讓開發人員隨心所欲地控制記憶體的使用,但是相對比較複雜。
  • 所有權:Rust獨特的模式,在編譯階段使用一系列規則進行檢查。由於這種檢查只發生在編譯階段,因此在運行時不會有任何的性能損失。

我們正式開始預熱。

堆疊(Stack)和堆積(Heap)

堆疊和堆積這兩個資料結構概念在中國大陸被稱為“棧”和“堆”,是兩種最基本的資料結構。

堆疊(Stack)類似於一疊盤子,我們最後放到頂上的盤子在取走的時候將會最先被取走。這種模式被稱為 “後進先出”。當我們要新增資料時,我們會稱呼為推入堆疊(pushing onto the stack),而移除資料則是叫做彈出堆疊(popping off the stack)。所有在堆疊上的資料都必須是已知固定大小。在編譯時屬於未知或可能變更大小的資料必須儲存在堆積。

堆積(Heap)就像一個雜亂無章的雜物堆,裡面的雜物有大有小,位置也不固定。當我們把資料放入堆積的時候,我們需要向記憶體申請一塊一定大小的空間,記憶體配置器(memory allocator)會找到一塊夠大的空位,標記為已佔用,然後回傳一個指標(pointer),指著該位置的位址。這樣的過程稱為在堆積上配置(allocating on the heap),或者有時直接簡稱為配置(allocating)就好。

所有權三原則

  1. Rust中每個物件都有主人。
  2. 同一時間一個物件只能有一個主人。
  3. 當主人離開作用域時,物件就會被丟棄。

作用域

一個變數的作用域就是該變數有效的範圍。當一個變數進入其作用域,它就是有效的,這種有效性應該一直持續到它離開作用域為止。

下面我們將以String型別來做範例,講解所有權的概念。

String型別

在Rust中,透過兩種方法創建字串是十分容易的。

第一種是透過字串字面值的方式來建立,這種方式較為簡單,但是建立的字串變數不可改變。

1
let s = "Hello, world.";

第二種方法是透過String型別來建立。這種方式建立的字串變數是可以被改變的,其在記憶體中的大小不固定,被放在堆積裡。

1
2
3
let s = String::from("Hello,");

s.push_str(", world.");

當我們使用這個方式創造出一個字串的時候,我們需要考慮兩件事情:

  1. 記憶體配置器需要向記憶體申請一塊空間。
  2. 不再需要這個字串時,我們需要以某種方法將此記憶體還給配置器。

當我們呼叫String::from時就等於完成第一個部分,它的實作會請求配置一塊它需要的記憶體。這邊大概和其他程式語言都一樣。

但是第二點有所不同。在具有GC機制的程式設計語言中,這件事情是由GC來幫我們做的,我們無須太過關注。而在手動操作的程式設計語言中,我們需要手動來進行這件事。

Rust選擇的是第三條道路——所有權。具體來說,每一塊記憶體空間都有它的“主人”,即變數。根據原則三,如果變數離開了它的作用域,那麼該記憶體空間就會被釋放。

1
2
3
4
5
6
7
8
9
fn main() {
{
let s = String::from("hello"); // s 在此開始視為有效

// 使用 s
} // 此作用域結束

// s 不再有效
}

事實上,在作用域結束後,Rust會自動為我們呼叫一個叫做drop的內建函式,釋放記憶體。

移動

我們來看一下下面的例子:

1
2
let x = 5;
let y = x;

發生了什麼呢?首先,我們在記憶體中開闢了一塊固定大小的空間(因為預設的i32型別是基本型別,大小固定)推入堆疊中,並且將它交給主人x。然後我們把這塊記憶體中的數字5拷貝一份,把這份拷貝交給新的主人y。

這似乎是一件順理成章的事情。然而,如果涉及到堆積中儲存的資料問題,可就不僅僅這麼簡單了。

在詳細講到之前,我們先來理解兩個重要的概念——拷貝(淺拷貝)和克隆(深拷貝)。

我們剛剛講到,存放在堆積中的資料會有一個儲存在堆疊中的指標來指向它。

  • 拷貝(淺拷貝):僅把堆疊中的指標拷貝一份推入堆疊,不拷貝堆積中的資料。堆疊中的新指標依然指向堆積中的原位置。
    淺拷貝

  • 克隆(深拷貝):不僅拷貝堆疊中的指標,還拷貝堆積中的資料。堆疊中的新指標指向的是堆積中的新資料。
    深拷貝

好,完成了知識的前置之後,我們仍然以String為例來進行說明。

1
2
3
let s_1 = String::from("Hello, world.");

let s_2 = s_1;

發生了什麼呢?事實上,我們是將堆疊中指向堆積的指標拷貝了一份推入堆疊,指向堆積中之前的資料。

那麼讀者可能就會說了,這不就是淺拷貝嗎?

是,也不是。拜託,你現在是在學Rust誒,按照這門語言在前面給你的震撼,還有什麼是不可能發生的事情嗎?

我們看一下如果常規的淺拷貝,在Rust中會產生什麼問題。

當變數s_1退出它的作用域之後,儲存在堆疊中的指標性資料會被drop掉,同時drop掉的還有儲存在堆積中的實際資料。

而後,變數s_2退出作用域,在drop掉堆疊中的指標性資料之後,忽然發現,本應該緊接著被drop的堆積中之實際資料,忽然沒有了!

這會造成一個error:雙重釋放(double free)。

所以,Rust實際上有幫我們又做了一件事情。當我們使用let s_2 = s_1;s_1拷貝到s_2之後,Rust就會講原來的s_1不再視為有效了,s_1退出作用域後,不會釋放任何東西。我們來求證一下:

1
2
3
4
5
6
fn main() {
let s1 = String::from("hello");
let s2 = s1;

println!("{}, world!", s1);
}

你會發現編譯出錯了。錯誤資訊是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0382]: borrow of moved value: `s1`
--> src/main.rs:5:28
|
2 | let s1 = String::from("hello");
| -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
3 | let s2 = s1;
| -- value moved here
4 |
5 | println!("{}, world!", s1);
| ^^ value borrowed here after move
|
= note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)

For more information about this error, try `rustc --explain E0382`.
error: could not compile `ownership` due to previous error

它告訴我們,s_1已經被“moved”。

是的,在Rust中,對於儲存在堆積中的資料的預設拷貝方式,既不是淺拷貝,也不是深拷貝,而是一種獨特的被稱作“移動”的方式。

那如果你確定了,你就是想要做一個深拷貝,不在意性能什麼的,你也可以使用一個方法,名叫clone()

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

這樣就完成了一次完整的深拷貝。

誒,這時候有的讀者可能就要問了。那為什麼儲存在堆疊中的基本資料,預設可以直接拷貝值再交給新的變數呢?

原因是因為像整數這樣的型別在編譯時是已知大小,所以只會存在在堆疊上。所以要拷貝一份實際數值的話是很快的。這也讓我們沒有任何理由要讓 x 在 y 建立後被無效化。換句話說,這邊沒有所謂淺拷貝與深拷貝的差別。

Rust中有一個特有的叫做“Copy”的徽記,用來標識那些型別的資料可以進行這樣的拷貝。如果一個型別有 Copy 特徵的話,一個變數在賦值給其他變數後仍然會是有效的。比如:

  • 所有整數型別像是 u32。
  • 布林型別 bool,它只有數值 true 與 false。
  • 所有浮點數型別像是 f64。
  • 字元型別 char。
  • 元組,不過包含的型別也都要有實作 Copy 才行。比如 (i32, i32) 就有實作 Copy,但 (i32, String) 則無。

這些都是可以擁有Copy徽記的。

所有權和函式

傳遞參數給函式,會是移動或拷貝,就像賦值一樣。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
fn main() {
let s = String::from("hello"); // s 進入作用域

takes_ownership(s); // s 的值進入函式

// 所以 s 也在此無效

let x = 5; // x 進入作用域

makes_copy(x); // x 本該移動進函式裡

// 但 i32 有 Copy,所以 x 可繼續使用

} // x 在此離開作用域,接著是 s。但因為 s 的值已經被移動了,因此它不會有任何動作。由於 x 是基本型別,因此會發生銷疊。

fn takes_ownership(some_string: String) { // some_string 進入作用域
println!("{}", some_string);
} // some_string 在此離開作用域並呼叫 `drop`
// 佔用的記憶體被釋放

fn makes_copy(some_integer: i32) { // some_integer 進入作用域
println!("{}", some_integer);
} // some_integer 在此離開作用域,發生銷疊。

回傳值會被函式移動到外面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
fn main() {
let s1 = gives_ownership(); // gives_ownership 移動它的回傳值給 s1

let s2 = String::from("哈囉"); // s2 進入作用域

let s3 = takes_and_gives_back(s2); // s2 移入 takes_and_gives_back
// 該函式又將其回傳值移到 s3
} // s3 在此離開作用域並釋放
// s2 已被移走,所以沒有任何動作發生
// s1 離開作用域並釋放

fn gives_ownership() -> String { // gives_ownership 會將他的回傳值

// 移動給呼叫它的函式

let some_string = String::from("你的字串"); // some_string 進入作用域

some_string // 回傳 some_string 並移動給

// 呼叫它的函式
}

// 此函式會取得一個 String 然後回傳它
fn takes_and_gives_back(a_string: String) -> String { // a_string 進入作用域

a_string // 回傳 a_string 並移動給呼叫的函式
}

參考、借用

我們來看一下這個例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
fn main() {
let s1 = String::from("hello");

let (s1, len) = calculate_length(s1);

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

fn calculate_length(s: String) -> (String, usize) {
let length = s.len(); // len() 回傳 String 的長度

(s, length)
}

似乎只有這樣,才能夠讓原始的String在往函式裡面走一圈之後,還能夠換個皮(變數遮蔽)繼續使用。

然而,這樣轉過來轉過去,實在是有夠麻煩的,那麼又沒有什麼更加簡單的方法呢?

有。Rust為我們提供了“參考”。參考(references)就像是指向某個地址的指標,我們可以追蹤存取到該處儲存的資訊,而該地址仍被其他變數所擁有。和指標不一樣的是,參考保證所指向的特定型別的數值一定是有效的。

使用參考來實現上面的內容:

1
2
3
4
5
6
7
8
9
10
11
fn main() {
let s1 = String::from("hello");

let len = calculate_length(&s1);

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

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

我們將引數s的類型從String變成了&String,表示這個引數的型別是一個String的參考。然後傳入一個String型別的變數的參考&s1

變數s有效的作用域和任何函式參數的作用域一樣,但當不再使用參考時,參考所指向的數值不會被丟棄,因為我們沒有所有權。

我們會稱呼建立參考這樣的動作叫做借用(borrowing)。

借用的參考,你不能夠修改它。如果你想要修改的話,你可以使用“可變參考”。

1
2
3
4
5
6
7
8
9
fn main() {
let mut s = String::from("hello");

change(&mut s);
}

fn change(some_string: &mut String) {
some_string.push_str(", world");
}

改變參考,會影響原來的變數一起改變。 比如經歷過去上面change()函式中走一遍,字串s的值也會改變。

可變參考有個很大的限制:如果你有一個數值的可變參考,你就無法再對該數值有其他任何參考。所以嘗試建立兩個 s 的可變參考的話就會失敗。比如下面:

1
2
3
4
5
6
7
8
fn main() {
let mut s = String::from("hello");

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

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

但是你一旦把第一個參考用掉,那麼第二個參考就可以被激活,同時第一個參考失效(參考的作用域結束於其最後一次被使用的地方):

1
2
3
4
5
6
7
8
9
fn main() {
let mut s = String::from("hello");

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

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

聽著,我知道這很逆天。但還有一點:你也不能夠同時持有一個變數的可變參考和不可變參考。

好!收尾。既如此,我們來總結一下借用適用的場景:

  • 只需要在函式中唯讀參數,不改變參數,使用不可變參考。
  • 需要在函式中修改參數,並且希望修改之後的結果反映在原來的變數上,使用可變參考。

發現問題了沒有?似乎我們更常見的需求是:我們既需要在函式內修改原來的參數,而且不希望修改的參數影響到原來的變數。

這樣的需求,使用借用就沒有辦法了。那麼有其他的辦法嗎?

當然有。還記得我們之前講過的深拷貝嗎?深拷貝是將原字串完全拷貝一份,相當於創建了兩個完全不同的變數了。

1
2
3
4
5
6
7
8
9
10
11
fn process_string(mut s: String) -> String {
s.push_str(" (已修改)");
s // 回傳修改後的副本
}

fn main() {
let my_string = String::from("Hello, Rust!");
let modified_string = process_string(my_string.clone()); // 傳入my_string的深拷貝
println!("函式內處理後的字串:{}", modified_string);
println!("函式外原字串:{}", my_string); // my_string 不變
}

切片

切片是一種特殊的參考,可以參考一串集合中的元素序列,而並非參考整個集合。

比如:

1
2
3
4
5
6
fn main() {
let s = String::from("hello world");

let hello = &s[0..5]; // 參考為hello
let world = &s[6..11]; // 參考為world
}