Lưu trữ danh sách các giá trị với Vectors
Cho tới nay chúng ta đã nhìn thấy kiểu danh sách là Vec<T>
- đọc là vector. xwVector cho phép chúng ta lưu trữ
nhiều các giá trị cùng kiểu bằng cách đặt chúng cạnh nhau trong bộ nhớ. Vector chỉ cho phép lưu trữ các giá trị
cùng kiểu dữ liệu. Eg: Cùng kiểu i32, i64, str, u8... Chúng hưu dụng khi chúng ta có danh sách các đối tượng, giống
như mỗi dòng text trong một file hoặc giá của các items trong một giỏ hàng.
Cách tạo một vector
Để tạo một vector mới và rỗng, chúng ta gọi hàm Vec::new
, xem ví dụ Listing 8-1
fn main() { let v: Vec<i32> = Vec::new(); }
Listing 8-1: Tạo một vector mới, rỗng, kiểu dữ liệu i32
Chú ý rằng, chúng thêm ký hiệu ở đây. Bởi vì, chúng ta đã không chèn bất kỳ giá trị nào vào vector này, Rust
sẽ không biết kiểu dữ liệu nào chúng ta định lưu trữ. Điều này khá là quan trọng. Bởi vì Vector đã
thực hiện sử dụng kiểu dữ liệu tổng quát (generics). Chúng ta sẽ xem xét cách để sử dụng
generics với kiểu dữ liệu mà bạn muốn trong Chương 10. Hiện tại, Kiểu Vec<T>
được cung cấp
bởi thư viện chuẩn và có thể lưu trữ bất kỳ giá tị nào. Khi chúng tạo một vector để lưu trữ một kiểu cụ thể,
chúng ta có thể chi tiết kiểu trong dấu <>. Trong 9-1, chúng ta đã nói rằng Vec<T>
trong đó v sẽ giữ thành phần với kiểu dữ liệu i32
Thường xuyên hơn, bạn sẽ tạo một Vec<T>
với giá trị khởi tạo và Rust sẽ tự ngầm hiểu(infer)) kiểu mà bạn muốn
lưu trữ, do đó hiếm bạn cần ký hiệu cho kiểu dữ liệu. Rust cũng cấp một macro vec!
mà sẽ tạo
một vector mới mà sẽ giữ các giá trị mà bạn truyền vào. Listing 8-2 tạo một Vec<i32
và dữ giá tị
1
,2
, và 3
. Kiểu số nguyên là i32
bởi vì nó là kiểu số nguyên mặc định (4 bytes). Điều này chúng ta
đã thảo luận trong “Data types” Chương 3
fn main() { let v = vec![1, 2, 3]; }
Listing 8-2: Tạo một vectỏ mới bao gồm các giá trị
Bởi vì chúng ta khởi tạo các giá trị i32
, Rust có thể ngầm hiểu kiểu của v
là Vec<i32>
, và ký hiệu
cho kiểu là không cần thiết. Tiêp theo, chúng ta sẽ xem xét cách để thay đổi một vector
Cập nhật một Vector
Để tạo một vector và sau đó thêm các thành phần tới chúng, chúng ta sử dụng phương thức push
như được chỉ ra
trong Listing 8-3
fn main() { let mut v = Vec::new(); v.push(5); v.push(6); v.push(7); v.push(8); }
Listing 8-3: Sử dụng phương thức push
để thêm các giá trị tới một
vector
Bởi vì với bất kỳ biến nào, nếu bạn muôn có thể thay đổi giá trị của chúng, bạn cũng sẽ cần làm cho nó mutable
(có thể thay đổi được) bằng cách sử dụng từ khoá mut
, như đã nói đến trong Chương 3. Số chúng đặt trong vector là kiểu i32
, và Rust
ngầm hiểu từ dữ liệu, do đó chúng ta không thực sự cần ký hiệu Vec<i32>
Huỷ thành phần trong một vector
Giống như bất kỳ kiếu struct
nào, một vector được giải phóng khi nó ra ngoài phạm vi của nó xem Listing 8-4.
fn main() { { let v = vec![1, 2, 3, 4]; // do stuff with v } // <- v goes out of scope and is freed here }
Listing 8-4: Nơi vector và các giá trị của nó bị huỷ
Khi vector bị huỷ, tất cả nội dung trong nó sẽ bị huỷ, nghĩa rằng tất cả số nguyên nó giữ sẽ bị xoá sạch.
Điều này có vẻ dường như đơn giản nhưng nếu như chúng ta bắt đầu học về tham chiếu
(reference
) tới các thành phần
của một vector, chắc chắn nó không còn dễ hiểu như vậy nữa
Đọc thành phần (elements) trong Vector
Có 2 cách để tham chiếu một giá trị lưu trong một vector: thông qua index (chỉ số) hoặc sử dụng phương thức get
.
Trong ví dụ dưới đây, chúng ta ký hiệu kiểu của các giá trị mà được trở về từ các hàm trên cho mục địch phân làm rõ hơn.
Listing 8-5 chỉ ra cả 2 phương thức truy xuất một giá trị trong một vector, một sử dụng chỉ số, một sử dụng phương thức get
fn main() { let v = vec![1, 2, 3, 4, 5]; let third: &i32 = &v[2]; println!("The third element is {}", third); match v.get(2) { Some(third) => println!("The third element is {}", third), None => println!("There is no third element."), } }
Listing 8-5: Sử dụng cú pháp chỉ số hoặc phương thức get
để truy xuất một đối tượng trong một vector
Chú ý có 2 chi tiết ở đây. Đầu tiên, chúng ta sử dụng giá trị chỉ số 2
để lấy phần tử thứ 3 bởi vì vector được
đánh chỉ số bắt đầu từ 0. Thứ hai, chúng ta lấy phần tử thứ 3 bởi sử dụng &
hoặc []
, mà đưa chúng ta
một tham chiếu hoặc sử dụng phương thức get
với chỉ số truyền vào như tham số, kết quả là Option<&T>
Lý do Rust cung cấp 2 cách để tham chiếu một thành phần là bạn có thể chọn cách chương trình hành xử (behaves) khi bạn cố gắng để sử dụng một giá trị ngoài dãy các thành phần xác định. Ví dụ, xem xét chuyện gì xảy ra khi chúng ta có một vector với chỉ 5 thành phần và bạn lại luốn truy xuất tới thành phần có index 100. Xem xét Listing 8-6.
fn main() { let v = vec![1, 2, 3, 4, 5]; let does_not_exist = &v[100]; let does_not_exist = v.get(100); }
Listing 8-6: Attempting to access the element at index 100 in a vector containing five elements
Khi bạn chạy đoạn code này, phương thức đầu tiên []
sẽ gây chương trình panic
(ngắt) bởi vì
nó tham chiếu tới một thành phần không tồn tại. Điều này là tốt khi bạn muốn chương trình crash
nếu
như bạn không muốn có sự truy xuất tới thành phần ngoài vector (Ví dụ như 1 số chương trình cố gắn truy nhập ô nhớ sau vector đề biết nhiều thông tin hơn chẳng hạn)
Khi phương thức get
được sử dụng với chỉ số nằm ngoài một vector, nó trở về None
ngoài trừ panicking (ngắt
).
Bạn sẽ sử dụng phương thức này nếu truy xuất một thành phần vược ra ngoài khoảng của vector có thể xảy ra thường xuyên
trong một số trường hợp. Code của bạn sẽ có logic để xử lý hoặc là Some(&element)
hoặc None
như đã thảo luận trong chương 6.Ví dụ, chỉ số có thể đến từ một người nhập vào một số. Nếu họ vô tình nhập một số mà quá lớn, chương trình
sẽ có giá trị None
, bạn có thể nói người dùng bao nhiều items trong vector hiện tại, và đưa cho họ một cơ hội khác với
giá trị nhập vào hợp lý hơn. Điều này là nhiều thân thiện hơn là ngắt chương trình chỉ vì một lỗi nhập số ngớ ngẩn.
Khi chương trình có một tham chiếu hợp lệ, chương trình borrow checker
(bộ kiểm tra nguyên lý mượn) thực thi luật
quyền sở hữu (ownership) và nguyên lý borrowing (đã nói trong chương 4) để đảm bảo các tham chiếu và bất kỳ tham chiếu
khác tới nội dung của vector còn lại hợp lệ. Nhắc lại luật này, bạn không thể có cùng lúc tham chiếu mutable và immutable trong cùng phạm vi (scope - Ví dụ cùng 1 hàm). Luật này áp dụng trong Listing 8-7, nơi chúng ta giữ một tham chiếu immutable(không thay đổi được) tới thành phần đầu tiên của vector và cố gắn thêm một thành phần tới cuối cùng. Chương trình này sẽ
không chạy nếu chúng ta cố gắng tham chiếu tới thành phần đó trong hàm.
fn main() {
let mut v = vec![1, 2, 3, 4, 5];
let first = &v[0];
v.push(6);
println!("The first element is: {}", first);
}
Listing 8-7: Cố gắng thêm một thành phần tới một vector trong khi giữ một tham chiếu tới một thành phần
Biên dịch code này sẽ lỗi:
$ cargo run
Compiling collections v0.1.0 (file:///projects/collections)
error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
--> src/main.rs:6:5
|
4 | let first = &v[0];
| - immutable borrow occurs here
5 |
6 | v.push(6);
| ^^^^^^^^^ mutable borrow occurs here
7 |
8 | println!("The first element is: {}", first);
| ----- immutable borrow later used here
For more information about this error, try `rustc --explain E0502`.
error: could not compile `collections` due to previous error
Code trong Listing 8-7 thoạt nhìn không có gì sai: Tại sao một tham chiếu tới thành phần đầu tiên quan tâm tới sự thay đổi tại thành phần cuối của một vector? Lỗi này là bởi vì cách vector hoạt động: bởi vì vectors đặt các giá trị cạnh trong trong bộ nhớ, thêm một thanh phần mới vào cuối của vector có thể yêu cầu cấp phát bộ nhớ và copy các thành phần của tới không gian mới này, nếu không đủ bộ nhớ để đặt tất cả các thành phần cạnh nhau nơi vector được lưu trữ hiện tại. Trong trường hợp này, tham chiếu tới thành phần đầu tiên trỏ tới bộ nhớ được thu hồi (deallocated memory). Luật borrowing ngăn chặn chương trình rơi vào tình huống này.
Chú ý: Cho nhiều thông tin chi tiết thực hiện kiểu
Vec<T>
, xem cuốn “The Rustonomicon”.
Duyệt qua các giá trị trong một Vector
Để truy xuất mỗi thành phàn trong một vector lần lượt, chúng ta có thể duyệt qua (iterate) tất các thành phần
hhown là sử dụng chỉ thị để truy xuất mỗi thành phần. Listing 8-8 chỉ cách để sử dụng for
lặp để có tham chiếu immutable tới mỗi thành phần trong một Vector i32
và in chúng
fn main() { let v = vec![100, 32, 57]; for i in &v { println!("{}", i); } }
Listing 8-8: In mỗi thành phần trong một Vector bởi duyệt qua các thành phần sử dụng vòng lặp for
Chúng ta cũng có thể duyệt qua tham chiếu mutable tới mỗi thành phần trong một vector mutable lần lượt để thay đổi tất cả thành phần.
Vòng lặp for
trong Listing 8-9 sẽ cộng thêm 50
tới mỗi thành phần
fn main() { let mut v = vec![100, 32, 57]; for i in &mut v { *i += 50; } }
Listing 8-9: Duyệt qua các tham chiếu mutable trỏ tới các thành phần trong một vector
Để thay đổi giá trị mà một tham chiếu mutable trỏ tới, chúng ta có thể sử toàn tử *
(gọi là dereference operator) để lấy giá trị trong i
trước khi chúng ta
có thể sử dụng toán tử +=
. Chúng ta sẽ nói nhiều về toán tử derefence này
“Toán từ Dereference như một con trỏ tới một giá trị”
trong chương 15
Sử dụng Enum để lưu trữ nhiều kiểu dữ liệu
Vectors có thể chỉ lưu trữ các giá trị mà có cùng kiểu. Điều này có thể hơi bất tiện; rõ ràng có trường hợp cho cần dể lưu trữ một danh sách các phần tử của kiểu khác nhau
May mắn thay, biến thể của một enum định nghĩa dưới cùng kiểu enum, nên khi chúng ta cần 1 kiểu để thể hiện các thành phần của kiểu khác nhau, chúng ta có thể sử dụng enum
!
Ví dụ, chúng ta muốn lấy gái trị từ một dòng trong 1 spreadsheet (tài liệu) nơi một số cột trong hàng bao gồm số nguyên dượng, một vài sốt thực, và một số string.
Chúng ta có thể định nghĩa một enum mà các biến của nó sẽ giữ kiểu khác nhau, và tất các biến enum sẽ được xem là cùng một kiểu: kiểu enum. Sau đó chúng ta có thể tạo một vector
để lưu trữ enum và do đó không giới giạn lưu trữ kiểu dữ liệu khác nhau. Chúng ta sẽ trình bày điều này trong Listing 8-10
fn main() { enum SpreadsheetCell { Int(i32), Float(f64), Text(String), } let row = vec![ SpreadsheetCell::Int(3), SpreadsheetCell::Text(String::from("blue")), SpreadsheetCell::Float(10.12), ]; }
Listing 8-10: Định nghĩa enum
để lưu trữ giá trị của kiểu khác nhau trong một vector
Rust cần để biết kiểu dữ liệu nào sẽ trong một Vector tại lúc biên dịch do đó nó biết trình chính xác bao nhiều bộ nhớ trong vùng heap sẽ được cần để lưu trữ mỗi thành phần.
Chúng ta cũng phải chính xác về kiểu dữ liệu nào được cho phép trong vector này. Nếu Rust cho phép một vector để lưu trữ bất kỳ kiểu nào, sẽ có lúc rằng một hoặc nhiều kiểu
sẽ gây lỗi với điều khiển thực hiện trong thành phần của Vector. Sử dụng enum với match
cú pháp nghĩa rằng Rust sẽ đảm bảo tại lúc biên dịch mõi trường hợp có thể xảy ra sẽ được
xử lý, bởi như đã nói ở Chương 6.
Nếu bạn không biết tập hợp kiểu có thể của kiểu dữ liệu mà một chương trình tại lúc runtime (lúc chạy chương trình) lưu giữ trong một Vector, công nghệ enum sẽ áp dụng được. Thay vào
đó, bạn có thể sử dụng trait object (đối tuợng trait), mà chúng ta sẽ học trong Chương 17
Nào, chúng ta đã thảo luận một số cách thông thường để sử dụng Vector, xem lại the API documentation để tất cả các phương thức hữu dụng định nghĩa trong
Vec<T>
trong thư viện chuẩn. Ví dụ, trong hàm push
, phương thức pop
xoá và trả về giá trị cuối cùng. Tiếp theo, cùng xem xét kiểu collection String