原文連結:https://hashrust.com/blog/arrays-vectors-and-slices-in-rust/
原文標題:Arrays, vectors and slices in Rust
公眾號:Rust 碎碎念
翻譯: Praying
引言(Introduction)
在本文中,我將會介紹 Rust 中的 array、vector 和 slice。有 C 和 C++程式設計經驗的程式設計師應該已經熟悉 array 和 vector,但因 Rust 致力於安全性(safety),所以與不安全的同類語言相比仍有一些區別。另外,slice 是一個全新且非常有用的概念。
Array
Array 是初學者最先接觸的資料型別之一。一個 array 是一組相同型別的資料集合,這些資料位於連續的記憶體塊中。例如,如果你像下面這樣分配一個陣列:
let array: [i32; 4] = [42, 10, 5, 2];
接著,所有的i32
整數在棧上緊挨著彼此被分配:
在 Rust 中,array 的大小(size)是型別的一部分。例如,下面的程式碼將無法編譯:
//error:expected an array with a fixed size of 4 elements,
//found one with 3 elements
let array: [i32; 4] = [0, 1, 2];
Rust 的嚴謹性避免了像 C/C++中的陣列到指標的衰變問題:
//C++ code
#include <iostream>
using namespace std;
//Looks can be deceiving: arr is not a pointer
//to an array of 5 integers. It has decayed to
//a pointer to an integer.
void print_array_size(int (*arr)[5]) {
//prints 8 (the size of a pointer)
cout << "Array size in print_array_size function: " << sizeof(arr) << endl;
}
int main()
{
int arr[5] = {1, 2, 3, 4, 5};
//prints 20 (size of 5 4-byte integers)
cout << "Array size in main function: " << sizeof(arr) << endl;
print_array_size(&arr);
return 0;
}
print_array_size
函式列印出了 8 而不是期望的 20(5 個整數,每個整數 4 位元組),因為arr
已經從一個指向包含 5 個整數的陣列(array)的指標衰退為指向一個整數的指標。相似的程式碼在 Rust 中能夠正確執行:
use std::mem::size_of_val;
fn print_array_size(arr: [i32; 5]) {
//prints 20
println!("Array size in print_array_size function: {}", size_of_val(&arr));
}
fn main() {
let arr: [i32; 5] = [1, 2, 3, 4, 5];
//print 20
println!("Array size in main function: {}", size_of_val(&arr));
print_array_size(arr);
}
C/C++和 Rust 在 array 上的另一個區別是,在 Rust 中訪問元素會進行邊界檢查。例如,在下面的 C++程式碼中,我們試圖訪問一個大小為 3 的 array 中的第 5 個元素,這導致了未定義行為[1]:
#include <iostream>
using namespace std;
int main()
{
int arr[3] = {1, 2, 3};
const auto index = 5;
//arr[index] is undefined behaviour
cout << "Integer at index " << index << ": " << arr[index] << endl;
return 0;
}
而類似的程式碼在 Rust 中則會 panic:
fn main() {
let arr: [i32; 3] = [1, 2, 3];
let index = 5;
//arr[index] panics with the following message:
//index out of bounds: the len is 3 but the index is 5
println!("Integer at index {}: {}", index, arr[index]);
}
你可能想知道,Rust 版本的程式碼怎麼就比 C++版本的程式碼好了?因為 C++版本的程式碼表現出未定義行為,它給了編譯器一個不受限制的許可,允許編譯器以優化的名義來做任何事。在最糟糕的情況下,這可能會把資訊洩露給攻擊者。
與之相對,Rust 版本的程式碼總是會 panic。此外,因為程式由於 panic 而終止,程式設計師更有可能注意並修復這個 bug。相反,C++把問題掩蓋起來並且程式可以像什麼事都沒發生一樣繼續執行。比起 C/C++的未定義行為,我寧願使用 Rust 的 panic。
Vector
Array 最大的一個限制是它的固定大小。與之相對,vector 可以在執行時擴容:
fn main() {
//There are three elements in the vector initially
let mut v: Vec<i32> = vec![1, 2, 3];
//prints 3
println!("v has {} elements", v.len());
//but you can add more at runtime
v.push(4);
v.push(5);
//prints 5
println!("v has {} elements", v.len());
}
Vector 是如何做到在執行時擴容的呢?在其內部,vector 把所有的元素放在一個分配在堆(heap)上的 array 上。當一個新元素被 push 進來時,vector 檢查 array 是否有足夠的剩餘空間。如果空間不足,vector 就分配一個更大的 array,將所有的元素都拷貝到這個新的 array 中,然後釋放舊的 array。這可以在下面的程式碼中驗證:
fn main() {
let mut v: Vec<i32> = vec![1, 2, 3, 4];
//prints 4
println!("v's capacity is {}", v.capacity());
println!("Address of v's first element: {:p}", &v[0]);//{:p} prints the address
v.push(5);
//prints 8
println!("v's capacity is {}", v.capacity());
println!("Address of v's first element: {:p}", &v[0]);
}
最開始,v
內部的 array 容量(capacity)為 4:
接著,一個新元素被 push 到 vector 中,這使得 vector 把所有元素拷貝到一個新的容量為 8 的內部 array 中:
上面這段程式碼還會列印出,在放入一個元素之前和放入之後,vector 裡的 array 中的第一個元素的地址。這兩個地址會互不相同。地址的變化清楚地證明了其幕後分配了一個容量為 8 的新 array。
如果你在 vector 中 push 進了一個元素但是卻沒有看到不同的地址,這可能是因為原始的 buffer 尾部還有足夠的空間,因此新舊 buffer 擁有相同的起始地址。嘗試 push 更多的元素,你就會看到不同的地址。閱讀 C 的庫函式
realloc
來理解這是如何運作的。
Slice
Slice 就像一個 array 或 vector 的臨時檢視(temporary views)。例如,如果你有一個 array 如下:
let arr: [i32; 4] = [10, 20, 30, 40];
你可以像下面這樣,建立一個包含第二個和第三個元素的 slice:
let s = &arr[1..3];
[1..3]
語法建立一個區間,從索引 1(包含)到 3(不包含)(譯註:即左閉右開)。如果你省略區間的第一個數([..3]
),它會預設從 0 開始,如果你省略最後一個數([1..]
),它會預設為到陣列的長度。如果你列印 slice [1..3]
中的元素,你將會得到 20 和 30:
//prints 20
println!("First element in slice: {:}", s[0]);
//prints 30
println!("Second element in slice: {:}", s[1]);
但是如果你嘗試訪問 slice 範圍之外的元素,它會 panic:
//panics: index out of bounds
println!("Third element in slice: {:}", s[2]);
但, slice 是如何知道它只有兩個元素呢?這是因為 slice 不是一個簡單的指向 array 的指標,它還在一個額外的長度欄位中標記了 slice 中的元素數量。
除了指向物件的地址外,還帶有某些額外資料的指標稱為胖指標(fat pointer)。Slice 不是 Rust 中唯一的胖指標型別。還有例如,trait 物件,除了指向物件的指標外,還有一個虛表指標。
例如,你可以建立一個 vector 的 slice:
let v: Vec<i32> = vec![1, 2, 3, 4];
let s = &v[1..3];
除了有一個指標指向v
的 buffer 中的第二個元素之外,s
還有一個長度為 8 位元組的欄位(length),其值為 2:
長度欄位(length)的存在可以通過下面的程式碼來看到,在這段程式碼中,一個 slice(&[i32]
)的大小為 16 位元組(8 位元組為 buffer pointer,8 位元組為長度欄位):
use std::mem::size_of;
fn main() {
//prints 8
println!("Size of a reference to an i32: {:}", size_of::<&i32>());
//print 16
println!("Size of a slice: {:}", size_of::<&[i32]>());
}
Array 的 slice 也是類似,但是 buffer pointer 不是指向堆(heap)上的 buffer,而是指向棧(stack)上的 array。
因為 slice 借用自底層的資料結構,所有的常見借用規則都在此適用。例如,下面的程式碼會被編譯器拒絕:
fn main() {
let mut v: Vec<i32> = vec![1, 2, 3, 4];
let s = &v[..];
v.push(5);
println!("First element in slice: {:}", s[0]);
}
為什麼呢?因為當 slice 被建立時,它指向 vector 內部 buffer 的第一個元素並且當一個新元素被 push 進 vector 時,它(指 vector)會分配一個新的 buffer 並且舊的 buffer 會被釋放。這就導致 slice 指向了一個無效的記憶體地址,如果訪問這個無效地址則會導致未定義行為。Rust 再一次從災難中拯救了你。
因為 array 和 vector 都可以建立 slice,它們(指 slice)是非常強大的抽象。因此,對於函式中的引數,預設的選擇應該是接收一個 slice 而不是一個 array 或 vector。事實上,很多函式,像
len
、is_empty
等,都是作用於 slice 而非 vector 或 array。
總結(Conclusion)
Array 和 vector 作為新手程式設計師學習過程中最先接觸的資料型別之一,Rust 支援它們也不足為奇。但是,正如我們所見,Rust 的安全性保證不允許程式設計師對這些基礎資料型別進行濫用。Slice 是 Rust 中的一個新概念,但是因為它們(指 slice)是這樣一個給力的抽象,你會發現它們在任意的 Rust 程式碼庫裡都被普遍使用。
參考資料
未定義行為: https://blog.regehr.org/archives/213