Học Rust bằng ví dụ
Rust là một ngôn ngữ lập trình hệ thống hiện đại tập trung vào tính an toàn, tốc độ và tính đồng thời. Nó đạt đươc các tiêu chí này bằng cách làm bộ nhớ trở nên an toàn mà không sử dụng đến garbage collection (bộ thu gom rác).
Đây là bản dịch tiếng Việt của Rust by Example (RBE), một tập các ví dụ minh họa cho các khái niệm và thư viện tiêu chuẩn của Rust. Để giữ được tính đúng đắn về mặt thuật ngữ, xuyên suốt nội dung của toàn bộ bản dịch này, sẽ có nhiều thuật ngữ được giữ nguyên dưới dạng tiếng Anh (có kèm theo diễn giải bằng tiếng Việt). Để hiểu thêm về những ví dụ này, đừng quên cài đặt ngôn ngữ Rust và xem tài liệu chính thức. Thêm vào đó, nếu bạn tò mò, bạn cũng có thể kiểm tra mã nguồn của trang web này hoặc mã nguồn của bản gốc tiếng Anh.
Nào, chúng ta cùng bắt đầu!
-
Hello World - Bắt đầu với chương trình Hello World truyền thống.
-
Primitives - Học về số nguyên có dấu, số nguyên không dấu và các kiểu dữ liệu nguyên thủy khác.
-
Custom Types -
struct
vàenum
. -
Variable Bindings - Học về mutable bindings, scope và shadowing.
-
Types - Học về cách thay đổi và định nghĩa các kiểu dữ liệu.
-
Flow of Control -
if
/else
,for
, and các luồng điều khiển khác. -
Functions - Học về Methods, Closures và High Order Functions.
-
Modules - Tổ chức mã code của bạn bằng cách sử dụng các modules.
-
Crates - Một crate là một đơn vị biên dịch trong Rust. Học về cách tạo library (thư viện).
-
Cargo - Xem qua một số tính năng cơ bản của Cargo - công cụ quản lý các package Rust chính thức.
-
Attributes - Attribute (Thuộc tính) là metadata (siêu dữ liệu) được áp dụng cho một số module, crate hoặc item.
-
Generics - Tìm hiểu về cách viết một hàm hoặc kiểu dữ liệu có khả năng hoạt động với nhiều loại đối số.
-
Scoping rules - Scope (Phạm vi) đóng một phần quan trọng trong cái khái niệm về ownership (quyền sở hữu), borrowing (mượn quyền) và lifetimes (vòng đời).
-
Traits - Một trait là một tập các methods được xác định cho một kiểu không xác định (unknown type):
Self
-
Error handling - Học cách Rust xử lý lỗi.
-
Std library types - Học về một vài custom types (kiểu dữ liệu tùy chỉnh) được cung cấp bởi
std
library. -
Std misc - Học thêm về một vài kiểu dữ liệu tùy chỉnh cho việc xử lý tệp (file) và luồng (threads).
-
Testing - Tất cả các loại thử nghiệm chương trình trong Rust.
-
Meta - Documentation (Tài liệu) và Benchmarking (Đo điểm chuẩn).
Hello World
Đây là mã nguồn của chương trình Hello World truyền thống được viết bằng ngôn ngữ Rust.
// Đây là một comment, nó sẽ được bỏ qua bởi trình biên dịch // Bạn có thể test mã code này bằng việc click vào nút "Run" ở trên -> // hoặc nếu bạn thích sử dụng bàn phím, hãy nhấn tổ hợp "Ctrl + Enter" // Mã code này là có thể sửa, hãy thử chỉnh sửa nó! // Sau đó bạn luôn có thể trở lại mã code ban đầu bằng việc click vào nút "Reset" -> // Đây là main function của chương trình fn main() { // Các câu lệnh ở đây được thực thi khi compiled binary (mã nhị phân đã biên dịch) được gọi // In văn bản ra Console println!("Hello World!"); }
println!
là một macro, thứ có thể in văn bản ra
console cho bạn.
Một binary có thể được tạo ra bằng cách sử dụng trình biên dịch của Rust: rustc
.
$ rustc hello.rs
rustc
sẽ tạo ra một hello
binary có thể thực thi.
$ ./hello
Hello World!
Thực hành
Nhấn 'Run' và xem kết quả. Tiếp theo, hãy thử thêm một dòng với
println!
macro thứ hai để có được output như sau:
Hello World!
I'm a Rustacean!
Comments (Giải thích) trong Rust
Bất kỳ chương trình nào cũng yêu cầu có comments (nhận xét, giải thích) và Rust hỗ trợ một số loại comments khác nhau:
- Regular comments, sẽ được bỏ qua bởi trình biên dịch:
// Line comments là loại comment kéo dài đến cuối dòng.
/* Block comments là loại comment của một khối lệnh.*/
- Doc comments, sẽ được phân tích cú pháp thành HTML library
documentation:
/// Tạo library docs cho mục bên dưới nó.
//! Tạo library docs cho mục bao quanh nó.
fn main() { // Đây là một ví dụ cho line comment // Có hai dấu gạch chéo / ở đầu dòng // Và tất cả những gì viết phía sau chúng trong dòng sẽ không được trình biên dịch đọc // println!("Hello, world!"); // Câu lệnh trên sẽ không được thực hiện vì nó đã được tính là một line comment. Nếu muốn chạy nó, thử xóa hai dấu gạch chéo và chạy lại. /* * Đây là một loại comment khác, block comment. Nói chung, * line comment là kiểu comment được khuyên dùng. Tuy nhiên * block comment cũng cực kỳ hữu ích khi muốn tạm thời vô hiệu hóa * một đoạn mã dài giúp bạn không mất công gõ nhiều dấu // ở đầu dòng. * /* Block comment có thể /* lồng vào nhau, */ như ví dụ này */ * /*/*/* Bạn hãy tự thử xem! */*/*/ */ // Bạn có thể thao tác với các biểu thức dễ dàng hơn bằng block comment // so với line comment. Thử xóa các dấu của block comment dưới đây // để thay đổi kết quả: let x = 5 + /* 90 + */ 5; println!("Is `x` 10 or 100? x = {}", x); }
Xem thêm tại đây:
Formatted print (Định dạng in dữ liệu) trong Rust
Printing (In văn bản) được xử lý bởi một loạt macros
được xác định trong std::fmt
,
một số trong số macro đó bao gồm:
format!
: ghi văn bản được định dạng vào mộtString
.print!
: giống nhưformat!
nhưng văn bản được in ra console (io::stdout).println!
: giống nhưprint!
nhưng sẽ xuống dòng sau khi in kí tự cuối.eprint!
: giống nhưprint!
nhưng văn bản được in với định dạng standard error (io::stderr).eprintln!
: giống nhưeprint!
nhưng sẽ xuống dòng sau khi in kí tự cuối.
Rust còn kiểm tra tính đúng đắn của định dạng tại compile time (thời điểm biên dịch).
fn main() { // Dấu `{}` sẽ tự động được thay thế bằng bất kỳ // đối số nào. Chúng sẽ được xâu chuỗi lại. println!("{} days", 31); // Có thể chỉ định vị trí cho các đối số bằng một số nguyên bên trong `{}`, // nó sẽ xác định đối số nào sẽ được dùng để thay thế. Các đối số được liệt kê // phía sau xâu được định dạng, và được đánh vị trí bắt đầu từ 0. println!("{0}, this is {1}. {1}, this is {0}", "Alice", "Bob"); // Và đối số cũng có thể được đặt tên như bên dưới đây println!("{subject} {verb} {object}", object="the lazy dog", subject="the quick brown fox", verb="jumps over"); // Các định dạng khác nhau được xác định bằng ký tự định dạng được chỉ định sau một dấu `:`. println!("Base 10 repr: {}", 69420); println!("Base 2 (binary) repr: {:b}", 69420); println!("Base 8 (octal) repr: {:o}", 69420); println!("Base 16 (hexadecimal) repr: {:x}", 69420); println!("Base 16 (hexadecimal) repr: {:X}", 69420); // Bạn có thể căn phải văn bản với chiều rộng được xác định. Câu lệnh dưới sẽ in ra // " 1". Với 5 khoảng trắng và một kí tự "1" ở cuối. println!("{number:>5}", number=1); // Bạn có thể thay các khoảng trắng trên bằng các số 0. Câu lệnh dưới sẽ xuất ra "000001". println!("{number:0>5}", number=1); // Bạn có thể sử dụng các đối số được đặt tên trong trình định dạng bằng cách thêm `$' println!("{number:0>width$}", number=1, width=5); // Rust thậm chí còn kiểm tra xem số lượng đối số được sử dụng trong xâu định dạng có chính xác hay không. println!("My name is {0}, {1} {0}", "Bond"); // Câu lệnh trên sẽ báo lỗi bởi bạn chưa điền đối số có vị trí 1. // FIXME ^ Để sửa nó, hãy thử thêm "James" vào sau "Bond". // Chỉ có các kiểu mà triển khai fmt::Display mới có thể được định dạng bằng `{}`. // User-defined types (Các kiểu do người dùng định nghĩa) sẽ không triển khai fmt::Display theo mặc định. #[allow(dead_code)] struct Structure(i32); // Câu lệnh này sẽ không được biên dịch vì `Structure` không triển khai fmt::Display println!("This struct `{}` won't print...", Structure(3)); // FIXME ^ Để hàm chạy đúng, hãy comment câu lệnh trên lại. // Đối với Rust 1.58 trở lên, bạn có thể lấy đối số từ các biến xung quanh. // Cũng giống như ở trên, câu lệnh dưới đây sẽ in ra // " 1". Với 5 khoảng trắng và một kí tự "1" ở cuối. let number: f64 = 1.0; let width: usize = 6; println!("{number:>width$}"); }
std::fmt
có nhiều traits
để quản lý việc hiển thị của văn bản.
Dưới đây là hai dạng cơ bản:
fmt::Debug
: Sử dụng dấu{:?}
. Định dạng văn bản cho mục đích gỡ lỗi.fmt::Display
: Sử dụng dấu{}
. Định dạng văn bản được hiển thị ra có hình thức thân thiện với người dùng.
Ở đây, chúng ta đã sử dụng fmt::Display
vì thư viện std cung cấp các triển khai (implementations)
cho những kiểu này. Để in văn bản cho các kiểu dữ liệu tùy chỉnh, cần nhiều bước hơn.
Việc triển khai fmt::Display
trait sẽ tự động triển khai
ToString
trait cho phép chúng ta chuyển đổi kiểu thành String
.
Thực hành
- Sửa 2 sự cố trong đoạn code bên trên (mục FIXME) để nó chạy được mà không bị lỗi.
- Thêm lời gọi
println!
macro để in ra:Pi is roughly 3.142
bằng cách kiểm soát số chữ số thập phân được hiển thị. Cho mục đích của bài tập này, hãy sử dụnglet pi = 3.141592
là một ước lượng cho số pi. (Gợi ý: bạn có thể cần kiểm tra tài liệustd::fmt
để thiết lập số lượng các chữ số thập phân được hiển thị)
Xem thêm tại:
std::fmt
, macros
, struct
,
và traits
Debug
Tất cả các kiểu dữ liệu muốn sử dụng các trait
của định dạng std::fmt
đều yêu cầu phải có
một implementation để có thể in được dữ liệu. Việc tự động triển khai chỉ được cung cấp
cho các kiểu có sẵn trong thư viện std
. Tất cả những kiểu khác đều phải triển khai
thủ công bằng cách nào đó.
Trait
fmt::Debug
làm cho việc này trở nên rất đơn giản. Tất cả các kiểu đều có thể
derive
(dẫn xuất) tự động tạo một fmt::Debug
implementation. Điều này không áp dụng cho fmt::Display
,
thứ mà bắt buộc phải được triển khai một cách thủ công.
#![allow(unused)] fn main() { // Kiểu struct này không thể in được dữ liệu với `fmt::Display` // hoặc `fmt::Debug`. struct UnPrintable(i32); // Thuộc tính `derive` sẽ tự động tạo một implementation được yêu cầu // để `struct` này có thể in được dữ liệu với `fmt::Debug`. #[derive(Debug)] struct DebugPrintable(i32); }
Tất cả các kiểu của thư viện std
cũng tự động có khả năng in dữ liệu với {:?}
:
// Derive một `fmt::Debug` implementation cho kiểu `Structure`. `Structure` // là một cấu trúc chỉ chứa một số nguyên `i32`. #[derive(Debug)] struct Structure(i32); // Đặt `Structure` vào trong cấu trúc `Deep`. Ở đây cũng tạo cho nó khả năng // in dữ liệu. #[derive(Debug)] struct Deep(Structure); fn main() { // In dữ liệu với `{:?}` là tương tự như với `{}`. println!("{:?} months in a year.", 12); println!("{1:?} {0:?} is the {actor:?} name.", "Slater", "Christian", actor="actor's"); // `Structure` có thể in dữ liệu! println!("Now {:?} will print!", Structure(3)); // Vấn đề với `derive` đó là không thể kiểm soát cách kết quả được // in ra thế nào. Sẽ thế nào nếu tôi chỉ muốn hiển thị ra một số `7`? println!("Now {:?} will print!", Deep(Structure(7))); }
fmt::Debug
làm cho mọi thứ có thể in được nhưng phải hy sinh tính linh hoạt trong việc
tùy chỉnh hiển thị kết quả. Rust có cung cấp tính năng "pretty printing" với {:#?}
để kết quả in ra được thân thiện hơn.
#[derive(Debug)] struct Person<'a> { name: &'a str, age: u8 } fn main() { let name = "Peter"; let age = 27; let peter = Person { name, age }; // Pretty print println!("{:#?}", peter); }
Để kiểm soát việc hiển thị kết quả, cần triển khai thủ công fmt::Display
.
Xem thêm tại đây:
attributes
, derive
, std::fmt
,
và struct
Display
fmt::Debug
trông không thân thiện và gọn gàng, và nó ít có lợi khi muốn
tùy chỉnh giao diện đầu ra. Để làm được điều này, bạn cần triển khai thủ công
fmt::Display
, với {}
. Thực hiện nó như sau:
#![allow(unused)] fn main() { // Import (bằng `use`) `fmt` module để sử dụng các thành phần nó. use std::fmt; // Định nghĩa cấu trúc mà ta sẽ triển khai `fmt::Display`. Nó là một tuple struct // có tên là `Structure` chứa một số nguyên `i32` struct Structure(i32); // Để có thể sử dụng `{}` để in dữ liệu, trait `fmt::Display` phải được triển khai thủ công // cho kiểu dữ liệu đó impl fmt::Display for Structure { // Trait này yêu cầu `fmt` với chữ kí hàm chính xác. fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { // Ghi phần tử đầu tiên vào output stream (luồng đầu ra) `f` được cung cấp. // Nó trả về `fmt::Result` để cho biết thao tác là thành công hay thất bại. // Lưu ý rằng `write!` có cú pháp rất giống với `println!`. write!(f, "{}", self.0) } } }
fmt::Display
trông gọn gàng hơn fmt::Debug
, nhưng điều này gây ra một
vấn đề cho thư viện std
. Đó là các ambiguous types (kiểu chưa được xác định rõ ràng)
sẽ được hiển thị như thế nào? Ví dụ, nếu thư viện std
triển khai một style hiển thị
duy nhất cho tất cả kiểu Vec<T>
, thì đầu ra là gì? Nó có thể là một trong hai dạng dưới đây không?
Vec<path>
:/:/etc:/home/username:/bin
(phân cách bởi:
)Vec<number>
:1,2,3
(phân cách bởi,
)
Không, bởi vì không có style lý tưởng cho tất cả các kiểu và thư viện std
không thể giả định để chọn một cái cụ thể. Do đó fmt::Display
sẽ không được triển khai choVec<T>
hoặc cho bất kỳ generic containers nào khác. Khi đó, fmt::Debug
phải được sử dụng
cho các trường hợp chung này.
Mặc dù vậy, đây không phải là vấn đề lớn bởi vì đối với bất kỳ kiểu container mới nào mà
không phải generic, thì fmt::Display
vẫn có thể được triển khai.
use std::fmt; // Import `fmt` // Một cấu trúc chứa hai số. Ở đây, `Debug` sẽ được derived để // đối chiếu kết quả với `Display`. #[derive(Debug)] struct MinMax(i64, i64); // Triển khai `Display` cho `MinMax`. impl fmt::Display for MinMax { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { // Sử dụng `self.number` để trỏ tới vị trí của mỗi điểm dữ liệu thành phần của struct. write!(f, "({}, {})", self.0, self.1) } } // Định nghĩa một cấu trúc mà ở đó các trường được đặt tên dùng cho việc so sánh với MinMax. #[derive(Debug)] struct Point2D { x: f64, y: f64, } // Tương tự, triển khai `Display` cho `Point2D`. impl fmt::Display for Point2D { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { // Tùy chỉnh để trỏ tới x, y thành phần write!(f, "x: {}, y: {}", self.x, self.y) } } fn main() { let minmax = MinMax(0, 14); println!("Compare structures:"); println!("Display: {}", minmax); println!("Debug: {:?}", minmax); let big_range = MinMax(-300, 300); let small_range = MinMax(-3, 3); println!("The big range is {big} and the small is {small}", small = small_range, big = big_range); let point = Point2D { x: 3.3, y: 7.2 }; println!("Compare points:"); println!("Display: {}", point); println!("Debug: {:?}", point); // Câu lệnh dưới này khi chạy sẽ bị lỗi. Mặc dù cả `Debug` và `Display` // đều được triển khai, tuy nhiên `{:b}` lại yêu cầu triển khai `fmt::Binary`. // println!("What does Point2D look like in binary: {:b}?", point); }
So, fmt::Display
has been implemented but fmt::Binary
has not, and
therefore cannot be used. std::fmt
has many such traits
and
each requires its own implementation. This is detailed further in
std::fmt
.
Dù fmt::Display
đã được triển khai nhưng fmt::Binary
thì chưa, và
do đó không thể sử dụng {:b}
. std::fmt
còn có nhiều traits
và
mỗi traits
đều có yêu cầu triển khai riêng của mình. Điều này được trình bày
chi tiết hơn trong std::fmt
.
Thực hành
Sau khi kiểm tra output của ví dụ trên, hãy sử dụng cấu trúc Point2D
làm mẫu
để tạo thêm cấu trúc Complex
cho ví dụ. Khi in dữ liệu theo cùng một cách,
đầu ra phải là:
Display: 3.3 + 7.2i
Debug: Complex { real: 3.3, imag: 7.2 }
Xem thêm tại đây:
derive
, std::fmt
, macros
, struct
,
trait
, và use
Testcase: List
Việc triển khai fmt::Display
cho một cấu trúc mà trong đó mỗi phần tử phải
được xử lý tuần tự là một công việc khó. Vấn đề đó là mỗi một write!
macro
sẽ tạo ra một fmt::Result
. Để giải quyết điều này đòi hỏi phải xử lý tất cả các kết quả.
Rust cung cấp toán tử ?
cho mục đích này.
Sử dụng ?
với write!
sẽ như sau:
// `?` sẽ thử `write!` xem nó có lỗi hay không. Nếu có lỗi,
// trả về một lỗi. Ngược lại, sẽ tiếp tục thực hiện bình thường.
write!(f, "{}", value)?;
Với việc sử dụng ?
, việc triển khai fmt::Display
cho một Vec
trở nên thuận tiện hơn:
use std::fmt; // Import `fmt` module. // Định nghĩa một cấu trúc `List` chứa một `Vec`. struct List(Vec<i32>); impl fmt::Display for List { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { // Tạo ra một tham chiếu tới `vec` đầu vào. let vec = &self.0; write!(f, "[")?; // Lặp qua từng phần tử `v` trong `vec`, đồng thời gán index (chỉ mục) // của `v` cho biến `count`. for (count, v) in vec.iter().enumerate() { // Với mỗi phần tử trừ phần tử đầu tiên, đều thêm một dấu phẩy ở phía sau. // Sử dụng toán tử ? để trả về nếu có lỗi. if count != 0 { write!(f, ", ")?; } write!(f, "{}", v)?; } // Đóng ngoặc và trả về một giá trị fmt::Result. write!(f, "]") } } fn main() { let v = List(vec![1, 2, 3]); println!("{}", v); }
Thực hành
Hãy thử thay đổi chương trình trên để index của mỗi phần tử trong vectơ cũng được in ra. Output mới sẽ như sau:
[0: 1, 1: 2, 2: 3]
Xem thêm tại đây:
for
, ref
, Result
, struct
,
?
, và vec!
Formatting (Định dạng dữ liệu)
Chúng ta thấy ở đây, việc định dạng dữ liệu được chỉ định thông qua một format string (xâu định dạng):
format!("{}", foo)
->"3735928559"
format!("0x{:X}", foo)
->"0xDEADBEEF"
format!("0o{:o}", foo)
->"0o33653337357"
Cùng một biến (foo
) nhưng có thể được định dạng theo nhiều cách khác nhau phụ thuộc vào
argument type (kiểu đối số) nào được sử dụng: X
hoặc o
hoặc unspecified (không xác định).
Chức năng định dạng này được triển khai thông qua các traits
, mỗi trait
tương ứng với một loại đối số. Trait định dạng phổ biến nhất là Display
, nó
xử lý các trường hợp mà ở đó kiểu đối số là không xác định: ví dụ:{}
.
use std::fmt::{self, Formatter, Display}; struct City { name: &'static str, // Vĩ độ lat: f32, // Kinh độ lon: f32, } impl Display for City { // `f` là một buffer (vùng đệm), và phương thức này phải ghi xâu định dạng vào nó fn fmt(&self, f: &mut Formatter) -> fmt::Result { let lat_c = if self.lat >= 0.0 { 'N' } else { 'S' }; let lon_c = if self.lon >= 0.0 { 'E' } else { 'W' }; // `write!` cũng như `format!`, nhưng nó sẽ ghi xâu định dạng vào trong một buffer `f`. write!(f, "{}: {:.3}°{} {:.3}°{}", self.name, self.lat.abs(), lat_c, self.lon.abs(), lon_c) } } #[derive(Debug)] struct Color { red: u8, green: u8, blue: u8, } fn main() { for city in [ City { name: "Dublin", lat: 53.347778, lon: -6.259722 }, City { name: "Oslo", lat: 59.95, lon: 10.75 }, City { name: "Vancouver", lat: 49.25, lon: -123.1 }, ].iter() { println!("{}", *city); } for color in [ Color { red: 128, green: 255, blue: 90 }, Color { red: 0, green: 3, blue: 254 }, Color { red: 0, green: 0, blue: 0 }, ].iter() { // Ở đây, hãy triển khai fmt::Display cho `color` để có thể // sử dụng `{}` println!("{:?}", *color); } }
Bạn có thể xem danh sách đầy đủ các formatting traits và các kiểu đối số của chúng
tại std::fmt
documentation.
Thực hành
Triển khai fmt::Display
trait cho cấu trúc Color
phía trên
để output được in ra có kết quả như thế này:
RGB (128, 255, 90) 0x80FF5A
RGB (0, 3, 254) 0x0003FE
RGB (0, 0, 0) 0x000000
Hai gợi ý nhỏ nếu bạn gặp khó khăn trong bài tập trên:
- Bạn có lẽ sẽ cần liệt kê các màu nhiều hơn một lần, cho nên hãy đặt tên cho chúng,
- Bạn có thể đệm một số lượng số 0 để độ dài xâu hiển thị bằng 2 với
:0>2
.
Xem thêm tại đây:
Primitives (Kiểu dữ liệu nguyên thủy)
Rust cung cấp khá nhiều primitives
:
Scalar Types (Các kiểu vô hướng)
- Số nguyên có dấu:
i8
,i16
,i32
,i64
,i128
vàisize
(pointer size) - Số nguyên không dấu:
u8
,u16
,u32
,u64
,u128
vàusize
(pointer size) - Số thực dấu phẩy động:
f32
,f64
char
Ký tự Unicode ví dụ như:'a'
,'α'
và'∞'
(Kích thước 4 byte)bool
làtrue
hoặcfalse
- và unit type
()
- kiểu đơn vị, chỉ có thể có giá trị là một tuple trống :()
Mặc dù giá trị của một unit type là một tuple, nhưng nó không được coi là một kiểu hỗn hợp vì nó là tuple trống, không chứa giá trị.
Compound Types (Các kiểu hỗn hợp)
- Mảng (arrays):
[1, 2, 3]
- Tuples:
(1, true)
Các biến trong Rust có thể là type annotated, nghĩa là chúng được chú thích kiểu dữ liệu khi khởi taọ.
Các biến là số cũng có thể được chú thích thông qua suffix (hậu tố) hoặc default (theo mặc định).
Theo mặc định thì các số nguyên là i32
còn số thực là f64
.
Lưu ý rằng Rust cũng có thể suy ra kiểu dữ lỉệu của biến theo ngữ cảnh.
fn main() { // Các biến trong Rust có thể là `type annotated`. let logical: bool = true; let a_float: f64 = 1.0; // Chú thích kiểu thông thường let an_integer = 5i32; // Chú thích kiểu bằng hậu tố // Nếu không chú thích, kiểu của biến được xác định theo mặc định. let default_float = 3.0; // `f64` let default_integer = 7; // `i32` // Kiểu của biến có thể được Rust suy ra theo ngữ cảnh. let mut inferred_type = 12; // Kiểu i64 được suy ra từ một dòng code khác. inferred_type = 4294967296i64; // Một biến mutable (dùng từ khóa `mut` trước tên biến) thì có thể thay đổi giá trị let mut mutable = 12; // Mutable `i32` mutable = 21; // Ở đây sẽ báo lỗi! Bởi không thể thay đổi kiểu của biến. mutable = true; // Các biến có thể được ghi đè với shadowing. let mutable = true; }
Xem thêm tại đây:
std
library, mut
, inference
, và shadowing
Literals và operators
Số nguyên 1
, số thực 1.2
, ký tự 'a'
, xâu "abc"
, boolean true
and kiểu đơn vị ()
đều có thể được diễn đạt bằng literals.
Literal là gì? Nó là một giá trị thể hiện chính nó, một giá trị cố định được chèn trực tiếp
vào code mà chương trình không thể thay đổi trong quá trình thực thi.
Các số nguyên có thể được biểu diễn theo các cách khác nhau dưới dạng số thập lục phân (hexadecimal literals),
bát phân (octal literals) hay nhị phân (binary literals), bằng cách sử dụng các tiền tố tương ứng: 0x
, 0o
or 0b
.
Dấu gạch dưới có thể được chèn vào trong các numeric literals (hiểu đơn giản là các số)
để giúp việc biểu diễn số được rõ ràng, dễ đọc hơn, ví dụ:
1_000
chính là 1000
, và 0.000_001
chính là 0.000001
nhưng dễ đọc hơn.
Chúng ta cũng cần cho trình biên dịch biết kiểu literals mà ta sử dụng. Như trong đoạn code bên dưới,
ta sẽ sử dụng hậu tố u32
để chỉ ra rằng literal ở đây là một nguyên không dấu 32-bit
và hậu tố i32
để chỉ ra rằng đây là một số nguyên có dấu 32 bit.
Các operators (toán tử, phép toán) là có sẵn trong Rust và mức độ ưu tiên của chúng trong Rust là tương tự như trong các ngôn ngữ lập trình được truyền cảm hứng từ C khác.
fn main() { // 100 là một numeric literals let i: u32 = 100; // Phép cộng hai số nguyên println!("1 + 2 = {}", 1u32 + 2); // Phép trừ hai số nguyên println!("1 - 2 = {}", 1i32 - 2); // TODO ^ Thử thay đổi `1i32` sang `1u32` và quan sát lỗi // để xem tại sao kiểu của biến lại quan trọng. // Short-circuiting boolean logic (Các phép toán logic: hội, tuyển, phủ định) println!("true AND false is {}", true && false); println!("true OR false is {}", true || false); println!("NOT true is {}", !true); // Các toán tử Bitwise (Thao tác bit) println!("0011 AND 0101 is {:04b}", 0b0011u32 & 0b0101); println!("0011 OR 0101 is {:04b}", 0b0011u32 | 0b0101); println!("0011 XOR 0101 is {:04b}", 0b0011u32 ^ 0b0101); println!("1 << 5 is {}", 1u32 << 5); println!("0x80 >> 2 is 0x{:x}", 0x80u32 >> 2); // Sử dụng dấu gạch dưới để việc biểu diễn số được sáng sủa và dễ đọc hơn! println!("One million is written as {}", 1_000_000u32); }
Tuples
Một tuple là một tập các phần tử có thứ tự mà chúng có thể có kiểu dữ liệu khác nhau. Tuples được tạo bằng cách
sử dụng cặp dấu ngoặc đơn ()
bên ngoài, và tất cả những gì nằm trong đó là những phần tử của tuple.
Bản thân mỗi tuple là một giá trị có type signature (chữ ký kiểu dữ liệu) là (T1, T2, ...)
,
trong đó T1
, T2
là các kiểu dữ liệu của các phần tử trong tuple. Các hàm có thể
sử dụng tuples cho mục đích trả về nhiều giá trị, vì các tuple có thể chứa một số lượng phần tử bất kỳ.
// Tuples có thể được sử dụng như đối số của hàm cũng như có thể dùng làm giá trị trả về. fn reverse(pair: (i32, bool)) -> (bool, i32) { // từ khóa `let` có thể được dùng để gán, liên kết (binding) các phần tử của tuple với các biến let (integer, boolean) = pair; (boolean, integer) } // Struct dưới đây được dùng cho mục bài tập thực hành bên dưới #[derive(Debug)] struct Matrix(f32, f32, f32, f32); fn main() { // Đây là một tuple với nhiều giá trị có kiểu dữ liệu khác nhau. let long_tuple = (1u8, 2u16, 3u32, 4u64, -1i8, -2i16, -3i32, -4i64, 0.1f32, 0.2f64, 'a', true); // Các giá trị có thể được lấy ra từ tuple thông qua chỉ mục. println!("long tuple first value: {}", long_tuple.0); println!("long tuple second value: {}", long_tuple.1); // Một tuple có thể là phần tử của một tuple cha. let tuple_of_tuples = ((1u8, 2u16, 2u32), (4u64, -1i8), -2i16); // Tuples có thể in được dữ liệu. println!("tuple of tuples: {:?}", tuple_of_tuples); // Nhưng các tuple dài (có nhiều hơn 12 phần tử) lại không thể in ra dữ liệu // let too_long_tuple = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13); // println!("too long tuple: {:?}", too_long_tuple); // TODO ^ Thử xóa 2 dòng comment trên để xem trình biên dịch báo lỗi gì. let pair = (1, true); println!("pair is {:?}", pair); println!("the reversed pair is {:?}", reverse(pair)); // Để tạo một tuple chỉ có một phần tử, bắt buộc cần phải có dấu phẩy ở cuối // để phân biệt tuple đó với một literal nằm trong một cặp dấu ngoặc đơn. println!("one element tuple: {:?}", (5u32,)); println!("just an integer: {:?}", (5u32)); // Có thể hủy cấu trúc (destructured) tuple thành các phần tử bên trong nó bằng cách // dùng binding (đã nói ở trên). let tuple = (1, "hello", 4.5, true); let (a, b, c, d) = tuple; println!("{:?}, {:?}, {:?}, {:?}", a, b, c, d); let matrix = Matrix(1.1, 1.2, 2.1, 2.2); println!("{:?}", matrix); }
Thực hành
-
Hãy triển khai
fmt::Display
trait choMatrix
struct ở ví dụ trên, từ đó bạn có thể đổi việc in dữ liệu từ định dạng debug{:?}
sang định dạng display{}
, output mong đợi sẽ như sau:( 1.1 1.2 ) ( 2.1 2.2 )
Bạn có thể sẽ cần mở lại ví dụ về print display ở đây.
-
Thêm một hàm
transpose
(chuyển vị) sử dụng hàmreverse
đã viết ở trên làm mẫu, hàm này sẽ chấp nhận một matrix làm đối số, và trả về một matrix mà so với matrix đầu vào, các hàng được thay thế bằng các cột và ngược lại. Ví dụ như sau:println!("Matrix:\n{}", matrix); println!("Transpose:\n{}", transpose(matrix));
kết quả mong đợi trên output như sau:
Matrix: ( 1.1 1.2 ) ( 2.1 2.2 ) Transpose: ( 1.1 2.1 ) ( 1.2 2.2 )
Arrays và Slices
Array (Mảng) là một tập hợp các đối tượng có cùng kiểu dữ liệu T
, được lưu trữ liền nhau
trong bộ nhớ. Mảng được tạo bằng cách sử dụng dấu ngoặc vuông []
và độ dài của chúng,
được xác định tại thời điểm biên dịch, mảng có type signature là [T; length]
.
Slices (Lát cắt dữ liệu) cũng tương tự như mảng, nhưng độ dài của chúng không được xác định
tại thời điểm biên dịch. Thay vào đó, một slice là một two-word object, từ đầu tiên là một con trỏ trỏ đến dữ liệu,
và từ thứ hai là chiều dài của slice. Kích thước của từ tương tự như usize, được xác định bởi kiến trúc của bộ
xử lý, ví dụ 64 bit trên x86-64. Slices được sử dụng để borrow (mượn quyền sử dụng) một phần của mảng và có type signature là &[T]
.
use std::mem; // Hàm này borrow một slice fn analyze_slice(slice: &[i32]) { println!("first element of the slice: {}", slice[0]); println!("the slice has {} elements", slice.len()); } fn main() { // Mảng có kích thước cố định (ở đây có chú thích type signature của mảng, // nhưng việc chú thích này là không bắt buộc) let xs: [i32; 5] = [1, 2, 3, 4, 5]; // Tất cả các phần tử trong mảng có thể được khởi tạo với cùng một giá trị. let ys: [i32; 500] = [0; 500]; // Chỉ mục các phần tử của mảng bắt đầu từ 0 println!("first element of the array: {}", xs[0]); println!("second element of the array: {}", xs[1]); // `len` trả về số lượng phần tử có trong mảng println!("number of elements in array: {}", xs.len()); // Mảng được cấp phát vùng nhớ trên bộ nhớ stack println!("array occupies {} bytes", mem::size_of_val(&xs)); // Mảng có thể được tự động borrow dưới dạng slices println!("borrow the whole array as a slice"); analyze_slice(&xs); // Slices có thể trỏ tới một phần của một mảng // dưới dạng [starting_index..ending_index] // starting_index là vị trí đầu tiên của slice // ending_index là vị trí cuối cùng của slice cộng thêm 1 println!("borrow a section of the array as a slice"); analyze_slice(&ys[1 .. 4]); // Ví dụ về slice rỗng `&[]` let empty_array: [u32; 0] = []; assert_eq!(&empty_array, &[]); assert_eq!(&empty_array, &[][..]); // giống như trên, nhưng dài dòng hơn // Truy cập tới chỉ mục ngoài giới hạn của mảng sẽ gây ra // lỗi biên dịch. //println!("{}", xs[5]); }
Xem thêm tại đây:
Custom Types (Kiểu dữ liệu tuỳ chỉnh)
Các kiểu dữ liệu tùy chỉnh của Rust được tạo chủ yếu thông qua hai từ khóa sau:
struct
: định nghĩa một cấu trúcenum
: định nghĩa một enumeration (kiểu liệt kê)
Constants (Hằng số) có thể được tạo thông qua từ khóa const
và static
.
Structures (Cấu trúc)
Có ba kiểu cấu trúc mà có thể tạo được qua từ khóa struct
:
- Tuple structs, về cơ bản là các tuple được đặt tên.
- Structs truyền thống như trong ngôn ngữ C.
- Unit structs, không có các trường dữ liệu, hữu ích cho mục đích làm generics.
// Đây là một attribute dùng để ẩn cảnh báo về những đoạn code không sử dụng. #![allow(dead_code)] #[derive(Debug)] struct Person { name: String, age: u8, } // Một unit struct struct Unit; // Một tuple struct struct Pair(i32, f32); // Một struct truyền thống với hai trường struct Point { x: f32, y: f32, } // Các struct có thể được tái sử dụng để làm trường dữ liệu cho struct khác. struct Rectangle { // Một hình chữ nhật có thể được xác định bằng vị trí top left (điểm trên cùng bên trái) // và bottom right (điểm dưới cùng bên phải) của nó trong không gian. top_left: Point, bottom_right: Point, } fn main() { // Tạo một `Person` struct let name = String::from("Peter"); let age = 27; let peter = Person { name, age }; // In struct bằng chế độ debug println!("{:?}", peter); // Khởi tạo một `Point` let point: Point = Point { x: 10.3, y: 0.4 }; // Truy cập các trường của điểm trên println!("point coordinates: ({}, {})", point.x, point.y); // Tạo một điểm mới bằng cú pháp cập nhật struct để lấy dữ liệu các trường còn lại từ một struct khác. let bottom_right = Point { x: 5.2, ..point }; // `bottom_right.y` sẽ giống như `point.y` bởi vì thông qua cú pháp cập nhật struct, chúng ta đã // lấy trường này từ point. println!("second point: ({}, {})", bottom_right.x, bottom_right.y); // Destructure point bằng `let` binding let Point { x: left_edge, y: top_edge } = point; let _rectangle = Rectangle { // Việc tạo một struct cũng là một biểu thức top_left: Point { x: left_edge, y: top_edge }, bottom_right: bottom_right, }; // Tạo một unit struct let _unit = Unit; // Tạo một tuple struct let pair = Pair(1, 0.1); // Truy cập các trường của một tuple struct println!("pair contains {:?} and {:?}", pair.0, pair.1); // Destructure một tuple struct let Pair(integer, decimal) = pair; println!("pair contains {:?} and {:?}", integer, decimal); }
Thực hành
- Viết hàm
rect_area
để tính diện tích của mộtRectangle
(thử sử dụng nested destructuring). - Viết một hàm
square
lấy mộtPoint
và một số thựcf32
làm đối số, trả về mộtRectangle
có top left là điểm đầu vào, còn chiều rộng và chiều dài bằng nhau và bằng số thực đầu vào.
See also
Enums (Kiểu liệt kê)
Từ khóa enum
cho phép tạo ra một kiểu dữ liệu đặc biệt mà cho phép một biến có giá trị
là một biến thể trong một tập hợp các biến thể được định sẵn.
// Tạo một ra `enum` để phân loại các sự kiện web. Chú ý cách mà hai // thông tin về tên và kiểu được dùng để xác định một biến thể: // `PageLoad != PageUnload` và `KeyPress(char) != Paste(String)`. // Mỗi một biến thể trong `enum` đều là khác nhau và độc lập. enum WebEvent { // Giống unit structs, PageLoad, PageUnload, // giống tuple structs, KeyPress(char), Paste(String), // giống structs truyền thống. Click { x: i64, y: i64 }, } // Một hàm lấy `WebEvent` enum làm đối số và không trả về giá trị. fn inspect(event: WebEvent) { match event { WebEvent::PageLoad => println!("page loaded"), WebEvent::PageUnload => println!("page unloaded"), // Destructure `c` từ bên trong `enum`. WebEvent::KeyPress(c) => println!("pressed '{}'.", c), WebEvent::Paste(s) => println!("pasted \"{}\".", s), // Destructure `Click` thành `x` và `y`. WebEvent::Click { x, y } => { println!("clicked at x={}, y={}.", x, y); }, } } fn main() { let pressed = WebEvent::KeyPress('x'); // `to_owned()` tạo một owned `String` từ một string slice let pasted = WebEvent::Paste("my text".to_owned()); let click = WebEvent::Click { x: 20, y: 80 }; let load = WebEvent::PageLoad; let unload = WebEvent::PageUnload; inspect(pressed); inspect(pasted); inspect(click); inspect(load); inspect(unload); }
Type aliases (Đặt bí danh)
Nếu bạn sử dụng type alias, bạn có thể dùng các biến thể trong enum thông qua alias (bí danh) của nó. Điều này có lẽ sẽ hữu ích nếu tên của enum quá dài hoặc quá chung chung, và bạn muốn đổi tên nó.
enum VeryVerboseEnumOfThingsToDoWithNumbers { Add, Subtract, } // Tạo một type alias type Operations = VeryVerboseEnumOfThingsToDoWithNumbers; fn main() { // Chúng ta có thể dùng các biến thể của `enum` thông qua alias của nó. let x = Operations::Add; }
Nơi mà bạn có thể thấy việc sử dụng alias thường xuyên nhất, đó là ở các khối lệnh impl
, chúng sử dụng Self
alias.
enum VeryVerboseEnumOfThingsToDoWithNumbers { Add, Subtract, } impl VeryVerboseEnumOfThingsToDoWithNumbers { fn run(&self, x: i32, y: i32) -> i32 { match self { Self::Add => x + y, Self::Subtract => x - y, } } }
Để học thêm về type aliases, bạn có thể đọc stabilization report của nó.
Xem thêm tại đây:
match
, fn
, String
, ToOwned
và "Type alias enum variants" RFC
use
Sử dụng từ khóa use
để dùng enum
mà không cần xác định scope một cách thủ công:
// Đây là một attribute dùng để ẩn cảnh báo về những đoạn code không sử dụng. #![allow(dead_code)] enum Status { Rich, Poor, } enum Work { Civilian, Soldier, } fn main() { // Sử dụng `use` để dùng các name trong `enum` mà không cần xác định // scope một cách thủ công use crate::Status::{Poor, Rich}; // Tự động `use` tất cả name nằm trong `Work`. use crate::Work::*; // Tương đương với `Status::Poor`. let status = Poor; // Tương đương với `Work::Civilian`. let work = Civilian; match status { // Không cần xác định phạm vi bởi đã sử dụng `use` Rich => println!("The rich have lots of money!"), Poor => println!("The poor have no money..."), } match work { // Không cần xác định phạm vi bởi đã sử dụng `use` Civilian => println!("Civilians work!"), Soldier => println!("Soldiers fight!"), } }
Xem thêm tại đây:
C-like
enum
trong Rust cũng có thể ở dạng C-like enums (enum trong ngôn ngữ C).
// Đây là một attribute dùng để ẩn cảnh báo về những đoạn code không sử dụng. #![allow(dead_code)] // enum với giá trị phần tử là ngầm định (gán theo vị trí, bắt đầu từ 0) enum Number { Zero, // 0 One, // 1 Two, // 2 } // enum với giá trị phần tử được gán cụ thể enum Color { Red = 0xff0000, Green = 0x00ff00, Blue = 0x0000ff, } fn main() { // `enums` có thể ép kiểu sang số nguyên. println!("zero is {}", Number::Zero as i32); println!("one is {}", Number::One as i32); println!("roses are #{:06x}", Color::Red as i32); println!("violets are #{:06x}", Color::Blue as i32); }
Xem thêm tại đây:
Testcase: linked-list (Danh sách liên kết)
Một cách thông dụng để triển khai một danh sách liên kết trong Rust là thông qua enums
:
use crate::List::*; enum List { // Cons: Là một tuple struct mà nó bọc một phần tử và một con trỏ để trỏ // tới nút tiếp theo. Cons(u32, Box<List>), // Nil: Một nút rỗng biểu thị sự kết thúc của linked list Nil, } // Triển khai các phương thức cho enum trên impl List { // Tạo một danh sách trống fn new() -> List { // `Nil` là một biến thể trong `List` Nil } // Hàm này dùng một danh sách và trả về cũng danh sách đó nhưng thêm một // phần tử mới ở đầu fn prepend(self, elem: u32) -> List { // `Cons` cũng là một biến thể trong List Cons(elem, Box::new(self)) } // Trả về độ dài của danh sách fn len(&self) -> u32 { // `self` phải được đối sánh (match) ở đây, bởi vì hành vi của phương thức này // phụ thuộc vào biến thể của `self`. // `self` có kiểu `&List`, `*self` có kiểu `List`, đối sánh trên một kiểu cụ thể // `T` được khuyên dùng hơn là đối sánh trên tham chiếu `&T`. // Tuy nhiên sau Rust 2018 (phiên bản 1.26 trở đi), bạn có thể sử dụng `self` // và tail (không cần ref), Rust sẽ tự động suy diễn ra &s và ref tail cho bạn. // Xem tại https://doc.rust-lang.org/edition-guide/rust-2018/ownership-and-lifetimes/default-match-bindings.html match *self { // Không lấy được quyền sở hữu của tail, bởi `self` chỉ là đi mượn, // nên thay vào đó, ta dùng một tham chiếu tới tail. Cons(_, ref tail) => 1 + tail.len(), // Trường hợp gốc: Một danh sách trống thì có độ dài = 0 Nil => 0 } /* Với Rust 1.26 trở đi, bạn có thể viết code ngắn gọn và thân thiện hơn như sau: match self { Cons(_, ref tail) => 1 + tail.len(), Nil => 0 } */ } // Trả về biểu diễn của danh sách dưới dạng một String phân bổ trong bộ nhớ heap fn stringify(&self) -> String { match *self { Cons(head, ref tail) => { // `format!` tương tự như `print!`, nhưng nó trả về một String // được phân bổ bộ nhớ heap thay vì in dữ liệu ra màn hình. format!("{}, {}", head, tail.stringify()) }, Nil => { format!("Nil") }, } /* Với Rust 1.26 trở đi: match self { Cons(head, tail) => { format!("{}, {}", head, tail.stringify()) }, Nil => { format!("Nil") }, } */ } } fn main() { // Tạo một linked-list trống let mut list = List::new(); // Thêm một số nút phần tử list = list.prepend(1); list = list.prepend(2); list = list.prepend(3); // In ra trạng thái cuối cùng của danh sách println!("linked list has length: {}", list.len()); println!("{}", list.stringify()); }
Xem thêm tại đây:
constants (Hằng số)
Rust có hai loại hằng số khác nhau có thể được khai báo trong bất kỳ phạm vi nào, kể cả global. Cả hai đều yêu cầu phải chú thích kiểu rõ ràng:
const
: Một giá trị không thể thay đổi (trường hợp thường gặp).static
: Một static variable (biến tĩnh) là một biến có thời gian tồn tại là'static
('static
lifetime). Static lifetime của nó được Rust tự suy ra và không cần phải được chỉ định. Một static variable chỉ có thể thay đổi giá trị được nếu đi kèm với từ khóamut
(trở thành một static mutable variable) nhưng việc truy cập hoặc sửa đổi một static mutable variable làunsafe
(không an toàn), muốn thực hiện được thì phải đưa vào trong một khối lệnhunsafe
.
// Các hằng số có thể khai báo ở global, nằm ngoài mọi phạm vi. static LANGUAGE: &str = "Rust"; static mut LANGUAGE_2 : &str = "Rust boy"; const THRESHOLD: i32 = 10; fn is_big(n: i32) -> bool { // Truy cập tới hằng số trong phạm vi hàm is_big n > THRESHOLD } fn main() { let n = 16; // Truy cập tới hằng số trong hàm main println!("This is {}", LANGUAGE); println!("The threshold is {}", THRESHOLD); println!("{} is {}", n, if is_big(n) { "big" } else { "small" }); // Lỗi! Không thể sửa một hằng số. THRESHOLD = 5; LANGUAGE = "C"; // FIXME ^ Hãy comment dòng các trên lại. // Lỗi! Truy cập hoặc sửa đổi một static mutable variable là không an toàn println!("This static mutable variable is {}", LANGUAGE_2); LANGUAGE_2 = "C++"; // FIXME ^ Hãy đưa hai dòng trên vào trong một `unsafe` block. }
Xem thêm tại đây:
The const
/static
RFC,
'static
lifetime
Variable Bindings (Ràng buộc giữa kiểu và giá trị với biến trong Rust)
Rust cung cấp sự an toàn cho kiểu (type safety) bằng các kiểu tĩnh. Các ràng buộc biến có thể được chú thích kiểu khi khai báo. Tuy nhiên, trong hầu hết các trường hợp, trình biên dịch sẽ có thể để suy ra kiểu dữ liệu của biến thông qua ngữ cảnh, giảm đáng kể gánh nặng cho việc chú thích.
Các giá trị (như literals) có thể được gán vào cho các biến, thông qua let
binding.
fn main() { let an_integer = 1u32; let a_boolean = true; let unit = (); // copy `an_integer` vào `copied_integer` let copied_integer = an_integer; println!("An integer: {:?}", copied_integer); println!("A boolean: {:?}", a_boolean); println!("Meet the unit value: {:?}", unit); // Trình biên dịch sẽ cảnh báo về việc có tạo ràng buộc biến mà không sử dụng; // những cảnh báo này sẽ đươc tắt nến bạn thêm trước tên biến một dấu gạch dưới. let _unused_variable = 3u32; let noisy_unused_variable = 2u32; // FIXME ^ Thêm dấu gạch dưới phía trước để loại bỏ cảnh báo // Có điều là những cảnh báo này sẽ không hiển thị trên trình duyệt // Bạn có thể thấy chúng khi cope đoạn lệnh này vào trong code editor của bạn. }
Mutability (Tính khả biến)
Các ràng buộc biến là immutable (bất biến) theo mặc định; nhưng có thể ghi đè chúng
nếu sử dụng từ khóa mut
để kích hoạt tính khả biến cho ràng buộc biến.
fn main() { let _immutable_binding = 1; let mut mutable_binding = 1; println!("Before mutation: {}", mutable_binding); // Ok mutable_binding += 1; println!("After mutation: {}", mutable_binding); // Lỗi! _immutable_binding += 1; // FIXME ^ Comment dòng trên lại }
Trình biên dịch sẽ đưa ra một chẩn đoán chi tiết về các lỗi về tính khả biến của ràng buộc biến.
Scope (Phạm vi) và Shadowing
Các ràng buộc biến đều có một phạm vi dùng để giới hạn sự tồn tại của nó trong một block (khối lệnh). Một
block là một tập hợp các câu lệnh được bao bởi dấu ngoặc nhọn {}
.
fn main() { // Ràng buộc này tồn tại trong hàm main let long_lived_binding = 1; // Đây là một block code, có scope nhỏ hơn so với hàm main { // Ràng buộc này chỉ tồn tại trong block này let short_lived_binding = 2; println!("inner short: {}", short_lived_binding); } // Kết thúc của block // Lỗi! `short_lived_binding` không tồn tại trong phạm vi này println!("outer short: {}", short_lived_binding); // FIXME ^ Comment dòng lệnh trên println!("outer long: {}", long_lived_binding); }
Variable shadowing (ẩn biến) cũng được cho phép trong Rust. Trong lập trình nói chung, variable shadowing xảy ra khi một biến được khai báo trong một phạm vi nhất định (block, method hoặc inner class) có cùng tên với một biến được khai báo trong phạm vi bên ngoài hoặc cùng phạm vi nhưng ở trước.
fn main() { // Đây là một ràng buộc biến let shadowed_binding = 1; { println!("before being shadowed: {}", shadowed_binding); // Ràng buộc này *shadows* shadowed_binding bên ngoài let shadowed_binding = "abc"; println!("shadowed in inner block: {}", shadowed_binding); } println!("outside inner block: {}", shadowed_binding); // Ràng buộc này *shadows* ràng buộc trước (let shadowed_binding = 1;) let shadowed_binding = 2; println!("shadowed in outer block: {}", shadowed_binding); }
Declare first (Khai báo trước, khởi tạo sau)
Có thể khai báo các ràng buộc biến trước rồi khởi tạo chúng sau. Tuy nhiên, hình thức này hiếm khi được sử dụng bởi vì nó có thể dẫn đến việc sử dụng những biến chưa được khởi tạo.
fn main() { // Khai báo một ràng buộc biến let a_binding; { let x = 2; // Khởi tạo ràng buộc biến đã khai báo phía trên a_binding = x * x; } println!("a binding: {}", a_binding); let another_binding; // Lỗi! Không được sử dụng ràng buộc chưa khởi tạo println!("another binding: {}", another_binding); // FIXME ^ Comment dòng trên lại another_binding = 1; println!("another binding: {}", another_binding); }
Trình biên dịch cấm sử dụng các biến chưa được khởi tạo, vì điều này sẽ dẫn đến những hành vi không xác định.
Freezing (Đóng băng dữ liệu)
Khi dữ liệu của một ràng buộc biến bị shadow bởi một ràng buộc có tính bất biến thì nó cũng sẽ bị freezes (đóng băng). Dữ liệu bị đóng băng sẽ không có khả năng sửa đổi cho đến khi ra khỏi phạm vi của ràng bụôc bất biến trên:
fn main() { // Ràng buộc biến này có tính khả biến let mut _mutable_integer = 7i32; { // Shadowing bởi một `_mutable_integer` bất biến let _mutable_integer = _mutable_integer; // Lỗi! Vì `_mutable_integer` bị đóng băng ở scope này _mutable_integer = 50; // FIXME ^ Comment dòng trên lại // `_mutable_integer` đi ra khỏi scope này } // Không có lỗi! Vì `_mutable_integer` không bị đóng băng ở scope này _mutable_integer = 3; }
Types (Kiểu dữ liệu)
Rust cung cấp nhiều cơ chế để thay đổi hoặc định nghĩa các primitive types (kiểu dữ liệu nguyên thủy) và các kiểu do người dùng tự định nghĩa. Các phần sau đây bao gồm:
- Casting (Ép kiểu) giữa các primitive types
- Chỉ định kiểu literals mong muốn
- Sử dụng type inference (Suy luận ra kiểu mà không cần chú thích kiểu)
- Aliasing types (Đặt bí danh cho kiểu)
Casting (Ép kiểu)
Rust không cung cấp ép kiểu ngầm định (implicit) giữa các kiểu dữ liệu nguyên thủy.
Tuy nhiên, ép kiểu tường minh (explicit) có thể được thực hiện bằng cách sử dụng từ khóa as
.
Các quy tắc chuyển đổi giữa các kiểu dữ liệu thường tuân theo các quy ước trong C, trừ các trường hợp có hành vi không xác định. Hành vi của tất cả các phép ép kiểu giữa các kiểu dữ liệu trong Rust phải được xác định rõ ràng.
// Loại bỏ tất cả các cảnh báo từ các phép ép kiểu bị tràn overflow. #![allow(overflowing_literals)] fn main() { let decimal = 65.4321_f32; // Lỗi! Vì không có ép kiểu ngầm định let integer: u8 = decimal; // FIXME ^ Comment dòng này lại // Ép kiểu tường minh let integer = decimal as u8; let character = integer as char; // Lỗi! Có những giới hạn trong các quy tắc chuyển đổi. // Một số thực float không thể được chuyển đổi trực tiếp thành một ký tự char. let character = decimal as char; // FIXME ^ Comment dòng này lại println!("Casting: {} -> {} -> {}", decimal, integer, character); // khi truyền bất kỳ giá trị nào sang kiểu số không âm, T, // T::MAX + 1 sẽ được dùng để cộng hoặc trừ cho đến khi giá trị // phù hợp với kiểu mới // 1000 trở thành một số u16 println!("1000 as a u16 is: {}", 1000 as u16); // 1000 - 256 - 256 - 256 = 232 // Phép ép kiểu này hoạt động như sau, 8 bit thấp (bit ở cực phải - LSB) của 1000 được giữ lại, // còn các bit cao (bit ở cực trái - MSB) sẽ bị cắt bớt. println!("1000 as a u8 is : {}", 1000 as u8); // -1 + 256 = 255 println!(" -1 as a u8 is : {}", (-1i8) as u8); // Với số nguyên dương thì phép ép kiểu trên cũng giống như phép chia lấy dư println!("1000 mod 256 is : {}", 1000 % 256); // Khi ép kiểu sang kiểu số nguyên có dấu, kết quả (theo bit) cũng giống // như ép kiểu sang kiểu số nguyên không dấu tương ứng. Nếu bit cao nhất của // kết quả là 1, thì giá trị là âm. Ngược lại, nếu bằng 0, thì giá trị là dương. // Tất nhiên là nếu số đó nằm trong khoảng giá trị của kiểu dữ liệu số cần ép, // thì không có gì thay đổi. println!(" 128 as a i16 is: {}", 128 as i16); // 128 as u8 -> -128 println!(" 128 as a i8 is : {}", 128 as i8); // 1000 as u8 -> 232 println!("1000 as a u8 is : {}", 1000 as u8); // 232 as i8 -> -24 println!(" 232 as a i8 is : {}", 232 as i8); // Kể từ phiên bản Rust 1.45, từ khóa `as` thực hiện một *saturating cast* // khi ép kiểu từ float sang int. Tức là nếu giá trị cuả số thực vượt quá // giới hạn trên hoặc nhỏ hơn giới hạn dưới, giá trị trả về // sẽ bằng với giới hạn biên của kiểu dữ liệu. // 300.0 -> 255 println!("300.0 is {}", 300.0_f32 as u8); // -100.0 as u8 -> 0 println!("-100.0 as u8 is {}", -100.0_f32 as u8); // nan as u8 -> 0 println!("nan as u8 is {}", f32::NAN as u8); // This behavior incurs a small runtime cost and can be avoided // with unsafe methods, however the results might overflow and // return **unsound values**. Use these methods wisely: // *Saturating cast* phát sinh một khoản chi phí thời gian chạy nhỏ nhưng có thể // tránh được với các unsafe method (phương thức không an toàn), tuy nhiên, kết quả // có thể tràn và trả về giá trị chưa tìm thấy **unsound values**. Nên nhớ hãy sử // dụng các phương pháp này một cách khôn ngoan: unsafe { // 300.0 -> 44 println!("300.0 is {}", 300.0_f32.to_int_unchecked::<u8>()); // -100.0 as u8 -> 156 println!("-100.0 as u8 is {}", (-100.0_f32).to_int_unchecked::<u8>()); // nan as u8 -> 0 println!("nan as u8 is {}", f32::NAN.to_int_unchecked::<u8>()); } }
Literals
Numeric literals có thể được chú thích kiểu bằng việc thêm vào suffix. Như ví dụ dưới đây,
ta có thể chỉ định rằng literal 42
sẽ có kiểu là i32
, bằng cách viết 42i32
.
Các numeric literals mà không có suffix thì kiểu của nó sẽ được xác định theo cách mà nó
được sử dụng. Nếu không có ràng buộc nào xuất hiện, trình biên dịch sẽ sử dụng i32
cho các
số nguyên, và f64
cho các số thực dấu phẩy động.
fn main() { // Literals có suffix, kiểu của chúng được xác định lúc khởi tạo biến let x = 1u8; let y = 2u32; let z = 3f32; // Literals không có suffix, kiểu phụ thuộc vào cách chúng được sử dụng let i = 1; let f = 1.0; // `size_of_val` trả về kích thước của biến theo bytes println!("size of `x` in bytes: {}", std::mem::size_of_val(&x)); println!("size of `y` in bytes: {}", std::mem::size_of_val(&y)); println!("size of `z` in bytes: {}", std::mem::size_of_val(&z)); println!("size of `i` in bytes: {}", std::mem::size_of_val(&i)); println!("size of `f` in bytes: {}", std::mem::size_of_val(&f)); }
Có một số khái niệm được sử dụng trong đoạn mã code trên chưa được giải thích, dưới đây là giải thích ngắn gọn cho chúng:
std::mem::size_of_val
là một hàm, nhưng được gọi dưới dạng full path. Code được chia thành các đơn vị logic gọi là modules. Trong trường hợp này, hàmsize_of_val
được định nghĩa trongmem
module, vàmem
module được định nghĩa trongstd
crate. Để biết thêm chi tiết, hãy xem thêm về modules và crates.
Inference (Suy luận kiểu)
Công cụ suy luận kiểu của Rust khá thông minh. Nó không chỉ nhìn vào kiểu của giá trị trong biểu thức khởi tạo. Nó còn xem xét cách các biến được sử dụng sau đó để suy ra kiểu của chúng. Đây là một ví dụ nâng cao về suy luận kiểu trong Rust:
fn main() { // Bởi vì ở đây có chú thích kiểu, trình biên dịch biết rằng `elem` có kiểu u8. let elem = 5u8; // Tạo một vector trống (một mảng có thể thêm phần tử). let mut vec = Vec::new(); // Tại thời điểm này, trình biên dịch không biết chính xác kiểu của `vec`, nó // chỉ biết rằng đây la một vector gì đó (`Vec<_>`). // Chèn thêm `elem` vào vector. vec.push(elem); // Và ở đây trình biên dịch đã biết `vec` là một vector của các số `u8` (`Vec<u8>`) // TODO ^ Thử comment dòng `vec.push(elem)` lại println!("{:?}", vec); }
Như vậy là ở đây không cần chú thích kiểu cho các biến, trình biên dịch rất vui và lập trình viên cũng vậy!
Aliasing (Đặt bí danh cho kiểu)
Từ khóa type
có thể được sử dụng để tạo ra một tên mới cho một kiểu đã tồn tại. Các kiểu
phải có tên dạng UpperCamelCase
, nếu không trình biên dịch sẽ ném ra một cảnh báo. Ngoại
lệ cho quy tắc trên là các kiểu dữ liệu nguyên thủy: usize
, f32
, etc.
// `NanoSecond`, `Inch`, và `U64` là các tên mới cho `u64`. type NanoSecond = u64; type Inch = u64; type U64 = u64; fn main() { // `NanoSecond` = `Inch` = `U64` = `u64`. let nanoseconds: NanoSecond = 5 as U64; let inches: Inch = 2 as U64; // Lưu ý rằng kiểu bí danh *không* cung cấp bất kỳ bổ sung nào, bởi vì // bí danh là *không phải* là một kiểu mới. // Như ở dưới đây ta có thể cộng được các đơn vị khác nhau, bởi bản chất // chúng đều có kiểu là số nguyên `u64` chứ không phải như tên bí danh là kiểu dữ liệu // cho đơn vị thời gian hay đơn vị độ dài. println!("{} nanoseconds + {} inches = {} unit?", nanoseconds, inches, nanoseconds + inches); }
Mục đích chính sử dụng các bí danh là để giảm thiểu việc dùng boilerplate; ví dụ như kiểu IoResult<T>
là một bí danh thay thế cho kiểu Result<T, IoError>
.
Xem thêm tại đây:
Conversion (Chuyển đổi)
Các kiểu dữ liệu nguyên thủy có thể chuyển đổi lẫn nhau thông qua casting (ép kiểu).
Rust xử lý việc chuyển đổi giữa các kiểu dữ liệu tùy chỉnh (ví dụ như struct
và enum
)
bằng việc sử dụng traits. Việc chuyển đổi nói chung là
sẽ sử dụng From
và Into
traits. Tuy nhiên, có những traits
cụ thể cho các trường
hợp phổ biến hơn, đặc biệt là khi chuyển đổi từ và sang String
.
From
và Into
Hai trait From
và Into
vốn dĩ có liên kết với nhau, và đây thực sự là một phần trong quá trình
triển khai chúng. Nếu bạn có thể chuyển đổi kiểu A từ kiểu B, thì ngược lại cũng có thể dễ dàng
chuyển kiểu B sang kiểu A.
From
Trait From
cho phép một kiểu xác định cách tạo ra chính nó từ một kiểu khác, do đó cung cấp một cơ chế
rất đơn giản để chuyển đổi giữa một số kiểu. Có nhiều cách triển khai trait này trong thư viện
tiêu chuẩn để chuyển đổi các kiểu dữ liệu nguyên thủy và thông dụng.
Ví dụ, chúng ta có thể dễ dàng chuyển đổi một str
thành một String
#![allow(unused)] fn main() { let my_str = "hello"; let my_string = String::from(my_str); }
Chúng ta cũng có thể làm điều tương tự để định nghĩa cách chuyển đổi kiểu cho kiểu tùy chỉnh của chúng ta.
use std::convert::From; #[derive(Debug)] struct Number { value: i32, } impl From<i32> for Number { fn from(item: i32) -> Self { Number { value: item } } } fn main() { let num = Number::from(30); println!("My number is {:?}", num); }
Into
Trait Into
đơn giản là phần tương hỗ của From
. Có nghĩa là, nếu bạn đã triển khai From
trait
cho kiểu của bạn, Into
sẽ gọi nó khi cần thiết.
Sử dụng trait Into
thường sẽ yêu cầu mô tả kỹ thuật của kiểu dữ liệu mục tiêu mà bạn muốn chuyển đổi tới vì trình biên dịch không thể xác định điều này. Tuy nhiên, đây chỉ là một sự đánh đổi nhỏ khi mà chúng ta nhận lại được một chức năng miễn phí.
use std::convert::From; #[derive(Debug)] struct Number { value: i32, } impl From<i32> for Number { fn from(item: i32) -> Self { Number { value: item } } } fn main() { let int = 5; // Try removing the type declaration let num: Number = int.into(); println!("My number is {:?}", num); }
TryFrom
và TryInto
Tương tự như From
và Into
, TryFrom
và TryInto
là
các trait phục vụ cho việc chuyển đổi giữa các kiểu. Nhưng không giống From
/Into
,
TryFrom
/TryInto
được sử dụng cho các chuyển đổi không ổn định (có thể thất bại) và
trả về Result
s.
use std::convert::TryFrom; use std::convert::TryInto; #[derive(Debug, PartialEq)] struct EvenNumber(i32); impl TryFrom<i32> for EvenNumber { type Error = (); fn try_from(value: i32) -> Result<Self, Self::Error> { if value % 2 == 0 { Ok(EvenNumber(value)) } else { Err(()) } } } fn main() { // TryFrom assert_eq!(EvenNumber::try_from(8), Ok(EvenNumber(8))); assert_eq!(EvenNumber::try_from(5), Err(())); // TryInto let result: Result<EvenNumber, ()> = 8i32.try_into(); assert_eq!(result, Ok(EvenNumber(8))); let result: Result<EvenNumber, ()> = 5i32.try_into(); assert_eq!(result, Err(())); }
To và from Strings
Chuyển đổi sang String
Chuyển đổi bất kỳ kiểu nào sang String
cũng đơn giản như việc triển khai trait ToString
cho kiểu đó. Thay vì làm như vậy trực tiếp, bạn nên triển khai trait
fmt::Display
, thứ mà sẽ tự động cung cấp ToString
và cũng cho phép in kiểu như đã đề cập trong phần print!
.
use std::fmt; struct Circle { radius: i32 } impl fmt::Display for Circle { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "Circle of radius {}", self.radius) } } fn main() { let circle = Circle { radius: 6 }; println!("{}", circle.to_string()); }
Phân tích cú pháp (Parsing) một String
Một trong những kiểu phổ biến để chuyển đổi từ string là thành kiểu số. Cách tiếp cận cho việc chuyển đổi này là sử dụng hàm parse
rồi thông qua suy luận kiểu của Rust hoặc chỉ định kiểu cần phân tích cú pháp bằng cú pháp 'turbofish'. Cả hai lựa chọn này được đề cập trong ví dụ bên dưới.
Điều này sẽ chuyển đổi string thành kiểu được chỉ định miễn là trait FromStr
được triển khai cho kiểu đó. Nó được thực hiện cho nhiều kiểu trong thư viện tiêu chuẩn. Để có được chức năng này trên một kiểu do người dùng tự định nghĩa, chỉ cần triển khai trait FromStr
cho kiểu đó.
fn main() { let parsed: i32 = "5".parse().unwrap(); let turbo_parsed = "10".parse::<i32>().unwrap(); let sum = parsed + turbo_parsed; println!("Sum: {:?}", sum); }
Expressions (Biểu thức trong Rust)
Một chương trình Rust (hầu hết) được tạo thành từ một loạt các câu lệnh (statement):
fn main() { // câu lệnh // câu lệnh // câu lệnh }
Có một số loại câu lệnh trong Rust. Hai loại phổ biến nhất là khai báo
ràng buộc biến và sử dụng biểu thức với dấu ;
ở cuối:
fn main() { // ràng buộc biến let x = 5; // biểu thức; x; x + 1; 15; }
Các khối lệnh cũng là biểu thức, vì vậy chúng có thể được sử dụng làm vế phải trong
một phép gán. Biểu thức cuối cùng trong khối sẽ được gán cho vế trái chẳng hạn như một
biến cục bộ. Tuy nhiên, nếu biểu thức cuối cùng của khối kết thúc bằng
dấu chấm phẩy, giá trị trả về sẽ là ()
.
fn main() { let x = 5u32; let y = { let x_squared = x * x; let x_cube = x_squared * x; // Giá trị của biểu thức này được gán cho `y` x_cube + x_squared + x }; let z = { // Dấu chấm phẩy loại bỏ biểu thức này và `()` được gán cho `z` 2 * x; }; println!("x is {:?}", x); println!("y is {:?}", y); println!("z is {:?}", z); }
Flow of Control (Cấu trúc điều khiển trong Rust)
An integral part of any programming language are ways to modify control flow:
if
/else
, for
, and others. Let's talk about them in Rust.
Một phần thiết yếu của bất kỳ ngôn ngữ lập trình nào là cách để sửa đổi luồng điều khiển: if
/else
, for
, và nhiều thứ khác. Hãy cùng nói về chúng trong Rust.
if/else
Cấu trúc phân nhánh if
-else
trong Rust tương tự như các ngôn ngữ khác. Nhưng không giống như nhiều ngôn ngữ khác, điều kiện boolean không cần được bao quanh bởi dấu ngoặc đơn và mỗi điều kiện được theo sau bởi một khối lệnh. Các điều kiện if
-else
là các biểu thức và tất cả các nhánh phải trả về cùng một kiểu.
fn main() { let n = 5; if n < 0 { print!("{} is negative", n); } else if n > 0 { print!("{} is positive", n); } else { print!("{} is zero", n); } let big_n = if n < 10 && n > -10 { println!(", and is a small number, increase ten-fold"); // Biểu thức này trả về một số nguyên `i32`. 10 * n } else { println!(", and is a big number, halve the number"); // Biểu thức này cũng phải trả về một số nguyên `i32`. n / 2 // TODO ^ Hãy thử thêm dấu chấm phẩy ở đây và xem lỗi gì xảy ra. }; // ^ Đừng quên đặt dấu chấm phẩy ở cuối! Tất cả các `let` binding đều cần nó. println!("{} -> {}", n, big_n); }
loop
Rust cung cấp từ khóa loop
để biểu thị một vòng lặp vô hạn.
Câu lệnh break
có thể được sử dụng để thoát khỏi một vòng lặp bất cứ lúc nào, trong khi
câu lệnh continue
có thể được sử dụng để bỏ qua phần còn lại của lần lặp và bắt đầu lần lặp tiếp theo.
fn main() { let mut count = 0u32; println!("Let's count until infinity!"); // Vòng lặp vô hạn loop { count += 1; if count == 3 { println!("three"); // Bỏ qua phần còn lại của lần lặp hiện tại continue; } println!("{}", count); if count == 5 { println!("OK, that's enough"); // Thoát khỏi vòng lặp break; } } }
Nesting và labels (Vòng lặp lồng nhau và nhãn)
Có thể dùng break
và continue
ở các vòng lặp bên ngoài (outer loops) trong khi
xử lý vòng lặp bên trong (inner loops). Trong những trường hợp này, các vòng lặp phải được
chú thích bằng nhãn 'label
và sau đó, các nhãn này phải được truyền phía sau
lệnh break
/continue
.
#![allow(unreachable_code)] fn main() { 'outer: loop { println!("Entered the outer loop"); 'inner: loop { println!("Entered the inner loop"); // Lệnh break này sẽ chỉ break vòng lặp bên trong bởi vì nó không có nhãn //break; // Lệnh break này sẽ break vòng lặp ngoài bởi vì nó có nhãn `'outer` break 'outer; } println!("This point will never be reached"); } println!("Exited the outer loop"); }
Trả về giá trị từ loop
Một trong những cách sử dụng của loop
đó là thử đi thử lại một thao tác
cho đến khi nó thành công. Tuy nhiên, nếu thao tác đó trả về một giá trị, bạn có thể
cần phải truyền nó cho phần còn lại của mã code: đặt giá trị sau lệnh break
và nó
sẽ được trả về bởi biểu thức loop
.
fn main() { let mut counter = 0; let result = loop { counter += 1; if counter == 10 { break counter * 2; } }; assert_eq!(result, 20); }
while
Từ khóa while
có thể được sử dụng để chạy một vòng lặp với một điều kiện,
vòng lặp sẽ không kết thúc cho đến khi điều kiện trở thành sai.
Hãy thử viết trò chơi FizzBuzz nổi tiếng bằng cách sử dụng vòng lặp while
.
fn main() { // Biến đếm let mut n = 1; // Vòng lặp white với điều kiện `n` nhỏ hơn 101 while n < 101 { if n % 15 == 0 { println!("fizzbuzz"); } else if n % 3 == 0 { println!("fizz"); } else if n % 5 == 0 { println!("buzz"); } else { println!("{}", n); } // Tăng biến đếm lên 1 n += 1; } }
for loops
for và range
Cấu trúc for in
có thể được sử dụng để chạy một vòng lặp thông qua một Iterator
. Một trong những cách dễ nhất để tạo một iterator là sử dụng ký hiệu phạm vi (range notation) là a..b
. Điều này sẽ giúp iterator lướt qua các giá trị từ a
(bao gồm cả a
) đến b
(không bao gồm b
) trong mỗi một bước lặp.
Hãy thử viết FizzBuzz bằng for
thay cho while
.
fn main() { // `n` sẽ lấy các giá trị: 1, 2, ..., 100 tại mỗi lần lặp for n in 1..101 { if n % 15 == 0 { println!("fizzbuzz"); } else if n % 3 == 0 { println!("fizz"); } else if n % 5 == 0 { println!("buzz"); } else { println!("{}", n); } } }
Ngoài ra, a..=b
có thể được sử dụng cho một phạm vi mà nó bao gồm cả hai điểm đầu và cuối. Phần code bên trên có thể được viết lại là:
fn main() { // `n` sẽ lấy các giá trị: 1, 2, ..., 100 tại mỗi lần lặp for n in 1..=100 { if n % 15 == 0 { println!("fizzbuzz"); } else if n % 3 == 0 { println!("fizz"); } else if n % 5 == 0 { println!("buzz"); } else { println!("{}", n); } } }
for và iterators
Cấu trúc for in
có thể tương tác với một Iterator
theo một số cách.
Như đã thảo luận trong phần về trait Iterator, theo mặc định, vòng lặp for
sẽ
áp dụng hàm into_iter
cho collection. Tuy nhiên, đây không phải là phương tiện duy nhất để chuyển đổi collection thành iterator.
into_iter
, iter
và iter_mut
đều xử lý việc chuyển đổi một collection thành iterator theo những cách khác nhau, bằng cách cung cấp các chế độ xem khác nhau về dữ liệu bên trong.
iter
- Nó sẽ mượn từng phần tử của collection qua mỗi lần lặp lại. Do đó collection sẽ không bị ảnh hưởng và có sẵn để sử dụng lại sau vòng lặp.
fn main() { let names = vec!["Bob", "Frank", "Ferris"]; for name in names.iter() { match name { &"Ferris" => println!("There is a rustacean among us!"), // TODO ^ Hãy thử xoá & và chỉ matching với "Ferris" _ => println!("Hello {}", name), } } println!("names: {:?}", names); }
into_iter
- Nó sẽ sử dụng collection để trên mỗi lần lặp lại, dữ liệu chính xác được cung cấp. Một khi collection đã được sử dụng, nó không còn có sẵn để sử dụng lại vì nó đã được 'moved' trong vòng lặp.
fn main() { let names = vec!["Bob", "Frank", "Ferris"]; for name in names.into_iter() { match name { "Ferris" => println!("There is a rustacean among us!"), _ => println!("Hello {}", name), } } println!("names: {:?}", names); // FIXME ^ Comment dòng trên lại }
iter_mut
- Nó có thể vay mượn từng phần tử của collection, cho phép collection được sửa đổi tại chỗ.
fn main() { let mut names = vec!["Bob", "Frank", "Ferris"]; for name in names.iter_mut() { *name = match name { &mut "Ferris" => "There is a rustacean among us!", _ => "Hello", } } println!("names: {:?}", names); }
Trong các đoạn mã trên, hãy lưu ý đến match
, đó là sự khác biệt chính trong các loại lặp.
Sự khác biệt về loại tất nhiên ngụ ý dẫn đến các hành động khác nhau có thể được thực hiện.
Xem thêm tại đây:
match
Rust cung cấp một pattern matching (đối sánh mẫu) với từ khóa match
, có thể được sử dụng giống
như switch
trong C. Nhánh (arm
) đầu tiên khớp dữ liệu sẽ được đánh giá và tất
cả các giá trị có thể có phải được bao phủ trong các nhánh (độ bao phủ bắt buộc là 100%).
fn main() { let number = 13; // TODO ^ Thử một giá trị khác cho `number` println!("Tell me about {}", number); match number { // Match với 1 giá trị đơn 1 => println!("One!"), // Match với nhiều giá trị 2 | 3 | 5 | 7 | 11 => println!("This is a prime"), // TODO ^ Thử thêm 13 vào danh sách các số lẻ trên // Match với một phạm vi 13..=19 => println!("A teen"), // Xử lý các trường hợp còn lại _ => println!("Ain't special"), // TODO ^ Hãy thử comment dòng bên trên } let boolean = true; // Match cũng có thể là một biểu thức let binary = match boolean { // Các nhánh của một match phải cover tất cả các giá trị có thể có false => 0, true => 1, // TODO ^ Hãy thử comment một trong các nhánh trên }; println!("{} -> {}", boolean, binary); }
Destructuring (Tách dữ liệu)
Một khối match
có thể tách các kiểu cấu trúc trong Rust thành các phần tử theo nhiều cách khác nhau.
- Destructuring Tuples
- Destructuring Arrays and Slices
- Destructuring Enums
- Destructuring Pointers
- Destructuring Structures
tuples
Tuple có thể được tách ra các phần tử bằng một khối match
như sau:
fn main() { let triple = (0, -2, 3); // TODO ^ Thử với giá trị khác của triple println!("Tell me about {:?}", triple); // Match có thể được sử dụng để phân tách một tuple match triple { // Tách phần tử thứ 2 và thứ 3 (0, y, z) => println!("First is `0`, `y` is {:?}, and `z` is {:?}", y, z), (1, ..) => println!("First is `1` and the rest doesn't matter"), (.., 2) => println!("last is `2` and the rest doesn't matter"), (3, .., 4) => println!("First is `3`, last is `4`, and the rest doesn't matter"), // `..` có thể được sử dụng để bỏ qua phần còn lại của tuple _ => println!("It doesn't matter what they are"), // `_` có nghĩa là không gán trị cho một biến } }
Xem thêm tại đây:
arrays/slices
Cũng giống như tuple, các array và slice cũng có thể được phân tách bằng match
:
fn main() { // Hãy thử thay đổi giá trị trong array, hoặc biến nó thành một slice! let array = [1, -2, 6]; match array { // Gán phần tử thứ 2 và thứ 3 cho các biến tương ứng [0, second, third] => println!("array[0] = 0, array[1] = {}, array[2] = {}", second, third), // Các giá trị đơn lẻ có thể được bỏ qua bằng _ [1, _, third] => println!( "array[0] = 1, array[2] = {} and array[1] was ignored", third ), // Bạn cũng có thể gán một vài thứ và bỏ qua phần còn lại [-1, second, ..] => println!( "array[0] = -1, array[1] = {} and all the other ones were ignored", second ), // Dòng này sẽ không được biên dịch // [-1, second] => ... // Hoặc cũng có thể lưu chúng trong một array/slice khác (kiểu của nó phụ thuộc // vào giá trị được match) [3, second, tail @ ..] => println!( "array[0] = 3, array[1] = {} and the other elements were {:?}", second, tail ), // Kết hợp các patterns nói ở trên, ở đây chúng ta có thể gán giá trị của // phần tử thứ nhất và thứ hai, và lưu phần còn lại trong một array đơn [first, middle @ .., last] => println!( "array[0] = {}, middle = {:?}, array[2] = {}", first, middle, last ), } }
Xem thêm tại đây:
Arrays và Slices và @
trong Binding
enums
Một enum
được phân tách như sau:
// `allow` dùng để tắt warning vì ở đây chỉ có // một biến thể của enum được sử dụng. #[allow(dead_code)] enum Color { // Ba biến thể này được chỉ định bằng tên. Red, Blue, Green, // Năm biến thể này là các `u32` tuple với tên là các color model (mô hình màu) khác nhau RGB(u32, u32, u32), HSV(u32, u32, u32), HSL(u32, u32, u32), CMY(u32, u32, u32), CMYK(u32, u32, u32, u32), } fn main() { let color = Color::RGB(122, 17, 40); // TODO ^ Hãy thử các biến thể khác cho `color` println!("What color is it?"); // Một `enum` có thể được phân tách bằng cách sử dụng `match`. match color { Color::Red => println!("The color is Red!"), Color::Blue => println!("The color is Blue!"), Color::Green => println!("The color is Green!"), Color::RGB(r, g, b) => println!("Red: {}, green: {}, and blue: {}!", r, g, b), Color::HSV(h, s, v) => println!("Hue: {}, saturation: {}, value: {}!", h, s, v), Color::HSL(h, s, l) => println!("Hue: {}, saturation: {}, lightness: {}!", h, s, l), Color::CMY(c, m, y) => println!("Cyan: {}, magenta: {}, yellow: {}!", c, m, y), Color::CMYK(c, m, y, k) => println!("Cyan: {}, magenta: {}, yellow: {}, key (black): {}!", c, m, y, k), // `match` không cần thêm các nhánh khác vì tất cả các biến thể của enum đã được kiểm tra } }
Xem thêm tại đây:
#[allow(...)]
, color models và enum
pointers/ref
Đối với con trỏ, cần phân biệt giữa destructuring và dereferencing vì chúng là những khái niệm khác nhau được sử dụng trong Rust theo cách khác với các ngôn ngữ như C/C++.
- Dereferencing (Truy cập dữ liệu có trong một vị trí trong bộ nhớ
được trỏ tới một con trỏ) sử dụng
*
- Destructuring (Tách dữ liệu) sử dụng
&
,ref
, vàref mut
fn main() { // Gán một tham chiếu với kiểu `i32`. `&` có nghĩa là ở đây có một // tham chiếu. let reference = &4; match reference { // Nếu `reference` match với `&val`, nó là kết quả của // một so sánh giữa: // `&i32` và `&val` // ^ We see that if the matching `&`s are dropped, then the `i32` // should be assigned to `val`. &val => println!("Got a value via destructuring: {:?}", val), } // Để tránh dùng `&`, bạn có thể dereference bằng `*` trước khi thực hiện matching. match *reference { val => println!("Got a value via dereferencing: {:?}", val), } // Điều gì sẽ xảy ra nếu bạn không bắt đầu với một tham chiếu? `reference` là một tham chiếu // vì phía bên phải đã là một tham chiếu: `&4`. Còn biến dưới đây không phải là // tham chiếu vì vế phải không phải là một tham chiếu. let _not_a_reference = 3; // Rust cung cấp từ khoá `ref` cho mục đích này. Nó sửa đổi // việc gán để tạo ra một tham chiếu cho phần tử; dưới đây là một // tham chiếu. let ref _is_a_reference = 3; // Theo đó, bằng cách định nghĩa 2 giá trị không có tham chiếu, tham chiếu // có thể lấy thông qua `ref` và `ref mut`. let value = 5; let mut mut_value = 6; // Sử dụng `ref` để tạo một tham chiếu. match value { ref r => println!("Got a reference to a value: {:?}", r), } // Sử dụng `ref mut` cũng tương tự. match mut_value { ref mut m => { // Ở đây có một tham chiếu. Cần dereference nó trước khi có thể // thêm bất cứ thứ gì vào nó. *m += 10; println!("We added 10. `mut_value`: {:?}", m); }, } }
Xem thêm tại đây:
structs
Tương tự với các kiểu đã nói, một struct
cũng có thể được phân tách bằng match
:
fn main() { struct Foo { x: (u32, u32), y: u32, } // Hãy thử thay đổi giá trị trong struct này và xem điều gì xảy ra let foo = Foo { x: (1, 2), y: 3 }; match foo { Foo { x: (1, b), y } => println!("First of x is 1, b = {}, y = {} ", b, y), // Bạn có thể phân tách một cấu trúc và đổi tên biến, // thứ tự là không quan trọng Foo { y: 2, x: i } => println!("y is 2, i = {:?}", i), // và bạn cũng có thể bỏ qua một vài biến: Foo { y, .. } => println!("y = {}, we don't care about x", y), // điều này sẽ phát sinh lỗi: pattern không đề cập đến trường `x` //Foo { y } => println!("y = {}", y), } }
Xem thêm tại đây:
Guards
match
guard có thể được thêm vào với mục đích làm filter cho nhánh.
enum Temperature { Celsius(i32), Fahrenheit(i32), } fn main() { let temperature = Temperature::Celsius(35); // ^ TODO Hãy thử với giá trị khác của `temperature` match temperature { Temperature::Celsius(t) if t > 30 => println!("{}C is above 30 Celsius", t), // Phần `if condition` ^ bên trên là một guard Temperature::Celsius(t) => println!("{}C is below 30 Celsius", t), Temperature::Fahrenheit(t) if t > 86 => println!("{}F is above 86 Fahrenheit", t), Temperature::Fahrenheit(t) => println!("{}F is below 86 Fahrenheit", t), } }
Lưu ý rằng trình biên dịch sẽ không tính đến các guard conditions khi kiểm tra xem tất cả các pattern có được bao phủ bởi biểu thức match hay không.
fn main() { let number: u8 = 4; match number { i if i == 0 => println!("Zero"), i if i > 0 => println!("Greater than zero"), // _ => unreachable!("Should never happen."), // TODO ^ bỏ comment dòng trên để sửa lỗi biên dịch } }
Xem thêm tại đây:
Binding
Việc truy cập gián tiếp vào một biến khiến nó không thể phân nhánh và sử dụng cho đến khi được gán lại (re-binding). match
sử dụng ký tự @
để gán các giá trị với các name:
// Hàm `age` có giá trị trả về là một số `u32`. fn age() -> u32 { 15 } fn main() { println!("Tell me what type of person you are"); match age() { 0 => println!("I haven't celebrated my first birthday yet"), // Có thể `match` trực tiếp n với 1 ..= 12 nhưng như vậy thì tuổi // của đứa trẻ sẽ là bao nhiêu? Thay vào đó, gán n với dãy số 1 ..= 12 bằng @. n @ 1 ..= 12 => println!("I'm a child of age {:?}", n), n @ 13 ..= 19 => println!("I'm a teen of age {:?}", n), // Không có binding, đơn giản là trả về kết quả n => println!("I'm an old person of age {:?}", n), } }
Bạn cũng có thể sử dụng binding để "destructure" các biến thể của enum
, chẳng hạn như Option
:
fn some_number() -> Option<u32> { Some(42) } fn main() { match some_number() { // Đây là `Some` variant, match nếu như giá trị của nó, mà gán với `n`, // bằng 42. Some(n @ 42) => println!("The Answer: {}!", n), // Match một số khác bất kỳ. Some(n) => println!("Not interesting... {}", n), // Match với bất cứ thứ gì khác (`None` variant). _ => (), } }
Xem thêm tại đây:
if let
Trong một vài trường hợp sử dụng, khi match enum, match
đôi khi rất rườm rà. Ví dụ:
#![allow(unused)] fn main() { // Biến `optional` có kiểu là `Option<i32>` let optional = Some(7); match optional { Some(i) => { println!("This is a really long string and `{:?}`", i); // ^ Thụt đầu dòng ở đây 2 lần để nhận biết việc // tách `i` khỏi option. }, _ => {}, // ^ Dòng trên bắt buộc phải có, bởi `match` cần cover đầy đủ các trường // hợp có thể có. Nhưng có vẻ nó khá lãng phí không gian ở đây? }; }
Sử dụng if let
sẽ giúp xử lý trường hợp này sạch hơn, ngoài ra còn cho phép chỉ định nhiều loại
failure options:
fn main() { // Tất cả đều có kiểu `Option<i32>` let number = Some(7); let letter: Option<i32> = None; let emoticon: Option<i32> = None; // Cấu trúc `if let` ở đây được dùng như sau: "if `let` sẽ destructure `number` thành // `Some(i)` mà không cần xét trường hợp `_`. if let Some(i) = number { println!("Matched {:?}!", i); } // Nếu bạn cần chỉ định một failure, hãy sử dụng else: if let Some(i) = letter { println!("Matched {:?}!", i); } else { // Destructure thất bại. Chuyển sang case failure. println!("Didn't match a number. Let's go with a letter!"); } // Điều kiện dùng trong case failure. let i_like_letters = false; if let Some(i) = emoticon { println!("Matched {:?}!", i); // Destructure thất bại. Sử dụng điều kiện trong `else if` để xem nhánh // failure thay thế nào nên được sử dụng } else if i_like_letters { println!("Didn't match a number. Let's go with a letter!"); } else { // Điều kiện bằng false. Nhánh này được sử dụng: println!("I don't like letters. Let's go with an emoticon :)!"); } }
Theo một cách tương tự, không chỉ Option
, if let
có thể được sử dụng để match bất kỳ giá trị enum nào:
// Enum ví dụ của chúng ta enum Foo { Bar, Baz, Qux(u32) } fn main() { // Tạo một vài biến bất kỳ với enum bên trên let a = Foo::Bar; let b = Foo::Baz; let c = Foo::Qux(100); // Biến a match với Foo::Bar if let Foo::Bar = a { println!("a is foobar"); } // Biến b không match với Foo::Bar // Nên ở đây không in ra cái gì cả if let Foo::Bar = b { println!("b is foobar"); } // Biến c match với Foo:Qux với một giá trị // Tương tự với Some() ở ví dụ trước if let Foo::Qux(value) = c { println!("c is {}", value); } // Binding cũng có thể làm việc trong `if let` if let Foo::Qux(value @ 100) = c { println!("c is one hundred"); } }
Một lợi ích khác là if let
cho phép chúng ta thực hiện việc match với các biến thể enum mà không được tham số hóa (non-parameterized enum variants). Điều này đúng ngay cả trong trường hợp enum không implement hoặc derive trait PartialEq
. Trong những trường hợp như vậy với match
, if Foo::Bar == a
sẽ bị lỗi khi biên dịch, vì các phiên bản của enum không thể đánh đồng được với nhau, tuy nhiên, if let
sẽ tiếp tục hoạt động bình thường trong case này.
Ở đây có một bài tập cho bạn. Hãy thử sửa lỗi trong đoạn code bên dưới bằng cách sử dụng if let
:
// Enum này được cố tình không implement cũng như derive trait PartialEq. // Đó là lý do tại sao mà việc so sánh Foo::Bar == a bên dưới sẽ gây ra lỗi. enum Foo {Bar} fn main() { let a = Foo::Bar; // Biến a match với Foo::Bar if Foo::Bar == a { // ^-- Điều này sẽ sinh ra lỗi biên dịch. Hãy sử dụng `if let`. println!("a is foobar"); } }
Xem thêm tại đây:
enum
, Option
, và if let RFC
while let
Tương tự như if let
, while let
cũng có thể khiến một chuỗi match
rườm rà trở nên đẹp hơn. Hãy xem xét trình tăng dần i
bên dưới:
#![allow(unused)] fn main() { // Biến `optional` có kiểu `Option<i32>` let mut optional = Some(0); // Lặp đi lặp lại test này. loop { match optional { // Nếu `optional` được destructure, khối lệnh sau sẽ được dùng. Some(i) => { if i > 9 { println!("Greater than 9, quit!"); optional = None; } else { println!("`i` is `{:?}`. Try again.", i); optional = Some(i + 1); } // ^ Yêu cầu 3 lần thụt đầu dòng! }, // Thoát vòng lặp nếu việc tách dữ liệu thất bại: _ => { break; } // ^ Dòng cuối này có vẻ thừa thãi nhưng lại bắt buộc phải có. Cần có một cách tốt hơn! } } }
Sử dụng while let
sẽ giúp chuỗi lặp match
trên gọn gàng hơn:
fn main() { // Biến `optional` có kiểu `Option<i32>` let mut optional = Some(0); // Cấu trúc `white let` được dùng như sau: "while `let` sẽ destructure `optional` thành // `Some(i)`, đánh giá trong các block (`{}`). while let Some(i) = optional { if i > 9 { println!("Greater than 9, quit!"); optional = None; } else { println!("`i` is `{:?}`. Try again.", i); optional = Some(i + 1); } // ^ Ít thụt đầu dòng hơn `if let` và cũng // không đòi hỏi xử lý trường hợp failure. } // ^ `if let` có tuỳ chọn bổ sung thêm `else`/`else if`. // Nhưng `while let` không có và không cần điều này. }
Xem thêm tại đây:
enum
, Option
, và white let RFC
Hàm
Các hàm được khai báo bằng từ khóa fn
. Những tham số của hàm phải được
chú thích kiểu dữ liệu, tương tự như các biến, và nếu hàm trả về một giá trị,
thì giá trị trả về phải được chỉ định ngay sau dấu mũi tên ->
.
Biểu thức cuối cùng trong hàm sẽ được sử dụng như là giá trị trả về.
Ngoài ra, câu lệnh return
có thể được sử dụng để trả về một giá trị sớm hơn
từ bên trong hàm, hoặc là trong vòng lặp hoặc trong câu lệnh if
.
Cùng thử viết lại FizzBuzz bằng hàm nhé!
// Thứ tự khai báo các hàm không bị ràng buộc như trong ngôn ngữ C/C++. fn main() { // Ta có thể gọi hàm ở ngay đây, rồi định nghĩa hàm ở đâu khác cũng được. fizzbuzz_to(100); } // Hàm trả về giá trị boolean fn is_divisible_by(lhs: u32, rhs: u32) -> bool { // Trường hợp đặc biệt, trả về giá trị của hàm sớm hơn. if rhs == 0 { return false; } // Đây là một biểu thức, từ khóa `return` không cần phải được sử dụng ở đây lhs % rhs == 0 } // Những hàm không trả về giá trị nào, sẽ trả về kiểu dữ liệu `()` fn fizzbuzz(n: u32) -> () { if is_divisible_by(n, 15) { println!("fizzbuzz"); } else if is_divisible_by(n, 3) { println!("fizz"); } else if is_divisible_by(n, 5) { println!("buzz"); } else { println!("{}", n); } } // Khi một hàm trả về `()`, không cần phải ghi ra kiểu dữ liệu trả về cũng được. fn fizzbuzz_to(n: u32) { for n in 1..=n { fizzbuzz(n); } }
Associated functions & Methods
Một số function được liên kết với một kiểu dữ liệu (type) cụ thể. Có hai dạng là: các hàm liên quan và phương thức. Hàm liên quan là hàm thường được định nghĩa trên một kiểu một cách chung chung, trong khi các phương thức là các hàm liên quan nhưng được gọi trên một thể hiện (instance) cụ thể của một kiểu.
struct Point { x: f64, y: f64, } // Triển khai tất cả các function & method liên quan đến `Point` impl Point { // Đây là một "associated function" bởi vì function này được kết nối với // một kiểu cụ thể là struct Point // // Associated functions không cần phải được gọi với một instance. // Các function này có thể được sử dụng dạng như một constructor. fn origin() -> Point { Point { x: 0.0, y: 0.0 } } // Một associated function khác, nhận vào hai đối số: fn new(x: f64, y: f64) -> Point { Point { x: x, y: y } } } struct Rectangle { p1: Point, p2: Point, } impl Rectangle { // Đây là một method // `&self` là cú pháp vắn tắt của `self: &Self`, trong đó `Self` là kiểu // của đối tượng gọi method. Trong trường hợp này `Self` = `Rectangle` fn area(&self) -> f64 { // sử dụng `self` cho phép truy cập vào các fields của struct thông qua toán tử dấu chấm "." let Point { x: x1, y: y1 } = self.p1; let Point { x: x2, y: y2 } = self.p2; // `abs` là một phương thức của kiểu dữ liệu `f64` trả về giá trị tuyệt đối của đối tượng gọi ((x1 - x2) * (y1 - y2)).abs() } fn perimeter(&self) -> f64 { let Point { x: x1, y: y1 } = self.p1; let Point { x: x2, y: y2 } = self.p2; 2.0 * ((x1 - x2).abs() + (y1 - y2).abs()) } // Phương thức này yêu cầu đối tượng gọi tới phải là mutable // `&mut self` là cú pháp viết tắt của `self: &mut Self` fn translate(&mut self, x: f64, y: f64) { self.p1.x += x; self.p2.x += x; self.p1.y += y; self.p2.y += y; } } // `Pair` sở hữu các tài nguyên: hai số nguyên 32 bit trên vùng nhớ heap struct Pair(Box<i32>, Box<i32>); impl Pair { // Phương thức này "sử dụng" các tài nguyên của đối tượng gọi tới // `self` là viết tắt của `self: Self` fn destroy(self) { // Destructure `self` let Pair(first, second) = self; println!("Destroying Pair({}, {})", first, second); // `first` và `second` ra khỏi scope và được giải phóng } } fn main() { let rectangle = Rectangle { // Associated functions được gọi bằng cách sử dụng ":" p1: Point::origin(), p2: Point::new(3.0, 4.0), }; // Method được gọi bằng cách sử dụng toán tử "." // Lưu ý rằng biến số `&self` được ngầm định truyền vào // `rectangle.perimeter()` === `Rectangle::perimeter(&rectangle)` println!("Rectangle perimeter: {}", rectangle.perimeter()); println!("Rectangle area: {}", rectangle.area()); let mut square = Rectangle { p1: Point::origin(), p2: Point::new(1.0, 1.0), }; // Error! `rectangle` đang là immutable, nhưng phương thức này yêu cầu một đối tượng mutable // rectangle.translate(1.0, 0.0); // TODO ^ Bỏ comment dòng này để kiểm tra // Okay! Đối tượng mutable có thể gọi tới phương thức mutable square.translate(1.0, 1.0); let pair = Pair(Box::new(1), Box::new(2)); pair.destroy(); // Error! Phương thức `destroy` được gọi trước đó đã "tiêu thụ" `pair` //pair.destroy(); // TODO ^ Bỏ comment dòng này để kiểm tra }
Closures
Closures là các hàm có thể capture môi trường bao quanh. Ví dụ, closure capture biến x
:
|val| val + x
Cú pháp và khả năng của closures làm cho chúng rất tiện lợi cho việc sử dụng ngay lập tức. Gọi một closure hoàn toàn giống như gọi một hàm. Tuy nhiên, cả kiểu dữ liệu đầu vào và đầu ra có thể được suy luận và tên biến đầu vào phải được chỉ định.
Một số đặc điểm khác của closures bao gồm:
- Sử dụng
||
thay vì()
để bao quanh biến đầu vào. - Tùy chọn việc đặt dấu mở ngoặc nhọn (
{}
) cho một biểu thức đơn (bắt buộc nếu không phải là biểu thức đơn). - Khả năng capture được các biến môi trường bên ngoài (outer environment variables).
fn main() { let outer_var = 42; // Một hàm thông thường không thể tham chiếu đến các biến trong môi trường bao quanh nó. //fn function(i: i32) -> i32 { i + outer_var } // TODO: xóa comment trên và xem lỗi biên dịch. Trình biên dịch // gợi ý chúng ta nên định nghĩa một closure thay vì. // Closures là vô danh, ở đây chúng ta đang liên kết chúng với các tham chiếu. // Chú thích giống với chú thích của hàm, nhưng là tùy chọn // giống như các dấu ngoặc nhọn `{}` bao quanh thân hàm. Những hàm vô danh này // được gán cho các biến được đặt tên phù hợp. let closure_annotated = |i: i32| -> i32 { i + outer_var }; let closure_inferred = |i | i + outer_var ; // Gọi các closures. println!("closure_annotated: {}", closure_annotated(1)); println!("closure_inferred: {}", closure_inferred(1)); // Một khi kiểu dữ liệu của closure đã được suy ra, thì nó không thể được suy ra lại với một kiểu dữ liệu khác. //println!("cannot reuse closure_inferred with another type: {}", closure_inferred(42i64)); // TODO: bỏ ghi chú dòng trên và xem lỗi trình biên dịch // Một closure không có đối số, trả về một `i32`. // Kiểu dữ liệu trả về được suy luận tự động. let one = || 1; println!("closure returning one: {}", one()); }
Capturing
Closures vốn rất linh hoạt và sẽ thực hiện bất cứ điều gì mà function yêu cầu để làm cho closure hoạt động mà không cần chú thích(annotation). Điều này cho phép việc capturing(khi khai báo closure) thích nghi với các trường hợp sử dụng(use case), đôi khi sử dụng moving và borrowing. Closures có thể capture(khai báo) các biến thông qua:
- bằng tham chiếu:
&T
- bằng tham chiếu có thể thay đổi:
&mut T
- bằng giá trị:
T
Chúng ưu tiên capture biến bằng tham chiếu và chỉ sử dụng các cách khác khi cần thiết.
fn main() { use std::mem; let color = String::from("green"); // Một closure để in ra biến `color` sẽ ngay lập tức mượn(borrows) (`&`) `color` và // lưu borrow và closure vào trong biến `print`. Nó vẫn sẽ giữ borrow cho đến khi `print` được sử dụng lần cuối // `println!` chỉ yêu cầu đối số bằng tham chiếu không thể thay đổi(immutable) vì vậy nó không đặt ra bất cứ hạn chế nào khác. let print = || println!("`color`: {}", color); // Gọi closure bằng cách sử dụng borrow. print(); // `color` có thể được mượn(borrow) một lần nữa bằng cách không thay đổi(immutably), vì closure chỉ giữ một tham chiếu không thể thay đổi(immutable) tới `color`. let _reborrow = &color; print(); // Một thao tác move hoặc reborrow sẽ có thể được phép thực hiện sau lần cuối cùng sử dụng `print` let _color_moved = color; let mut count = 0; // Một closure để tăng giá trị của `count` có thể sử dụng `&mut count` hoặc `count` // nhưng `&mut count` có ít hạn chế hơn cho nên closure sẽ chọn `&mut count`. Ngay lập tức borrows `count`. // // Cần thêm `mut` cho `inc` bởi vì một tham chiếu `&mut` được lưu dữ liệu bên trong. // Do đó, việc gọi closure làm thay đổi trong closure, điều này yêu cầu một `mut`. let mut inc = || { count += 1; println!("`count`: {}", count); }; // Gọi tới closure bằng cách sử dụng một mutable borrow. inc(); // Closure vẫn mượn(borrows) `count` ở dạng có thể thay đổi(mutably) bởi vì nó được gọi sau đó. // Nếu bạn thực hiện mượn lại(reborrow) sẽ dẫn đến một lỗi. // let _reborrow = &count; // ^ TODO: thử bỏ chú thích(uncommenting) dòng phía trên. inc(); // closure không cần phải mượn(borrow) `&mut count`. // Do đó, có thể mượn(reborrow) lại mà không gặp lỗi let _count_reborrowed = &mut count; // Một kiểu dữ liệu không thể sao chép(A non-copy type). let movable = Box::new(3); // `mem::drop` yêu cầu `T` phải truyền vào theo giá trị(take by value). // Một kiểu dữ liệu có thể sao chép (copy type) sẽ sao chép vào closure mà không làm thay đổi đối tượng ban đầu. // Kiểu dữ liệu không thể sao chép phải được di chuyển(move) và do đó, `movable` sẽ được di chuyển ngay vào closure. let consume = || { println!("`movable`: {:?}", movable); mem::drop(movable); }; // biến `consume` chỉ có thể gọi một lần. consume(); // consume(); // ^ TODO: Thử bỏ chú thích(uncommenting) dòng phía trên. }
Sử dụng move
trước dấu '||' thì closure sẽ buộc phải lấy quyền sở hữu(ownership) của các biến được capture:
fn main() { // `Vec` có tính không thể sao chép (non-copy). let haystack = vec![1, 2, 3]; let contains = move |needle| haystack.contains(needle); println!("{}", contains(&1)); println!("{}", contains(&4)); // println!("There're {} elements in vec", haystack.len()); // ^ bỏ chú thích(uncommenting) dòng phía trên sẽ trả về lỗi compile // bởi vì trình kiểm tra borrow checker không cho phép sử dụng lại biến sau khi nó đã được move. // Xóa `move` khỏi chữ kí của closure sẽ khiến closure // mượn(borrow) biến _haystack_ một cách không thể thay đổi, do đó _haystack_ vẫn // khả dụng và bỏ chú thích(uncommenting) dòng trên sẽ không gây ra lỗi. }
See also:
Box
and std::mem::drop
Sử dụng closure dưới dạng tham số đầu vào (input parameters)
Mặc dù, Rust sẽ tự chọn cách capture các biến trong quá trình biên dịch mà không cần chúng ta phải khai báo kiểu tường minh, nhưng Rust sẽ không chấp nhận sự không rõ ràng này khi chúng ta định nghĩa hàm. Do đó, khi chúng ta viết một hàm và truyền một closure (khối đóng) vào làm tham số đầu vào, thì kiểu dữ liệu hoàn chỉnh của closure đó phải được kí hiệu bằng cách sử dụng một trong một vài trait sau đây, các trait này sẽ được xác định dựa trên cách mà closure sử dụng các giá trị mà nó capture được. Theo mức độ thứ tự giảm dần của mức độ hạn chế các trait đó lần lượt là:
Fn
: closure sử dụng các giá trị mà nó capture được dưới dạng tham chiếu (&T).FnMut
: closure sử dụng các giá trị mà nó capture được dưới dạng tham chiếu có thể thay đổi được (&mut T).FnOnce
: closure sử dụng các giá trị mà nó capture được dưới dạng tham trị (T).
Trên cơ sở từng biến một, rust compiler sẽ capture các biến theo cách ít hạn chế nhất có thể.
Giả sử một tham số closure được khai báo với kiểu là FnOnce
, điều này có nghĩa là closure đó có thể capture1 các biến bằng tham chiếu &T
, tham chiếu thay đổi được &mut T
và tham trị T
,
nhưng compiler sẽ lựa chọn cách capture các biến dựa vào cách mà các captured variables được sử dụng trong closure đó.
Điều này có thể xảy ra bởi vì một khi ta có thể di chuyển (move) một biến thì ta cũng có thể thực hiện các loại vay mượn (borrow) khác đối với biến đó,
tuy nhiên điều ngược lại sẽ không đúng. Nếu một tham số được khai báo là Fn
thì việc capture biến bằng tham chiếu thay đổi được &mut T
hoặc tham trị T
là không thể.
Tuy nhiên, &T
được phép sử dụng trong trường hợp này.
Ở ví dụ dưới đây, bạn hãy thử lần lượt thay đổi các kiểu khai báo Fn
, FnMut
và FnOnce
để xem thử có điều gì xảy ra không nhé:
// Đây là hàm nhận tham số truyền vào là một closure và thực thi closure đó. // <F> là kí hiệu rằng F là một tham số có kiểu generic fn apply<F>(f: F) where // Closure này không có input và cũng không trả về gì cả F: FnOnce() { // ^ TODO: Hãy thử thay `FnOnce` thành `Fn` hoặc `FnMut`. f(); } // Đây là hàm nhận tham số truyền vào là một closure và trả về `i32`. fn apply_to_3<F>(f: F) -> i32 where // Closure này nhận vào một `i32` và trả về một `i32`. F: Fn(i32) -> i32 { f(3) } fn main() { use std::mem; let greeting = "hello"; // Một kiểu dữ liệu không thể sao chép. // Phương thức `to_owned` tạo dữ liệu được sở hữu bởi biến farewell từ dữ liệu được mượn let mut farewell = "goodbye".to_owned(); // Capture 2 biến: `greeting` theo tham chiếu và // `farewell` theo tham trị. let diary = || { // `greeting` được capture theo tham chiếu trong closure này nên yêu cầu khai báo bằng `Fn`. println!("I said {}.", greeting); // Ở đây có sự thay đổi giá trị của captured variable `farewell`, // nên `farewell` được closure này capture theo tham chiếu thay đổi được. // Do đó closure phải được khai báo bằng `FnMut`. farewell.push_str("!!!"); println!("Then I screamed {}.", farewell); println!("Now I can sleep. zzzzz"); // Gọi hàm drop để bắt buộc closure này capture biến `farewell` // bằng tham trị. Do đó closure phải được khai báo bằng `FnOnce`. mem::drop(farewell); }; // Gọi hàm có chức năng thực thi closure truyền vào. apply(diary); // `double` đáp ứng điều kiện ràng buộc về trait của `apply_to_3`. let double = |x| 2 * x; println!("3 doubled: {}", apply_to_3(double)); }
See also:
std::mem::drop
, Fn
, FnMut
, Generics, where and FnOnce
Chú thích của người dịch: "Việc capture một biến trong closure là một cách để lưu trữ giá trị của một biến trong phạm vi mà closure được khai báo
và sử dụng biến đó. Khi closure được gọi, nó sẽ truy cập và sử dụng các giá trị được capture."
Type anonymity
Closure (đóng gói) nhắm gọn các biến từ phạm vi bao quanh một cách ngắn gọn. Điều này có gây ra bất kỳ hệ quả gì không? Chắc chắn là có. Dễ thấy rằng sử dụng một closure như một tham số của hàm đòi hỏi sử dụng generics, điều này cần thiết bởi cách chúng được định nghĩa:
#![allow(unused)] fn main() { // `F` phải là một generic. fn apply<F>(f: F) where F: FnOnce() { f(); } }
Khi một closure được định nghĩa, compiler tự động tạo ra một kiểu cấu trúc không xác định mới để lưu giữ các biến bên trong closure, trong khi đó, triển khai chức năng thông qua một trong các traits
: Fn
, FnMut
, hoặc FnOnce
cho kiểu không xác định đó. Kiểu dữ liệu này được gán cho biến và được lưu trữ khi gọi đến.
Vì kiểu dữ liệu mới này là kiểu dữ liệu không xác định, bất kì khi nào sử dụng trong một function sẽ yêu cầu kiểu generics. Tuy nhiên, một tham số kiểu không giới hạn(unbounded) <T>
vẫn sẽ là mơ hồ và không được phép. Vì vậy, ràng buộc bởi một trong các traits
: Fn
, FnMut
, hoặc
FnOnce
(kiểu nó được triển khai) là đủ để xác định kiểu của nó.
// `F` phải triển khai `Fn` cho một closure mà không có inputs // và không trả lại gì cả - chính xác những gì được yêu cầu cho `print` fn apply<F>(f: F) where F: Fn() { f(); } fn main() { let x = 7; // Capture `x` vào một kiểu không xách định và triển khai `Fn` cho nó // Lưu trữ nó trong `print`. let print = || println!("{}", x); apply(print); }
See also:
A thorough analysis, Fn
, FnMut
,
and FnOnce
Input functions
Vì closures có thể được sử dụng như là đối số, bạn có thể thắc mắc liệu có thể áp dụng như vậy đối với các hàm được không. Và thực sự là có thể! Nếu bạn khai báo một hàm mà nó nhận một closure làm tham số, thì bất kỳ hàm nào thỏa mãn điều kiện của closure đó có thể được truyền vào làm tham số.
// Định nghĩa một hàm mà nó nhận một đối số generic `F` // ràng buộc nó bằng `Fn`, và gọi nó fn call_me<F: Fn()>(f: F) { f(); } // Định nghĩa một hàm ở bên ngoài thỏa mãn ràng buộc `Fn` fn function() { println!("I'm a function!"); } fn main() { // Định nghĩa một closure thỏa mãn ràng buộc `Fn` let closure = || println!("I'm a closure!"); call_me(closure); call_me(function); }
Một điều cần lưu ý thêm à, Fn
, FnMut
, và FnOnce
traits
chỉ định
cách một closure lấy các biến từ phạm vi bao quanh.
Xem thêm:
Sử dụng closure dưới dạng tham số đầu ra (output parameters)
Không những closure có thể sử dụng dưới dạng tham số truyền vào, mà còn có thể sử dụng closure dưới dạng kết quả trả về của một hàm.
Tuy nhiên, các kiểu closure ẩn danh theo định nghĩa là không xác định nên ta phải dùng cú pháp impl Trait
để return closure.
Các trait hợp lệ có thể được dùng để khai báo kiểu trả về là một closure là:
Fn
FnMut
FnOnce
Hơn nữa, từ khóa move
phải được sử dụng để chỉ ra rằng tất cả các giá trị được capture đều bằng tham trị. Điều này là bắt buộc vì
bất kỳ việc capture giá trị nào theo tham chiếu sẽ bị loại bỏ ngay khi hàm thoát ra, dẫn đến các lỗi về tham chiếu không hợp lệ trong closure.
fn create_fn() -> impl Fn() { let text = "Fn".to_owned(); move || println!("This is a: {}", text) } fn create_fnmut() -> impl FnMut() { let text = "FnMut".to_owned(); move || println!("This is a: {}", text) } fn create_fnonce() -> impl FnOnce() { let text = "FnOnce".to_owned(); move || println!("This is a: {}", text) } fn main() { let fn_plain = create_fn(); let mut fn_mut = create_fnmut(); let fn_once = create_fnonce(); fn_plain(); fn_mut(); fn_once(); }
Đọc thêm:
Fn
, FnMut
, Generics và impl Trait.
Các ví dụ trong std
Phần này bao gồm các ví dụ về việc sử dụng closures từ thư viện std
.
Iterator::any
Iterator::any
là một hàm khi được truyền một iterator(trình vòng lặp), sẽ trả về true
nếu có bất kỳ phần tử nào thỏa mãn mệnh đề. Nếu không thì false
. Điểm nhấn của nó:
#![allow(unused)] fn main() { pub trait Iterator { // Kiểu lặp đi lặp lại. type Item; // `any` truyền `&mut self` có nghĩa là người gọi có thể mượn và sửa đổi giá trị, // nhưng không thể tiêu thụ nó. fn any<F>(&mut self, f: F) -> bool where // `FnMut` có nghĩa là bất kì biến nào được chụp nhiều nhất có thể được sửa đổi, không được sử dụng. // `&Self::Item` cho biết nó đưa các đối số vào closure bằng giá trị. F: FnMut(Self::Item) -> bool; } }
fn main() { let vec1 = vec![1, 2, 3]; let vec2 = vec![4, 5, 6]; // `Phương thức iter() cho `vecs trả về kiểu `&i32`. Destructure thành `i32`. println!("2 in vec1: {}", vec1.iter() .any(|&x| x == 2)); // Phương thức `into_iter()` cho vecs trả về kiểu `i32`. Không yêu cầu Destructure. println!("2 in vec2: {}", vec2.into_iter().any(|x| x == 2)); // Phương thức `iter()` chỉ mượn `vec1` và phần tử của nó, vì thế họ có thể sử dụng lại. println!("vec1 len: {}", vec1.len()); println!("First element of vec1 is: {}", vec1[0]); // Phương thức `into_iter()` chuyển `vec2` và các phần tử của nó, vì thế chúng // không thể được sử dụng lại // println!("First element of vec2 is: {}", vec2[0]); // println!("vec2 len: {}", vec2.len()); // TODO: Thử không comment hai dòng trên và xem biên dịch lỗi.. let array1 = [1, 2, 3]; let array2 = [4, 5, 6]; // Phương thức `iter()` cho mảng trả về kiểu `&i32`. println!("2 in array1: {}", array1.iter() .any(|&x| x == 2)); // Phương thức `into_iter()` cho mảng trả về kiểu `i32`. println!("2 in array2: {}", array2.into_iter().any(|x| x == 2)); }
Xem thêm
Tìm kiếm thông qua các iterator (trình vòng lặp)
Iterator::find
là một hàm lặp qua một trình vòng lặp rồi tìm kiếm giá trị đầu tiên thỏa mãn một số điều kiện nào đó. Nếu không có giá trị nào thỏa mãn điều kiện, nó sẽ trả về None
. Điểm nhấn của nó:
#![allow(unused)] fn main() { pub trait Iterator { // Kiểu lặp đi lặp lại. type Item; // `find` truyền `&mut self` nghĩa là người gọi có thể mượn và sửa đổi, // nhưng không được tiêu thụ. fn find<P>(&mut self, predicate: P) -> Option<Self::Item> where // `FnMut` có nghĩa là bất kì biến nào được chụp nhiều nhất có thể được sửa đổi, không được sử dụng. // &Self::Item cho biết cho biết nó nhận các đối số vào closure bằng tham chiếu P: FnMut(&Self::Item) -> bool; } }
fn main() { let vec1 = vec![1, 2, 3]; let vec2 = vec![4, 5, 6]; // Phương thức `iter()` cho vecs sẽ trả về kiểu `&i32`. let mut iter = vec1.iter(); // Phương thức `into_iter()` cho vecs sẽ trả về kiểu `i32`. let mut into_iter = vec2.into_iter(); // Phương thức`iter()` cho vecs sẽ trả về kiểu `&i32`, và chúng tôi muốn tham chiếu tới một phần tử của nó, // vì thế chúng tôi phải destructure `&&i32` thành `i32`. println!("Find 2 in vec1: {:?}", iter .find(|&&x| x == 2)); // Phương thức `into_iter()` cho vecs sẽ trả về kiểu `i32`, và chúng tôi muốn tham chiếu tới một trong các mục của nó, // Vì thế, chúng tôi phải destructure `&i32` thành `i32` println!("Find 2 in vec2: {:?}", into_iter.find(| &x| x == 2)); let array1 = [1, 2, 3]; let array2 = [4, 5, 6]; // Phương thức `iter()` cho mảng sẻ trả sẽ về kiểu `&i32` println!("Find 2 in array1: {:?}", array1.iter() .find(|&&x| x == 2)); // Phương thức `into_iter()` cho mảng sẽ trả về kiểu `i32` println!("Find 2 in array2: {:?}", array2.into_iter().find(|&x| x == 2)); }
Iterator::find
cung cấp cho bạn một tham chiếu đến giá trị. Nhưng nếu bạn muốn lấy *chỉ số
*của giá trị, hãy sử dụng
Iterator::position`.
fn main() { let vec = vec![1, 9, 3, 3, 13, 2]; // Phương thức `iter()` cho vecs sẽ trả về kiểu `&i32` và phương thức `position()` không cung cấp một tham chiếu nào, vì thế // chúng ta phải destructure `&i32` thành `i32` let index_of_first_even_number = vec.iter().position(|&x| x % 2 == 0); assert_eq!(index_of_first_even_number, Some(5)); // Phương thức `into_iter()` cho vecs trả về kiểu `i32` và phương thức `position()` không cung cấp một tham chiếu nào, vì thế // chúng ta phải destructure.... let index_of_first_negative_number = vec.into_iter().position(|x| x < 0); assert_eq!(index_of_first_negative_number, None); }
Xem thêm:
std::iter::Iterator::find std::iter::Iterator::find_map std::iter::Iterator::position std::iter::Iterator::rposition
Higher Order Functions
Rust cung cấp Higher Order Functions (HOF - Hàm bậc cao). HOF là function nhận vào một hay nhiều function và/hoặc trả về kết quả là một function. Các HOF và các vòng lặp lười (lazy iterator) thêm gia vị cho các tính năng của Rust.
fn is_odd(n: u32) -> bool { n % 2 == 1 } fn main() { println!("Tìm tổng của tất cả các số lẻ có bình phương nhỏ hơn 1000"); let upper = 1000; // Cách tiếp cận mệnh lệnh (imperative approach) // Khai báo một biến tích lũy let mut acc = 0; // Lặp: 0, 1, 2, ... tới vô tận for n in 0.. { // Bình phương số n let n_squared = n * n; if n_squared >= upper { // Thoát khỏi vòng lặp nếu đạt tới giới hạn trên break; } else if is_odd(n_squared) { // Nếu n là số lẻ, cộng dồn n vào biến acc acc += n_squared; } } println!("imperative style: {}", acc); // Cách tiếp cận function (functional approach) let sum_of_squared_odd_numbers: u32 = (0..).map(|n| n * n) // Bình phương tất cả các số tự nhiên .take_while(|&n_squared| n_squared < upper) // Nhỏ hơn giới hạn .filter(|&n_squared| is_odd(n_squared)) // Là số lẻ .sum(); // Và tính tổng println!("functional style: {}", sum_of_squared_odd_numbers); }
Option và Iterator đều triển khai HOF.
Diverging functions
Các hàm "Diverging" (không kết thúc) không bao giờ trả về giá trị. Chúng được đánh dấu bằng !
, đây là một kiểu dữ liệu rỗng.
#![allow(unused)] fn main() { fn foo() -> ! { panic!("This call never returns."); } }
Khác với tất cả các kiểu dữ liệu khác, kiểu dữ liệu này không thể được khởi tạo,
vì tập hợp tất cả các giá trị có thể của kiểu dữ liệu này là rỗng.
Lưu ý rằng, điều này khác với kiểu ()
(kiểu unit), mà có đúng một giá trị có thể.
Ví dụ, hàm này trả về như bình thường, mặc dù không có thông tin nào trong giá trị trả về.
fn some_fn() { () } fn main() { let _a: () = some_fn(); println!("This function returns and you can see this line."); }
Khác với hàm này, hàm này sẽ không bao giờ trả lại quyền điều khiển cho người gọi.
#![feature(never_type)]
fn main() {
let x: ! = panic!("This call never returns.");
println!("You will never see this line!");
}
Mặc dù điều này có vẻ như một khái niệm trừu tượng, thực tế nó rất hữu ích và thường tiện dụng.
Ưu điểm chính của kiểu dữ liệu này là nó có thể được chuyển đổi sang bất kỳ kiểu dữ liệu nào khác
và do đó được sử dụng ở những nơi cần đúng kiểu dữ liệu, ví dụ như trong các khối match
.
Điều này cho phép chúng ta viết mã như thế này:
fn main() { fn sum_odd_numbers(up_to: u32) -> u32 { let mut acc = 0; for i in 0..up_to { // Lưu ý rằng kiểu trả về của biểu thức match này phải là u32 // vì kiểu của biến "addition" là kiểu dữ liệu u32. let addition: u32 = match i%2 == 1 { // Biến "i" có kiểu dữ liệu là u32, điều này hoàn toàn hợp lệ. true => i, // Mặt khác, biểu thức "continue" không trả về giá trị kiểu u32 // nhưng vẫn không sao cả, vì nó không bao giờ trả về và do đó // không vi phạm yêu cầu kiểu dữ liệu của biểu thức match. false => continue, }; acc += addition; } acc } println!("Sum of odd numbers up to 9 (excluding): {}", sum_odd_numbers(9)); }
Đây cũng là kiểu trả về của các hàm lặp mãi mãi (e.g. loop {}
)
như các máy chủ mạng hoặc các hàm kết thúc quá trình thực thi (e.g. exit()
).
Modules
Rust cung cấp một hệ thống module mạnh mẽ có thể được sử dụng để phân chia các mã theo cấp bậc thành các đơn vị logic (module) và quản lý phạm vi (công khai/riêng tư) (public/private)
giữa chúng.
Một module là một tập hợp các thứ như: hàm, cấu trúc, đặc điểm, khối impl
và thậm chí nó có thể bao gồm các module khác.
Phạm vi
Mặc định, các mục trong một module có phạm vi là riêng tư, nhưng điều này có thể được ghi đè bằng từ khóa pub
. Chỉ những mục công khai của một module có thể được truy cập từ bên ngoài phạm vi của nó.
// Một module tên là `my_mod` mod my_mod { // Các mục trong module có phạm vi mặc định là riêng tư. fn private_function() { println!("called `my_mod::private_function()`"); } // Sử dụng từ khóa `pub` để ghi đè phạm vi mặc định. pub fn function() { println!("called `my_mod::function()`"); } // Các mục có thể truy cập lẫn nhau trong cùng một module, // thậm chí phạm vi của nó là riêng tư. pub fn indirect_access() { print!("called `my_mod::indirect_access()`, that\n> "); private_function(); } // Các module có thể lồng nhau. pub mod nested { pub fn function() { println!("called `my_mod::nested::function()`"); } #[allow(dead_code)] fn private_function() { println!("called `my_mod::nested::private_function()`"); } // Các hàm được khai báo bằng cách sử dụng cú pháp `pub(in path)` chỉ hiện diện // trong đường dẫn được chỉ định. `path` phải là một module cha hoặc tổ tiên (ancestor). pub(in crate::my_mod) fn public_function_in_my_mod() { print!("called `my_mod::nested::public_function_in_my_mod()`, that\n> "); public_function_in_nested(); } // Các hàm được khai báo bằng cách sử dụng cú pháp `pub(self)` chỉ hiện diện // trong module hiện tại, tương tự như khi ta để phạm vi của chúng là riêng tư. pub(self) fn public_function_in_nested() { println!("called `my_mod::nested::public_function_in_nested()`"); } // Các hàm được khai báo bằng cách sử dụng cú pháp `pub(super)` chỉ hiện diện // trong module cha. pub(super) fn public_function_in_super_mod() { println!("called `my_mod::nested::public_function_in_super_mod()`"); } } pub fn call_public_function_in_my_mod() { print!("called `my_mod::call_public_function_in_my_mod()`, that\n> "); nested::public_function_in_my_mod(); print!("> "); nested::public_function_in_super_mod(); } // pub(crate) khiến cho các hàm có thể hiện diện chỉ trong crate hiện tại. pub(crate) fn public_function_in_crate() { println!("called `my_mod::public_function_in_crate()`"); } // Các module lồng nhau sẽ theo cùng một quy tắc về phạm vi. mod private_nested { #[allow(dead_code)] pub fn function() { println!("called `my_mod::private_nested::function()`"); } // Các mục cha có phạm vi riêng tư sẽ giới hạn phạm vi của một mục con, // ngay cả khi nó được khai báo là công khai trong một phạm vi lớn hơn. #[allow(dead_code)] pub(crate) fn restricted_function() { println!("called `my_mod::private_nested::restricted_function()`"); } } } fn function() { println!("called `function()`"); } fn main() { // Các module cho phép phân biệt giữa các mục có cùng tên. function(); my_mod::function(); // Các mục công khai, bao gồm các mục trong các module lồng nhau, có thể được // truy cập từ bên ngoài module cha. my_mod::indirect_access(); my_mod::nested::function(); my_mod::call_public_function_in_my_mod(); // Các mục khai báo pub(crate) có thể được truy cập từ bất cứ đầu trong cùng một crate. my_mod::public_function_in_crate(); // Các mục khai báo pub(in path) chỉ có thể được truy cập trong module được chỉ định. // Lỗi! hàm `public_function_in_my_mod` là riêng tư. //my_mod::nested::public_function_in_my_mod(); // TODO ^ Hãy thử bỏ ghi chú trên dòng này // Các mục riêng tư của module không thể được truy cập trực tiếp, ngay cả khi // nó được lồng bên trong một module công khai: // Lỗi! `private_function` là riêng tư //my_mod::private_function(); // TODO ^ Hãy thử bỏ ghi chú trên dòng này // Lỗi! `private_function` là riêng tư //my_mod::nested::private_function(); // TODO ^ Hãy thử bỏ ghi chú trên dòng này // Error! `private_nested` là một module riêng tư //my_mod::private_nested::function(); // TODO ^ Hãy thử bỏ ghi chú trên dòng này // Error! `private_nested` là một module riêng tư //my_mod::private_nested::restricted_function(); // TODO ^ Hãy thử bỏ ghi chú trên dòng này }
Phạm vi của cấu trúc
Các cấu trúc (struct) có một mức độ phạm vi truy cập được thêm vào đối với các trường của chúng. Phạm vi mặc định là riêng tư (private), và có thể được ghi đè bằng từ khóa pub
. Phạm vi này chỉ được xem xét khi một cấu trúc được truy cập từ bên ngoài module mà nó được định nghĩa, và mục đích của nó là che thông tin (đóng gói) (encapsulation).
mod my { // Một cấu trúc công khai với một trường công khai của kiểu chung (generic) `T` pub struct OpenBox<T> { pub contents: T, } // Một cấu trúc công khai với một trường riêng tư của kiểu chung (generic) `T` pub struct ClosedBox<T> { contents: T, } impl<T> ClosedBox<T> { // Một phương thức khởi tạo công khai pub fn new(contents: T) -> ClosedBox<T> { ClosedBox { contents: contents, } } } } fn main() { // Các cấu trúc công khai với các trường công khai có thể được khởi tạo một cách tự nhiên như sau let open_box = my::OpenBox { contents: "public information" }; // và các trường của chúng có thể được truy cập một cách bình thường. println!("The open box contains: {}", open_box.contents); // Các cấu trúc công khai với các trường riêng tư không thể được khởi tạo bằng cách sử dụng tên trường. // Lỗi! `ClosedBox` có các trường riêng tư //let closed_box = my::ClosedBox { contents: "classified information" }; // TODO ^ Hãy thử bỏ ghi chú của dòng trên này // Tuy nhiên, các cấu trúc với các trường riêng tư có thể được tạo bằng cách sử dụng // phương thức khởi tạo công khai let _closed_box = my::ClosedBox::new("classified information"); // và các trường riêng tư của một cấu trúc dù có phạm vi công khai thì cũng không thể được truy cập. // Lỗi! Trường `contents` là riêng tư //println!("The closed box contains: {}", _closed_box.contents); // TODO ^ Hãy thử bỏ ghi chú của dòng trên này }
Xem thêm:
Khai báo use
Khai báo use
có thể được sử dụng để ánh xạ một đường dẫn đầy đủ với một cái tên mới, để truy cập dễ dàng hơn. Nó thường được sử dụng như sau:
use crate::deeply::nested::{
my_first_function,
my_second_function,
AndATraitType
};
fn main() {
my_first_function();
}
Bạn có thể sử dụng từ khóa as
để ánh xạ những thứ được dẫn xuất vào module hiện tại với một cái tên khác:
// Ánh xạ đường dẫn `deeply::nested::function` đến `other_function`. use deeply::nested::function as other_function; fn function() { println!("called `function()`"); } mod deeply { pub mod nested { pub fn function() { println!("called `deeply::nested::function()`"); } } } fn main() { // Truy cập `deeply::nested::function` một cách dễ dàng. other_function(); println!("Entering block"); { // Cách này tương đương với `use deeply::nested::function as function`. // `function()` này sẽ che đi cái bên ngoài. use crate::deeply::nested::function; // các khai báo `use` có phạm vi cục bộ. Trong trường hợp này, // việc che đạy của `function()` chỉ áp dụng trong khối này. function(); println!("Leaving block"); } function(); }
super
và self
Các từ khóa super
và self
có thể được sử dụng trong đường dẫn để tránh sự nhầm lẫn khi truy cập các mục và cũng để tránh việc phải cố định các đường dẫn không cần thiết.
fn function() { println!("called `function()`"); } mod cool { pub fn function() { println!("called `cool::function()`"); } } mod my { fn function() { println!("called `my::function()`"); } mod cool { pub fn function() { println!("called `my::cool::function()`"); } } pub fn indirect_call() { // Hãy gọi tất cả các hàm có tên `function` từ phạm vi này! print!("called `my::indirect_call()`, that\n> "); // Từ khóa `self` chỉ đến phạm vi của module hiện tại - trong trường hợp này là `my`. // Khi sử dụng `self::function()` và gọi `function()` trực tiếp đều cho kết quả giống nhau, // vì chúng đều tham chiếu đến cùng một hàm. self::function(); function(); // Chúng ta cũng có thể sử dụng `self` để truy cập một module khác bên trong `my`: self::cool::function(); // Từ khóa `super` chỉ đến phạm vi cha mà chứa `my` (nằm ngoài module `my`). super::function(); // Cách này sẽ ánh xạ `cool::function` trong phạm vi của *crate*. // Trong trường hợp này, phạm vi của crate là phạm vi ngoài cùng { use crate::cool::function as root_function; root_function(); } } } fn main() { my::indirect_call(); }
Phân cấp tập tin
Các module có thể được ánh xạ vào một cấu trúc tập tin/thư mục. Hãy phân tích ví dụ visibility trong các tập tin:
$ tree .
.
├── my
│ ├── inaccessible.rs
│ └── nested.rs
├── my.rs
└── split.rs
Trong tập tin split.rs
:
// Khai báo này sẽ tìm tập tin tên là `my.rs` và sẽ
// chèn nội dung của nó vào trong một module tên `my` trong phạm vi này (scope).
mod my;
fn function() {
println!("called `function()`");
}
fn main() {
my::function();
function();
my::indirect_access();
my::nested::function();
}
Trong tập tin my.rs
:
// Tương tự `mod inaccessible` và `mod nested` sẽ tìm các tập tin `nested.rs`
// và `inaccessible.rs` và chèn chúng vào đây dưới các module tương ứng.
mod inaccessible;
pub mod nested;
pub fn function() {
println!("called `my::function()`");
}
fn private_function() {
println!("called `my::private_function()`");
}
pub fn indirect_access() {
print!("called `my::indirect_access()`, that\n> ");
private_function();
}
Trong tập tin my/nested.rs
:
pub fn function() {
println!("called `my::nested::function()`");
}
#[allow(dead_code)]
fn private_function() {
println!("called `my::nested::private_function()`");
}
Trong tập tin my/inaccessible.rs
:
#[allow(dead_code)]
pub fn public_function() {
println!("called `my::inaccessible::public_function()`");
}
Kiểm tra xem mọi thứ vẫn hoạt động như trước:
$ rustc split.rs && ./split
called `my::function()`
called `function()`
called `my::indirect_access()`, that
> called `my::private_function()`
called `my::nested::function()`
Crates
Một crate là một đơn vị biên dịch trong Rust. Bất cứ khi nào rustc some_file.rs
được gọi, some_file.rs
được xem như tập tin crate. Nếu some_file.rs
có các khai báo mod
bên trong nó, thì nội dung của các tập tin module sẽ được chèn vào những nơi khai báo mod
trong tập tin crate được tìm thấy, trước khi chạy trình biên dịch nó. Nói cách khác, các module không được biên dịch một cách riêng lẻ, mà chỉ các crate mới được biên dịch.
Một crate có thể được biên dịch thành một mã nhị phân hoặc thành một thư viện. Mặc định, rustc
sẽ tạo ra một mã nhị phân từ một crate. Hành vi này có thể bị ghi đè bằng cách truyền tham số --crate-type
đến lib
.
Tạo một thư viện
Hãy tạo một thư viện, và sau đó xem cách liên kết nó với một crate khác.
pub fn public_function() {
println!("called rary's `public_function()`");
}
fn private_function() {
println!("called rary's `private_function()`");
}
pub fn indirect_access() {
print!("called rary's `indirect_access()`, that\n> ");
private_function();
}
$ rustc --crate-type=lib rary.rs
$ ls lib*
library.rlib
Các thư viện có tiền tố là lib
, và mặc định chúng có cùng tên với tập tin crate, nhưng cái tên mặc định này có thể được ghi đè bằng cách truyền tùy chọn --crate-name
đến trình biên dịch rustc
hoặc bằng cách sử dụng thuộc tính crate_name
.
Sử dụng thư viện
Để liên kết một crate đến thư viện mới này bạn có thể sử dụng cờ --extern
của rustc
. Tất cả các mục của nó sẽ được nhập vào dưới một module có tên giống với thư viện. Module này thường hoạt động tương tự như các module khác.
// extern crate rary; // Có thể yêu cầu phiên bản Rust 2015 edition hoặc cũ hơn
fn main() {
rary::public_function();
// Lỗi! `private_function` là một hàm riêng tư trong thư viện
//rary::private_function();
rary::indirect_access();
}
# Nếu library.rlib là đường dẫn đến thư viện đã được biên dịch, giả sử nó
# nằm cùng thư mục với executable.rs, bạn có thể biên dịch và chạy như sau:
$ rustc executable.rs --extern rary=library.rlib && ./executable
called rary's `public_function()`
called rary's `indirect_access()`, that
> called rary's `private_function()`
Cargo
cargo
là công cụ quản lý gói chính thức của Rust. Nó có rất nhiều tính năng thực sự để giúp cải thiện chất lượng và tốc độ viết mã của nhà phát triển hữu ích! Bao gồm các chức năng:
- Quản lý các phụ thuộc và tích hợp với crates.io (nơi đăng ký các gói chính thức của Rust)
- Tự động phát hiện và thực thi unit tests, sử dụng câu lệnh cargo test
- Tự động phát hiện và thực thi benchmarks, sử dụng câu lệnh cargo bench
Chương này chúng ta sẽ đi nhanh qua 1 số khái niêm cơ bản, nhưng bạn có thể tìm thấy những tài liệu toàn diện trong The Cargo Book.
Dependencies
Hầu hết các chương trình đều phụ thuộc vào một vài thư viện nào đó. Nếu bạn đã từng tự tay quản lí các thư viện phụ thuộc này, bạn sẽ biết việc này có thể rất phiền toái. May mắn thay, hệ sinh thái Rust đi kèm với công cụ cargo! cargo có thể quản lý các phụ thuộc cho một dự án.
Để tạo mới một project Rust,
một tệp nhị phân cargo new foo tạo thư viện cargo new --lib bar
Trong phần tiếp theo của chương này, giả sử rằng chúng ta đang tạo 1 tập nhị phân, chứ không phải một thư viện, song các khái niệm là giống nhau.
Sau khi chạy các câu lệnh phía trên, bạn sẽ thấy một hệ thống phân cấp tệp tin như sau:
. ├── bar │ ├── Cargo.toml │ └── src │ └── lib.rs └── foo ├── Cargo.toml └── src └── main.rs
main.rs
là tệp tin gốc của dự án foo
vừa được tạo - không có gì mới ở đây. Cargo.toml
là tệp tin chứa cấu hình của cargo
đối với dự án này. Nếu bạn xem bên trong tệp tin, bạn sẽ thấy giống như thế này:
#![allow(unused)] fn main() { [package] name = "foo" version = "0.1.0" authors = ["mark"] [dependencies] }
Trường name
bên dưới [package]
xác định tên của dự án. Tên dự án cũng được sử dụng nếu bạn xuất bản crate lên crates.io
. Nó cũng là tên của tệp nhị phân đầu ra khi được biên dịch.
Trường version
xác định phiên bản của crate theo quy chuẩn Semantic Versioning.
Trường authors
là một danh sách các tác giả được sử dụng khi xuất bản crate.
Phần [dependencies]
để bạn tùy ý thêm các thư viện phụ thuộc vào dự án của mình.
Ví dụ, giả sử rằng chúng ta muốn chương trình của mình có một CLI tuyệt vời. Bạn có thể tìm kiếm thấy rất nhiều package trên crates.io (Địa chỉ chính thức của hệ thống đăng ký gói của Rust). Một lựa chọn phổ biến là clap. Tại thời điểm viết bài này, phiên bản mới nhất của clap
được công bố là 2.27.1
. Để thêm một phục thuộc cho chương trình của mình, chúng ta đơn giản chỉ cần thêm vào tệp Cargo.toml
ở phía dưới phần dependencies
:clap = "2.27.1"
. Và thế là xong. Bạn có thể bắt đầu sử dụng clap
trong chương trình của mình.
cargo
cũng hỗ trợ các kiểu dependency khác nữa. Ở đây là một thí dụ nhỏ:
#![allow(unused)] fn main() { [package] name = "foo" version = "0.1.0" authors = ["mark"] [dependencies] clap = "2.27.1" # from crates.io rand = { git = "https://github.com/rust-lang-nursery/rand" } # từ repo online. bar = { path = "../bar" } # đường dẫn trong hệ thống tệp cục bộ. }
cargo
không chỉ là một trình quản lí các phụ thuộc. Tất cả các tùy chọn cấu hình sẵn có đều được liệt kê trong đặc tả định dạng của Cargo.toml
Để build dự án, chúng ta có thể thực thi lệnh cargo build
ở bất kì đâu trong thư mục dự án (ngay cả trong các thư mục con). Chúng ta có thể sử dụng cargo run
để build và chạy chương trình. Lưu ý rằng những câu lệnh này sẽ giải quyết tất cả các phụ thuộc, tải xuống các crate nếu cần, và build tất cả mọi thứ, bao gồm crate của bạn. ( Lưu ý rằng nó chỉ build lại những gì mà nó chưa được build trước đó, tương tự như make
).
Trên đây là tất cả về quản lý các phụ thuộc.
Conventions
Trong chương trước, chúng ta đã thấy cấu trúc thư mục phân cấp như sau:
foo
├── Cargo.toml
└── src
└── main.rs
Tuy nhiên, giả sử rằng chúng ta muốn có hai tệp nhị phân trong cùng một dự án. Vậy thì sao?
cargo
hỗ trợ điều này. Tên tệp nhị phân mặc định là main
, như là chúng ta đã thấy trước đó, nhưng bạn có thể thêm những tệp nhị phân bổ sung bằng cách đặt chúng vào trong thư mục bin/
:
foo
├── Cargo.toml
└── src
├── main.rs
└── bin
└── my_other_bin.rs
Để yêu cầu cargo
chỉ biên dịch hay chạy tệp nhị phân này, chúng ta chỉ cần chạy cargo
với cờ --bin my_other_bin
, trong đó my_other_bin
là tên của tệp nhị phân mà chúng ta muốn làm việc.
Ngoài các tệp nhị phân bổ sung, cargo
hỗ trợ nhiều tính năng hơn như benchmarks, tests, và các ví dụ.
Trong chương tiếp theo, chúng ta sẽ xem xét kỹ hơn các bài kiểm tra
Tests
Như chúng ta đã biết kiểm thử không thể thiếu đối với bất kì giai đoạn nào trong quá trình phát triển phần mềm. Rust ưu tiên cho việc unit test( kiểm thử đơn vị) và integration test( kiểm thử tích hợp) ( xem chương này tại TRPL).
Từ các chương kiểm thử trong liên kết phía trên, chúng ta sẽ biết cách để viết unit test và integration test.
Về mặt tổ chức, chúng ta đặt các unit test bên trong các module, chúng sẽ chạy kiểm tra và tích hợp kiểm thử trong chính thư mục tests/
:
#![allow(unused)] fn main() { foo ├── Cargo.toml ├── src │ └── main.rs │ └── lib.rs └── tests ├── my_test.rs └── my_other_test.rs }
Mỗi tệp tin trong tests
là một integration test riêng biệt, tức là kiểm thử đó nhằm kiểm tra thư viện của bạn như thể nó được gọi từ một crate phụ thuộc.
Chương Testing này xây dựng trên 3 kiểu kiểm thử: Unit, Doc, và Integration.
cargo
đơn thuần giúp bạn chạy toàn bộ các kiểm thử đó một cách rất dễ dàng.
$ cargo test
Bạn sẽ thấy kết quả đầu ra như sau:
#![allow(unused)] fn main() { $ cargo test Compiling blah v0.1.0 (file:///nobackup/blah) Finished dev [unoptimized + debuginfo] target(s) in 0.89 secs Running target/debug/deps/blah-d3b32b97275ec472 running 3 tests test test_bar ... ok test test_baz ... ok test test_foo_bar ... ok test test_foo ... ok test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out }
Bạn cũng có thể chạy kiểm thử với tên của phép kiểm thử cụ thể(test_foo
):
#![allow(unused)] fn main() { $ cargo test test_foo }
#![allow(unused)] fn main() { $ cargo test test_foo Compiling blah v0.1.0 (file:///nobackup/blah) Finished dev [unoptimized + debuginfo] target(s) in 0.35 secs Running target/debug/deps/blah-d3b32b97275ec472 running 2 tests test test_foo ... ok test test_foo_bar ... ok test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out }
Lưu ý: Cargo có thể chạy nhiều kiểm thử cùng lúc, vì thế hãy chắc chắn rằng không xảy ra tranh chấp tài nguyên.
Một ví dụ về kiểm thử đồng thời này gây ra sự cố nếu hai kiểm thử cố gắng ghi vào cùng một tệp tin, chẳng hạn như dưới đây:
#![allow(unused)] fn main() { #[cfg(test)] mod tests { // Import vào chương trình các mô-đun cần thiết. use std::fs::OpenOptions; use std::io::Write; // Kiểm thử này được ghi ra một tệp. #[test] fn test_file() { // Mở tệp ferris.txt hoặc tạo ra nó nếu nó không tồn tại. let mut file = OpenOptions::new() .append(true) .create(true) .open("ferris.txt") .expect("Failed to open ferris.txt"); // Ghi vào tệp tin từ "Ferris" 5 lần. for _ in 0..5 { file.write_all("Ferris\n".as_bytes()) .expect("Could not write to ferris.txt"); } } // Kiểm thử này cố ghi vào cùng tệp tin( ferris.txt). #[test] fn test_file_also() { // Mở tệp ferris.txt hoặc tạo ra nó nếu nó không tồn tại. let mut file = OpenOptions::new() .append(true) .create(true) .open("ferris.txt") .expect("Failed to open ferris.txt"); // ghi vào tệp ti từ "Corro" 5 lần. for _ in 0..5 { file.write_all("Corro\n".as_bytes()) .expect("Could not write to ferris.txt"); } } } }
Mặc dù mục đích là để có được kết quả như sau:
#![allow(unused)] fn main() { $ cat ferris.txt Ferris Ferris Ferris Ferris Ferris Corro Corro Corro Corro Corro }
Nhưng điều thực sự được ghi vào ferris.txt là:
#![allow(unused)] fn main() { $ cargo test test_foo Corro Ferris Corro Ferris Corro Ferris Corro Ferris Corro Ferris }
Build Scripts
Đôi khi một bản build bình thường từ cargo
là không đủ. Crate của bạn có lẽ cần một số điều kiện tiên quyết trước khi cargo
có thể biên dịch thành công, những thứ như là tạo mã, hoặc một số mã gốc cần được biên dịch. Để giải quyết vấn đề này chúng ta có các kịch bản build mà Cargo có thể chạy.
Để thêm một kịch bản build vào gói của bạn, có thể chỉ định nó trong
Cargo.toml
như sau:
[package]
...
build = "build.rs"
Nếu không, Cargo sẽ tìm tệp build.rs
trong thư mục dự án theo mặc định.
Cách sử dụng một build script
Build script chỉ đơn giản là một tệp Rust khác sẽ được biên dịch và gọi trước khi biên dịch bất cứ thứ gì đó trong package(gói). Do đó, nó có thể được sử dụng để đáp ứng các điều kiện tiên quyết trong crate của bạn.
Cargo cung cấp script với những input qua biến môi trường được chỉ định ở đây để có thể sử dụng.
Script cung cấp output qua stdout. Tất cả các dòng in ra được ghi vào target/debug/build/<pkg>/output
. Hơn nữa, những dòng với tiền tố là cargo:
sẽ được Cargo thông dịch trực tiếp và do đó có thể được sử dụng để xác định các tham số cho quá trình biên dịch của gói.
Để biết thêm thông số kỹ thuật và ví dụ, hãy đọc Cargo specification.
Attributes
Một thuộc tính là metadata(siêu dữ liệu) áp dụng cho 1 số module, crate hoặc mục nào đó. Metadata này có thể được sử dụng để/cho:
- biên dịch mã có điều kiện
- đặt tên crate, phiên bản và loại (một tệp kiểu nhị phân hay là một thư viện)
- vô hiệu hóa lints (những cảnh báo)
- bật các tính năng của trình biên dịch (macros, glob imports, etc.)
- liên kết đến một thư viện ngoài
- đánh dấu các chức năng như là unit tests
- đánh dấu các chức năng sẽ là một phần của benchmark
- thuộc tính như macro
Khi các thuộc tính áp dụng cho toàn bộ crate, cú pháp của nó là #![crate_attribute]
, và khi chúng áp dụng cho một module hoặc mục, cú pháp là #[item_attribute]
(bỏ đi !
).
Các thuộc tính có thể nhận các đối số với những cú pháp khác nhau:
#[attribute = "value"]
#[attribute(key = "value")]
#[attribute(value)]
Các thuộc tính có thể có nhiều giá trị và cũng có thể được phân tách trên nhiều dòng:
#[attribute(value, value2)]
#[attribute(value, value2, value3,
value4, value5)]
dead_code
Trình biên dịch cung cấp một dead_code lint sẽ cảnh báo về các hàm không được sử dụng. Một thuộc tính có thể được sử dụng để vô hiệu hóa lint.
fn used_function() {} // `#[allow(dead_code)]` là một thuộc tính vô hiệu hóa `dead_code` lint #[allow(dead_code)] fn unused_function() {} fn noisy_unused_function() {} // FIXME ^ Thêm một thuộc tính để vô hiệu hóa cảnh báo fn main() { used_function(); }
Lưu ý rằng trong các chương trình thực tế, bạn nên loại bỏ dead code. Trong những ví dụ trên chúng tôi sẽ cho phép dead code ở một số nơi để làm nổi bật nó trong các ví dụ.
Crates
Thuộc tính crate_type
có thể được sử dụng để thông báo cho trình biên dịch biết một crate là một tệp kiểu nhị phân hay là một thư viện( và thậm chí là một loại thư viện nào), và thuộc tính crate_name
có thể được sử dụng để đặt tên cho crate.
Tuy nhiên, điều quan trọng cần lưu ý là cả 2 thuộc tính crate_type
và crate_name
đều không có tác dụng gì khi sử dụng Cargo - trình quản lý gói Rust. Kể từ khi Cargo được sử dụng cho phần lớn các project Rust, điều này có nghĩa việc sử dụng crate_type
và crate_name
trong thế giới thực là tương đối hạn chế.
// Crate này là một thư viện #![crate_type = "lib"] // Crate này có tên là "rary" #![crate_name = "rary"] pub fn public_function() { println!("called rary's `public_function()`"); } fn private_function() { println!("called rary's `private_function()`"); } pub fn indirect_access() { print!("called rary's `indirect_access()`, that\n> "); private_function(); }
Khi thuộc tính crate_type
này được sử dụng, chúng ta không còn cần truyền cờ --crate-type
cho trình biên dịch rustc
nữa.
$ rustc lib.rs
$ ls lib*
library.rlib
cfg
Có thể kiểm tra điều kiện cấu hình thông qua 2 toán tử khác nhau:
- thuộc tính
cfg
:#[cfg(...)]
ở vị trí thuộc tính - macro
cfg!
:cfg!(...)
trong biểu thức boolean
Trong khi cái đầu tiên sử dụng kiểm tra điều kiện khi biên dịch, điều kiện sau
đánh giá là true
hay false
trong thời gian chạy. Cả 2 đều dùng những tham số giống nhau.
cfg!
, không giống #[cfg]
, không xóa bất kỳ mã nào và chỉ đánh giá true
hoặc false
. Ví dụ, tất cả các block trong một câu điều kiện if/else cần phải hợp lệ khi sử dụng marco cfg!
, bất kể cái gì cfg!
đang đánh giá.
// Hàm này chỉ được biên dịch nếu OS là Linux #[cfg(target_os = "linux")] fn are_you_on_linux() { println!("You are running linux!"); } // Và hàm này chỉ được biên dịch nếu OS *không* là linux #[cfg(not(target_os = "linux"))] fn are_you_on_linux() { println!("You are *not* running linux!"); } fn main() { are_you_on_linux(); println!("Are you sure?"); if cfg!(target_os = "linux") { println!("Yes. It's definitely linux!"); } else { println!("Yes. It's definitely *not* linux!"); } }
See also:
the reference, cfg!
, and macros.
Custom
Một số điều kiện target_os
được cung cấp hoàn toàn bởi rustc
, nhưng một số điều kiện tùy phải chuyển tới rustc
bằng cách sử dụng cờ --cfg
.
#[cfg(some_condition)] fn conditional_function() { println!("condition met!"); } fn main() { conditional_function(); }
Thử chạy đoạn code trên xem điều gì xảy ra khi không sử dụng cờ tùy chỉnh cfg
.
Với cờ tùy chỉnh cfg
:
$ rustc --cfg some_condition custom.rs && ./custom
condition met!
Generics
Generics là chủ đề về việc khái quát hóa các kiểu dữ liệu và các hàm trong các trường hợp bao quát hơn. Điều này rất hữu ích cho việc giảm sự trùng lặp của mã theo nhiều cách, nhưng lại yêu cầu cú pháp phức tạp hơn. Cụ thể, việc sử dụng generic bắt buộc phải được chú ý rất kĩ để xác định các kiểu dữ liệu nào là kiểu generic được xem xét hợp lệ. Việc sử dụng generic đơn giản và phổ biến nhất là dùng các tham số có dạng kiểu dữ liệu.
Tham số có dạng kiểu dữ liệu được chỉ định là generic bằng cách sử dụng dấu ngoặc nhọn và Camel case: <Aaa, Bbb, ...>
. "Tham số kiểu generic" thường được biểu diễn dưới dạng <T>
. Trong Rust, "generic" cũng miêu tả bất cứ thực thể gì chấp nhận một hoặc nhiều tham số kiểu generic, có dạng <T>
. Bất kỳ kiểu nào được chỉ định là tham số kiểu generic đều mang tính khái quát, và tất cả những kiểu còn lại phải cụ thể (không generic).
Ví dụ, định nghĩa một hàm generic foo
nhận đối số T
đối với bất kỳ kiểu nào:
fn foo<T>(arg: T) { ... }
Do T
đã được chỉ định là tham số kiểu generic bằng cách sử dụng <T>
, nó được coi là khái quát khi sử dụng ở đây như là (arg: T)
. Trường này đúng ngay cả khi T
đã được định nghĩa trước đó như là một struct.
Ví dụ này thể hiện một số cú pháp trong thực tế:
// Một kiểu dữ liệu riêng biệt `A`. struct A; // Khi định nghĩ kiểu `Single`, lần sử dụng đầu tiên của `A` không được đi kèm với `<A>`. // Vì thế, `Single` là một kiểu dữ liệu riêng biệt, and kiểu `A` ở trên. struct Single(A); // ^ Đây là lúc `Single` lần đầu sử dụng kiểu `A`. // Ở đây, `<T>` đi kèm với lần sử dụng đầu tiên của `T`, vì vậy `SingleGen` là một kiểu dữ liệu generic. // Bởi vì tham số kiểu `T` là khái quát, nó có thể là bất cứ thứ gì, bao gồm // cả kiểu dữ liệu riêng biệt A được định nghĩa ở trên. struct SingleGen<T>(T); fn main() { // `Single` là kiểu dữ liệu cụ thể và rõ ràng có tham số A. let _s = Single(A); // Tạo một biến `_char` có kiểu `SingleGen<char>` // và gán nó với giá trị `SingleGen('a')`. // Trong trường hợp này, `SingleGen` có một tham số đã được chỉ định rõ ràng. let _char: SingleGen<char> = SingleGen('a'); // Kiểu `SingleGen` cũng có thể chứa kiểu tham số được ngầm chỉ định let _t = SingleGen(A); // Sử dụng `A` được xác định ở trên. let _i32 = SingleGen(6); // Sử dụng `i32`. let _char = SingleGen('a'); // Sử dụng `char`. }
Tham khảo:
Functions
Một bộ quy tắc có thể được áp dụng cho các function: Một kiểu T
trở thành generic khi khai báo generic type '
Việc sử dụng generic function đôi khi yêu cầu kiểu dữ liệu cụ thể cho tham số . Điều này xảy ra nếu hàm được gọi trong trường hợp kiểu trả về là generic, hoặc nếu trình biên dịch không đủ thông tin để suy ra các kiểu của tham số cần thiết.
Một hàm với kiểu dữ liệu cụ thể có cú pháp như sau: fun::<A, B, ...>()
.
struct A; // Kiểu cụ thể `A`. struct S(A); // Kiểu cụ thể `S`. struct SGen<T>(T); // Kiểu Generic `SGen`. // Tất cả các function dưới đây sẽ take ownership của biến được truyền vào và // ngay lập tức thoát khỏi scope, giải phóng variable. // Định nghĩa function `reg_fn` với tham số ``_s` có kiểu `S` // Không có `<T>` nên đây không phải là generic function. fn reg_fn(_s: S) {} // Định nghĩa function `gen_spec_t` với một tham số `_s` có kiểu `SGen<T>`. // Hàm này đã chỉ rõ kiểu tham số là `A`, nhưng bời vì `A` không được // chỉ định như một kiểu tham số generic cho `gen_spec_t` nên đây không phải là generic. fn gen_spec_t(_s: SGen<A>) {} // Định nghĩa một hàm `gen_spec_i32` với tham số là `_s` có kiểu `SGen<i32`>. // Hàm này chỉ rõ kiểu tham số là `i32`. // Bởi vì `i32` không phải là một kiểu generic, nên hàm này cũng không phải là generic function. fn gen_spec_i32(_s: SGen<i32>) {} // Định nghĩa một `generic` function với tham số '_s' có kiểu 'SGen<T>'. // Bởi vì `SGen<T>` được đứng trước bởi `<T>`, nên function này là generic bởi `T`. fn generic<T>(_s: SGen<T>) {} fn main() { // Sử dụng non-generic function reg_fn(S(A)); // Concrete type. gen_spec_t(SGen(A)); // Ngầm chỉ định type parameter `A` gen_spec_i32(SGen(6)); // Ngầm chỉ định type parameter `i32`. // Chỉ định cụ thể kiểu `char` cho `generic()` function. generic::<char>(SGen('a')); // Chỉ định cụ thể kiểu `char` cho `generic()` function. generic(SGen('c')); }
Implementation
Tương tự như các hàm(function), các triển khai cũng yêu cầu sự cẩn thận để giữ cho chúng có tính tổng quát(generic).
#![allow(unused)] fn main() { struct S; // Kiểu dữ liệu cụ thể struct `S` struct GenericVal<T>(T); // Kiểu giữ liệu chung(generic) `GenericVal` // triển khai của GenericVal trong đó chúng ta chỉ định rõ ràng các kiểu tham số: impl GenericVal<f32> {} // Chỉ định rõ ràng kiểu `f32` impl GenericVal<S> {} // Chỉ định rõ ràng kiểu `S` được định nghĩa ở trên // `<T>` Phải đặt trước kiểu để giữ tính tổng quát impl<T> GenericVal<T> {} }
struct Val { val: f64, } struct GenVal<T> { gen_val: T, } // triển khai của Val impl Val { fn value(&self) -> &f64 { &self.val } } // triển khai của GenVal cho kiểu dữ liệu chung `T` impl<T> GenVal<T> { fn value(&self) -> &T { &self.gen_val } } fn main() { let x = Val { val: 3.0 }; let y = GenVal { gen_val: 3i32 }; println!("{}, {}", x.value(), y.value()); }
See also:
functions returning references, impl
, and struct
Traits
Tất nhiên trait
cũng có thể là generic. Ở đây chúng ta định nghĩa một phương thức triển khai lại Drop trait
như một phương thức generic để Drop
chính nó và input.
// Các kiểu không thể coppy. struct Empty; struct Null; // Một trait generic `T`. trait DoubleDrop<T> { // Định nghĩa một method trên type hiện tại, method nhận một giá trị khác // cũng có kiểu `T` và không làm gì với nó. fn double_drop(self, _: T); } // Implement `DoubleDrop<T>` cho mọi generic parameter `T` và // caller `U`. impl<T, U> DoubleDrop<T> for U { // Method này sẽ take ownership của cả 2 tham số, // sau đó giải phóng bộ nhớ cho cả 2. fn double_drop(self, _: T) {} } fn main() { let empty = Empty; let null = Null; // Giải phóng `empty` and `null`. empty.double_drop(null); //empty; //null; // ^ TODO: Try uncommenting these lines. }
Bounds
Khi làm việc với generics, các tham số thường phải sử dụng các traits như bounds
để chỉ định chức năng mà một kiểu triển khai. Ví dụ sau sử dụng trait Display
để in và do đó nó yêu cầu T
được ràng buộc bởi Display
; nói cách khác, T
phải triển khai Display
.
// Định nghĩa hàm `printer` nhận kiểu `T` generic,
// kiểu `T` phải triển khai trait `Display`.
fn printer<T: Display>(t: T) {
println!("{}", t);
}
Bounding hạn chế generic thành các kiểu phù hợp với bounds. Nghĩa là:
struct S<T: Display>(T);
// Lỗi! `Vec<T>` không triển khai `Display`.
let s = S(vec![1]);
Một công dụng khác của bounding là các generic instance được phép truy cập đến các methods của các traits được chỉ định trong bounds. Ví dụ:
// Một trait triển khai marker in: `{:?}`. use std::fmt::Debug; trait HasArea { fn area(&self) -> f64; } impl HasArea for Rectangle { fn area(&self) -> f64 { self.length * self.height } } #[derive(Debug)] struct Rectangle { length: f64, height: f64 } #[allow(dead_code)] struct Triangle { length: f64, height: f64 } // Generic `T` phải triển khai `Debug`. // Bất kể là loại nào, nó vẫn sẽ hoạt động đúng cách. fn print_debug<T: Debug>(t: &T) { println!("{:?}", t); } // `T` phải triển khai `HasArea`. Bất kỳ kiểu nào đáp ứng // bound này đều có thể sử dụng hàm `area` của `HasArea`. fn area<T: HasArea>(t: &T) -> f64 { t.area() } fn main() { let rectangle = Rectangle { length: 3.0, height: 4.0 }; let _triangle = Triangle { length: 3.0, height: 4.0 }; print_debug(&rectangle); println!("Area: {}", area(&rectangle)); //print_debug(&_triangle); //println!("Area: {}", area(&_triangle)); // ^ TODO: Hãy thử bỏ dấu chú thích cho phần này. // | Lỗi: Không triển khai `Debug` hoặc `HasArea`. }
Ngoài ra, mệnh đề where
cũng có thể được sử dụng để áp dụng bounds
trong một số trường hợp để biểu đạt rõ ràng hơn.
Xem thêm:
Testcase: empty bounds
Bộ kiểm thử: ràng buộc rỗng
Theo cách các ràng buộc (bounds) hoạt động thì là ngay cả khi một trait
không bao gồm bất cứ hàm nào, ta vẫn có thể sử dụng nó như một ràng buộc.
Eq
và Copy
là những ví dụ của các trait
như vậy từ thư viện std
.
struct Cardinal; struct BlueJay; struct Turkey; trait Red {} trait Blue {} impl Red for Cardinal {} impl Blue for BlueJay {} // Những hàm này chỉ hợp lệ với các kiểu dữ liệu đã triển khai các trait này. // Thực tế là dù trait không có bất kì hàm nào cũng chẳng sao. fn red<T: Red>(_: &T) -> &'static str { "red" } fn blue<T: Blue>(_: &T) -> &'static str { "blue" } fn main() { let cardinal = Cardinal; let blue_jay = BlueJay; let _turkey = Turkey; // Hàm `red()` không hoạt động với kiểu blue_jay // và ngược lại bởi các ràng buộc (bounds). println!("Chim hồng y giáo chủ có màu {}", red(&cardinal)); println!("Chim giẻ cùi làm có màu {}", blue(&blue_jay)); //println!("Gà tây có màu {}", red(&_turkey)); // ^ TODO: Hãy thử bỏ comment dòng trên. }
Tham khảo thêm:
std::cmp::Eq
, std::marker::Copy
, and trait
s
Multiple bounds
Trong Rust, chúng ta có thể áp dụng nhiều ràng buộc cho một kiểu dữ liệu bằng cách sử dụng toán tử +
. Thông thường, các kiểu dữ liệu khác nhau được phân tách bằng dấu ,
.
use std::fmt::{Debug, Display}; fn compare_prints<T: Debug + Display>(t: &T) { println!("Debug: `{:?}`", t); println!("Display: `{}`", t); } fn compare_types<T: Debug, U: Debug>(t: &T, u: &U) { println!("t: `{:?}`", t); println!("u: `{:?}`", u); } fn main() { let string = "words"; let array = [1, 2, 3]; let vec = vec![1, 2, 3]; compare_prints(&string); //compare_prints(&array); // TODO ^ thử bỏ chú thích(uncommenting) dòng phía trên. compare_types(&array, &vec); }
See also:
Mệnh đề where
Một bound (ràng buộc) cũng có thể được định nghĩa bằng cách sử dụng mệnh đề where
ngay trước dấu {
, thay vì phải định nghĩa tại ngay lần đề cập đầu tiên của một kiểu.
Ngoài ra, các mệnh đề where
có thể áp dụng bound cho các kiểu tuỳ ý, chứ không chỉ riêng cho các tham số kiểu (type parameters).
Việc sử dụng mệnh đề where
sẽ hữu dụng khi rơi vào những trường hợp sau:
- Khi chỉ định rõ các kiểu dữ liệu generic và bound của chúng một cách riêng biệt.
impl <A: TraitB + TraitC, D: TraitE + TraitF> MyTrait<A, D> for YourType {}
// Biểu thị bounds bằng mệnh đề `where`
impl <A, D> MyTrait<A, D> for YourType where
A: TraitB + TraitC,
D: TraitE + TraitF {}
- Khi mà việc sử dụng mệnh đề
where
sẽ trực quan dễ đọc hơn việc sử dụng cú pháp thông thường:
Phần impl
trong ví dụ dưới đây sẽ không thể được biểu thị trực tiếp nếu không dùng mệnh đề where
use std::fmt::Debug; trait PrintInOption { fn print_in_option(self); } // Ở đoạn này, nếu không dùng `where`, ta sẽ phải khai báo như sau `T: Debug` hoặc là // dùng một cách gián tiếp khác để khai báo, do đó ta sử dụng mệnh đề `where` để dễ đọc hơn: impl<T> PrintInOption for T where Option<T>: Debug { // Chúng ta muốn `Option<T>: Debug` sẽ là bound ở đây bởi vì đó là thứ sẽ được in ra. // Nếu sử dụng cách khác sẽ dẫn đến việc sử dụng một bound không đúng. fn print_in_option(self) { println!("{:?}", Some(self)); } } fn main() { let vec = vec![1, 2, 3]; vec.print_in_option(); }
Đọc thêm:
New Type Idiom
Thành ngữ newtype
đảm bảo tại thời điểm biên dịch rằng giá trị với đúng kiểu được cung cấp cho chương trình.
Ví dụ, một hàm xác minh tuổi tác phải được truyền vào một giá trị kiểu Years
.
struct Years(i64); struct Days(i64); impl Years { pub fn to_days(&self) -> Days { Days(self.0 * 365) } } impl Days { /// Căt bỏ phần dư của năm, tính năm tròn. pub fn to_years(&self) -> Years { Years(self.0 / 365) } } fn old_enough(age: &Years) -> bool { age.0 >= 18 } fn main() { let age = Years(5); let age_days = age.to_days(); println!("Đủ lớn {}", old_enough(&age)); println!("Đủ lớn {}", old_enough(&age_days.to_years())); // println!("Đủ lớn {}", old_enough(&age_days)); }
Bỏ ghi chú dòng cuối cùng để thấy rằng kiểu được cung cấp phải là Years
.
Để lấy được giá trị newtype
theo kiểu cơ sở, bạn có thể sử dụng cú pháp tuple
hoặc phân rã (destructuring) như sau:
struct Years(i64); fn main() { let years = Years(42); let years_as_primitive_1: i64 = years.0; // Tuple let Years(years_as_primitive_2) = years; // Destructuring }
Xem thêm:
Associated items
"Associated Items" là một tập hợp các quy tắc liên quan đến các item
của các kiểu khác nhau.
Nó là một phần mở rộng của trait
generics, nó cho phép các trait
có thể định nghĩa các item
mới bên trong.
Một item như vậy được gọi là associated type, nó cung cấp các mẫu sử dụng đơn giản hơn khi trait
được khai báo với tính chất generic đối với kiểu container của nó.
Xem thêm:
The Problem
Một trait
được khai báo với tính chất generic đối với kiểu container của nó có các yêu cầu về kiểu -
người sử dụng trait
phải chỉ định tất cả các kiểu generic của nó.
Trong ví dụ dưới đây, trait
Contains
cho phép sử dụng các kiểu generic A
và B
.
Sau đó, trait được triển khai cho kiểu Container
, chỉ định kiểu i32
cho A
và B
để có thể sử dụng với hàm difference().
Bởi vì Contains
là generic, chúng ta buộc phải chỉ định rõ ràng tất cả các
kiểu generic cho fn difference()
. Trong thực tế, chúng ta muốn có một cách để
biểu thị rằng A
và B
được xác định bởi đầu vào C
. Bạn sẽ thấy trong phần tiếp
theo, associated types cho ta khả năng đó.
struct Container(i32, i32); // Trait kiểm tra xem 2 phần tử có được lưu trữ bên trong container hay không. // Trait cũng trích xuất giá trị đầu tiên hoặc cuối cùng. trait Contains<A, B> { fn contains(&self, _: &A, _: &B) -> bool; // Yêu cầu `A` và `B` rõ ràng. fn first(&self) -> i32; // Không yêu cầu rõ ràng `A` hoặc `B`. fn last(&self) -> i32; // Không yêu cầu rõ ràng `A` hoặc `B`. } impl Contains<i32, i32> for Container { // Trả về `true` nếu các số được lưu trữ bằng nhau. fn contains(&self, number_1: &i32, number_2: &i32) -> bool { (&self.0 == number_1) && (&self.1 == number_2) } // Lấy số đầu tiên. fn first(&self) -> i32 { self.0 } // Lấy số cuối cùng. fn last(&self) -> i32 { self.1 } } // `C` chứa `A` và `B`. Vì vậy, phải biểu thị lại `A` và // `B` sẽ khá là bất tiện. fn difference<A, B, C>(container: &C) -> i32 where C: Contains<A, B> { container.last() - container.first() } fn main() { let number_1 = 3; let number_2 = 10; let container = Container(number_1, number_2); println!("Container có chứa {} và {}: {}", &number_1, &number_2, container.contains(&number_1, &number_2)); println!("Số đầu: {}", container.first()); println!("Số cuối: {}", container.last()); println!("Hiệu 2 số là: {}", difference(&container)); }
Xem thêm:
Associated types
Việc sử dụng "Associated types" giúp code dễ đọc hơn bằng cách chuyển
các kiểu bên trong cục bộ vào một trait như là các kiểu output.
Cú pháp định nghĩa trait
:
#![allow(unused)] fn main() { // `A` và `B` được định nghĩa trong trait thông qua từ khóa `type`. // (Lưu ý: `type` trong ngữ cảnh này khác với `type` khi sử dụng cho các aliases). trait Contains { type A; type B; // Cập nhật cú pháp để tham chiếu đến các kiểu mới này theo cách chung chung (generically). fn contains(&self, _: &Self::A, _: &Self::B) -> bool; } }
Lưu ý rằng các hàm sử dụng trait
Contains
không cần phải biểu thị A
hoặc B
nữa:
// Không sử dụng associated types
fn difference<A, B, C>(container: &C) -> i32 where
C: Contains<A, B> { ... }
// Sử dụng associated types
fn difference<C: Contains>(container: &C) -> i32 { ... }
Tiếp theo, chúng ta sẽ viết lại ví dụ từ phần trước bằng cách sử dụng associated types:
struct Container(i32, i32); // Trait kiểm tra xem 2 phần tử có được lưu trữ bên trong container hay không. // Trait cũng trích xuất giá trị đầu tiên hoặc cuối cùng. trait Contains { // Định nghĩa kiểu generics mà các method có thể sử dụng. type A; type B; fn contains(&self, _: &Self::A, _: &Self::B) -> bool; fn first(&self) -> i32; fn last(&self) -> i32; } impl Contains for Container { // Chỉ định kiểu dữ liệu `A` và `B`. Nếu kiểu dữ liệu `input` // là `Container(i32, i32)`, kiểu dữ liệu `output` sẽ được xác định // là `i32` và `i32`. type A = i32; type B = i32; // `&Self::A` và `&Self::B` cũng hợp lệ ở đây. fn contains(&self, number_1: &i32, number_2: &i32) -> bool { (&self.0 == number_1) && (&self.1 == number_2) } // Lấy số đầu tiên. fn first(&self) -> i32 { self.0 } // Lấy số cuối cùng. fn last(&self) -> i32 { self.1 } } fn difference<C: Contains>(container: &C) -> i32 { container.last() - container.first() } fn main() { let number_1 = 3; let number_2 = 10; let container = Container(number_1, number_2); println!("Container có chứa {} và {}: {}", &number_1, &number_2, container.contains(&number_1, &number_2)); println!("Số đầu: {}", container.first()); println!("Số cuối: {}", container.last()); println!("Hiệu 2 số là: {}", difference(&container)); }
Phantom type parameters
Một tham số kiểu phantom là một kiểu không xuất hiện trong thời gian chạy, nhưng được kiểm tra tĩnh (và chỉ) tại thời điểm biên dịch.
Các kiểu dữ liệu có thể sử dụng các tham số kiểu generic bổ sung để hoạt động như các nhãn hoặc để thực hiện kiểm tra kiểu tại thời điểm biên dịch. Những tham số bổ sung này không giữ các giá trị bộ nhớ và không có hành vi thời gian chạy (runtime behavior).
Trong ví dụ sau, chúng ta kết hợp std::marker::PhantomData với khái niệm tham số kiểu phantom để tạo các tuple chứa các kiểu dữ liệu khác nhau.
use std::marker::PhantomData; // Một cấu trúc tuple phantom được tạo ra dựa trên kiểu `A` với tham số kiểu `B` ẩn. #[derive(PartialEq)] // Cho phép kiểu này có thể kiểm tra bằng nhau. struct PhantomTuple<A, B>(A, PhantomData<B>); // Một cấu trúc phantom được tạo ra dựa trên kiểu `A` với tham số kiểu `B` ẩn. #[derive(PartialEq)] // Cho phép kiểu này có thể kiểm tra bằng nhau. struct PhantomStruct<A, B> { first: A, phantom: PhantomData<B> } // Ghi chú: bộ nhớ được cấp phát cho kiểu tổng quát `A`, nhưng không được cấp phát cho `B`. // Vì vậy, `B` không thể được dùng trong tính toán. fn main() { // Ở đây, `f32` và `f64` là các tham số ẩn. // Kiểu PhantomTuple được chỉ định là `<char, f32>`. let _tuple1: PhantomTuple<char, f32> = PhantomTuple('Q', PhantomData); // Kiểu PhantomTuple được chỉ định là `<char, f64>`. let _tuple2: PhantomTuple<char, f64> = PhantomTuple('Q', PhantomData); // Kiểu chỉ định là `<char, f32>`. let _struct1: PhantomStruct<char, f32> = PhantomStruct { first: 'Q', phantom: PhantomData, }; // Kiểu chỉ định là `<char, f64>`. let _struct2: PhantomStruct<char, f64> = PhantomStruct { first: 'Q', phantom: PhantomData, }; // Compile-time Error! Kiểu dữ liệu không khớp với kiểu đã chỉ định nên không thể so sánh: // println!("_tuple1 == _tuple2 yields: {}", // _tuple1 == _tuple2); // Compile-time Error! Kiểu dữ liệu không khớp với kiểu đã chỉ định nên không thể so sánh: // println!("_struct1 == _struct2 yields: {}", // _struct1 == _struct2); }
Xem thêm:
Derive, struct, and TupleStructs
Testcase: unit clarification
Bộ kiểm thử: Sự tường minh của Đơn vị đo lường
Một phương pháp hữu ích cho việc chuyển đổi đơn vị có thể được thực hiện bằng cách
triển khai trait Add
với một tham số có kiểu dữ liệu ảo. Dưới đây, Trait
Add
được
triển khai như một ví dụ:
// Cấu trúc này sẽ áp dụng: `Self + RHS = Output`
// trong đó RHS sẽ mặc định là Self nếu không được chỉ định trong việc triển khai
pub trait Add<RHS = Self> {
type Output;
fn add(self, rhs: RHS) -> Self::Output;
}
// `Output` phải có kiểu `T<U>` sao cho `T<U> + T<U> = T<U>`.
impl<U> Add for T<U> {
type Output = T<U>;
...
}
Toàn bộ đoạn mã được triển khai:
use std::ops::Add; use std::marker::PhantomData; /// Tạo các enums trống để định nghĩa các loại đơn vị. #[derive(Debug, Clone, Copy)] enum Inch {} #[derive(Debug, Clone, Copy)] enum Mm {} /// `Length` là một kiểu dữ liệu có tham số ảo `Unit`, và /// không phải là generic theo đơn vị chiều dài (trong trường hợp này là f64) /// `f64` vốn đã được triển khai các trait `Clone` and `Copy`. #[derive(Debug, Clone, Copy)] struct Length<Unit>(f64, PhantomData<Unit>); /// Trait `Add` định nghĩa hành vi (behavior) của phép tính `+`. impl<Unit> Add for Length<Unit> { type Output = Length<Unit>; // Hàm add() trả về một struct `Length` chứa tổng. fn add(self, rhs: Length<Unit>) -> Length<Unit> { // `+` thực thi trait `Add` của kiểu `f64`. Length(self.0 + rhs.0, PhantomData) } } fn main() { // Chỉ định biến `one_foot` có tham số mang kiểu dữ liệu ảo là `Inch`. let one_foot: Length<Inch> = Length(12.0, PhantomData); // `one_meter` có tham số mang kiểu dữ liệu ảo là `Mm`. let one_meter: Length<Mm> = Length(1000.0, PhantomData); // `+` goi tới phương thức `add()` mà ta đã triển khai cho struct `Length<Unit>`. // // Vì struct `Length` đã được triển khai trait `Copy`, hàm `add()` sẽ không tiêu thụ 2 biến // `one_foot` và `one_meter` mà chỉ copy chúng trở thành `self` and `rhs`. let two_feet = one_foot + one_foot; let two_meters = one_meter + one_meter; // Phép cộng hoạt động. println!("one foot + one_foot = {:?} in", two_feet.0); println!("one meter + one_meter = {:?} mm", two_meters.0); // Các phép tính vô lý sẽ thất bại như là một điều hiển nhiên: // Lỗi tại thời điểm biên dịch: không phù hợp kiểu dữ liệu. // let one_feter = one_foot + one_meter; }
Tham khảo:
Borrowing (&
), Bounds (X: Y
), enum, impl & self,
Overloading, ref, Traits (X for Y
), and TupleStructs.
Scoping rules
Scope (phạm vi) đóng một vai trò trong quyền sở hữu, mượn và thời gian tồn tại. Nghĩa là, nó chỉ ra cho trình biên dịch tại đâu thì quyền mượn hợp lệ, tại đâu tài nguyên có thể được giải phóng, và khi nào các biến được tạo hoặc hủy.
RAII
Biến trong Rust sẽ có vai trò nhiều hơn việc chỉ lưu tài nguyên trên stack: chúng còn sở hữu tài nguyên đó.
ví dụ như Box<T>
sở hữu tài nguyên trên heap. Rust tuân thủ RAII
(Resource Acquisition Is Initialization) do đó khi một đối tượng ra khỏi scope của nó,
destructor của nó sẽ được gọi và tài nguyên của nó sở hữu được giải phóng.
Hành vi này giúp đảm bảo chống lại các lỗi rò rỉ tài nguyên (resource leak), do đó bạn không cần phải tự giải phóng tài nguyên một cách thủ công hoặc lo lắng về rò rỉ bộ nhớ nữa! Dưới đây là một ví dụ minh họa ngắn:
// raii.rs fn create_box() { // Cấp phát một integer trên heap let _box1 = Box::new(3i32); // `_box1` được xoá ở đây và giải phóng khỏi bộ nhớ } fn main() { // Cấp phát một integer trên heap let _box2 = Box::new(5i32); // Một scope lồng nhau: { // Cấp phát một integer trên heap let _box3 = Box::new(4i32); // `_box3` được xoá ở đây và giải phóng khỏi bộ nhớ } // Tạo thêm nhiều biến box nữa // Không cần phải tự giải phóng bộ nhớ một cách thủ công! for _ in 0u32..1_000 { create_box(); } // `_box2` được xoá ở đây và giải phóng khỏi bộ nhớ }
Tất nhiên là ta có thể kiểm tra lại các lỗi về bộ nhớ bằng cách sử dụng valgrind
:
$ rustc raii.rs && valgrind ./raii
==26873== Memcheck, a memory error detector
==26873== Copyright (C) 2002-2013, and GNU GPL'd, by Julian Seward et al.
==26873== Using Valgrind-3.9.0 and LibVEX; rerun with -h for copyright info
==26873== Command: ./raii
==26873==
==26873==
==26873== HEAP SUMMARY:
==26873== in use at exit: 0 bytes in 0 blocks
==26873== total heap usage: 1,013 allocs, 1,013 frees, 8,696 bytes allocated
==26873==
==26873== All heap blocks were freed -- no leaks are possible
==26873==
==26873== For counts of detected and suppressed errors, rerun with: -v
==26873== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 2 from 2)
Không có lỗi rò rỉ tài nguyên nào!
Destructor
Khái niệm về destructor trong Rust được cung cấp thông qua trait Drop
.
Destructor được gọi khi tài nguyên ra khỏi phạm vi. Trait này không nhất thiết phải
được implement cho tất cả các kiểu mà chỉ cần implement cho kiểu của bạn khi bạn cần
thực hiện một logic riêng với destructor của kiểu đó.
Hãy chạy ví dụ dưới đây để xem cách mà trait Drop
hoạt động. Bất cứ khi nào biến trong hàm
main
ra khỏi scope của hàm thì destructor mà đã tuỳ chỉnh sẽ được gọi.
struct ToDrop; impl Drop for ToDrop { fn drop(&mut self) { println!("ToDrop is being dropped"); } } fn main() { let x = ToDrop; println!("Made a ToDrop!"); }
Đọc thêm:
Ownership và moves
Bởi vì các biến tự chịu trách nhiệm cho việc giải phóng tài nguyên1 của chúng nên một tài nguyên chỉ có thể có một owner. Điều này giúp ngăn chặn việc tài nguyên bị giải phóng nhiều lần. Hãy lưu ý rằng, không phải tất cả các biến đều sở hữu tài nguyên (ví dụ: references)
Khi thực hiện các phép gán (let x = y
) hoặc truyền đối số vào hàm bằng giá trị (foo(x)
),
ownership của tài nguyên sẽ được chuyển cho đối tượng khác. Trong Rust, ta gọi nó là move.
Sau khi move tài nguyên, owner trước đó không thể sử dụng tài nguyên đó nữa. Điều này giúp tránh hiện tượng tạo ra các con trỏ treo (dangling pointers).
// Hàm này lấy đi ownership của tài nguyên được cấp phát trên heap fn destroy_box(c: Box<i32>) { println!("Destroying a box that contains {}", c); // `c` bị xoá và giải phóng khỏi bộ nhớ } fn main() { // Một giá trị integer được cấp phát trên _stack_ let x = 5u32; // *Copy* `x` vào `y` - không có tài nguyên nào bị moved let y = x; // Cả hai giá trị đều có thể được sử dụng độc lập println!("x is {}, and y is {}", x, y); // `a` là một con trỏ đến một giá trị integer được cấp phát trên _heap_ let a = Box::new(5i32); println!("a contains: {}", a); // *Move* `a` đến `b` let b = a; // Địa chỉ của con trỏ `a` (chứ không phải dữ liệu) được copy đến `b` // Cả hai bây giờ đều trỏ đến cùng một dữ liệu được cấp phát trên heap, // nhưng `b` giờ đây sở hữu dữ liệu đó. // Lỗi xảy ra! `a` không thể nào truy cập dữ liệu được nữa vì nó không còn // sỡ hữu dữ liệu được cấp phát trên heap đó nữa. //println!("a contains: {}", a); // TODO ^ Hãy thử bỏ chú thích dòng này // Hàm này lấy đi ownership của tài nguyên được cấp phát trên heap khỏi `b` destroy_box(b); // Tài nguyên trên heap đã được giải phóng tại thời điểm này, hành động sau sẽ dẫn đến việc // giải tham chiếu (dereference) một tài nguyên đã được giải phóng khỏi bộ nhớ, nhưng điều này bị cấm bởi compiler. // Lỗi xảy ra! Nguyên nhân giống như lỗi trước đó. //println!("b contains: {}", b); // TODO ^ Hãy thử bỏ chú thích dòng này }
Lời người dịch: Tài nguyên (resource) được đề cập trong ngữ cảnh của Rust có thể là các giá trị như một đối tượng heap-allocated (có nghĩa là giá trị được cấp phát động và quản lý bởi hệ điều hành), file handle, socket, hoặc bất cứ thứ gì khác mà ứng dụng của bạn cần phải cấp phát, sử dụng và giải phóng khi không cần thiết nữa để giải phóng tài nguyên và tránh lãng phí bộ nhớ hoặc tài nguyên hệ thống.
Mutability
Tính biến đổi của dữ liệu có thể thay đổi khi quyền sở hữu được chuyển giao.
fn main() { let immutable_box = Box::new(5u32); println!("immutable_box chứa {}", immutable_box); // Lỗi biến đổi (không thể thay đổi giá trị biến immutable) //*immutable_box = 4; // *Move* hộp, sẽ thay đổi quyền sở hữu (và khả năng biến đổi) của nó let mut mutable_box = immutable_box; println!("mutable_box chứa {}", mutable_box); // Sửa đổi nội dung của hộp (hợp lệ vì biến là mutable) *mutable_box = 4; println!("mutable_box bây giờ chứa {}", mutable_box); }
Partial moves
Trong quá trình destructuring (giải cấu trúc) của một biến, cả hai mẫu ràng buộc
theo cách di chuyển
và theo tham chiếu
có thể được sử dụng đồng thời.
Điều này sẽ dẫn đến việc di chuyển một phần của biến, có nghĩa
là một số phần của biến sẽ được di chuyển trong khi các phần khác vẫn được giữ nguyên
.Trong trường hợp đó, biến cha không thể được sử dụng sau đó như một
thể duy nhất, tuy nhiên các phần chỉ được tham chiếu (và không được di chuyển) vẫn có thể được sử dụng.
fn main() { #[derive(Debug)] struct Person { name: String, age: Box<u8>, } let person = Person { name: String::from("Alice"), age: Box::new(20), }; // `name` được di chuyển ra khỏi `person`, nhưng `age` được tham chiếu let Person { name, ref age } = person; println!("Tuổi của người đó là {}", age); println!("Tên của người đó là {}", name); // Lỗi! không được mượn một phần giá trị đã di chuyển: `person` bị di chuyển một phần //println!("Cấu trúc person là {:?}", person); // không thể sử dụng `person` nhưng có thể sử dụng // `person.age` vì nó không được di chuyển println!("Tuổi của người đó từ cấu trúc person là {}", person.age); }
(Trong ví dụ này, chúng ta lưu trữ biến age
trên heap để minh họa về di
chuyển một phần: nếu xóa ref
trong đoạn mã trên, sẽ xảy ra lỗi vì quyền
sở hữu của person.age
đã được di chuyển đến biến age
. Nếu Person.age
được
lưu trữ trên stack, không cần phải sử dụng ref
vì định nghĩa của age
sẽ
sao chép dữ liệu từ person.age
mà không cần di chuyển nó.)
Xem thêm:
Borrowing
Trong đa phần các trường hợp, chúng ta sẽ có thể muốn truy cập dữ liệu mà không cần phải quan tâm nhiều đến quyền sở hữu của nó. Để làm được điều này, Rust cung cấp cho chúng ta một cơ chế gọi là borrowing
- Mượn trong tiếng Việt. Điều này có nghĩa là thay vì truyền vào giá trị (T
), ta chỉ cần truyền vào tham chiếu của nó (&T
).
Trình biên dịch sẽ đảm bảo chắc chắn (thông qua trình kiểm tra mượn - borrow checker
) rằng các tham chiếu sẽ luôn được trỏ đến các đối tượng hợp lệ. Điều này có nghĩa là, trong khi các tham chiếu đến một đối tượng còn tồn tại, thì đối tượng đó không thể bị xóa bỏ.
// Function này sẽ lấy ownership của Box và hủy nó fn eat_box_i32(boxed_i32: Box<i32>) { println!("Destroying box that contains {}", boxed_i32); } // Function này sẽ borrow một giá trị i32 fn borrow_i32(borrowed_i32: &i32) { println!("This int is: {}", borrowed_i32); } fn main() { // Tạo một Box i32, và một stacked i32 let boxed_i32 = Box::new(5_i32); let stacked_i32 = 6_i32; // Mượn nội dung của Box. Ownership sẽ không bị thay đổi, // nên nội dung có thể được mượn nhiều lần. borrow_i32(&boxed_i32); borrow_i32(&stacked_i32); { // Tham chiếu dữ liệu chứa bên trong Box let _ref_to_i32: &i32 = &boxed_i32; // Lỗi! // Không thể hủy `boxed_i32` do giá trị bên trong nó sẽ được mượn ở phía sau trong cùng 1 scope. eat_box_i32(boxed_i32); // FIXME ^ Comment dòng này để fix lỗi // Thử mượn `_ref_to_i32` khi giá trị bên trong nó đã bị hủy borrow_i32(_ref_to_i32); // `_ref_to_i32` đã đi ra khỏi scope và không được mượn nữa } // `boxed_i32` có thể từ bỏ quyền sở hữu - ownership đối với `eat_box` và có thể được tiêu hủy eat_box_i32(boxed_i32); }
Mutability
Kiểu dữ liệu có thể thay đổi có thể được mượn theo kiểu có thể thay đổi bằng cách sử dụng &mut T
. Điều này được gọi là mutable reference(tham chiếu có thể thay đổi) và nó cung cấp quyền truy cập đọc/ghi cho borrower. Ngược lại, &T
mượn dữ liệu thông qua immutable reference(tham chiếu không thể thay đổi) và borrower chỉ có thể đọc dữ liệu nhưng không thể sửa đổi nó:
#[allow(dead_code)] #[derive(Clone, Copy)] struct Book { // &'static str là một tham chiếu đến một chuỗi được cấp phát trong bộ nhớ chỉ đọc. author: &'static str, title: &'static str, year: u32, } // Hàm này lấy một tham chiếu tới một book fn borrow_book(book: &Book) { println!("I immutably borrowed {} - {} edition", book.title, book.year); } // Hàm này lấy tham chiếu tới một book có thể thay đổi và thay đổi `year` thành 2014 fn new_edition(book: &mut Book) { book.year = 2014; println!("I mutably borrowed {} - {} edition", book.title, book.year); } fn main() { // Tạo một đối tượng Book không thể thay đổi với tên `immutabook` let immutabook = Book { // string literals have type `&'static str` author: "Douglas Hofstadter", title: "Gödel, Escher, Bach", year: 1979, }; // Tạo một bản sao có thể thay đổi của `immutabook` và gọi nó là `mutabook`. let mut mutabook = immutabook; // Mượn một đối tượng không thể thay đổi sử dụng tham chiếu không thể thay đổi. borrow_book(&immutabook); // Mượn một đối tượng có thể thay đổi sử dụng tham thiếu không thể thay đổi. borrow_book(&mutabook); // Mượn một đối tượng có thể thay đổi sử dụng tham chiếu có thể thay đổi new_edition(&mut mutabook); // Lỗi! Không thể mượn một đối tượng không thể thay đổi bằng cách sử dụng một tham chiếu có thể thay đổi new_edition(&mut immutabook); // FIXME ^ Comment dòng phía trên }
See also:
Aliasing
Trong Rust, dữ liệu có thể mượn theo kiểu không thể thay đổi (immutable) bao nhiêu lần tùy thích, nhưng trong khi nó đang được mượn theo kiểu này, dữ liệu gốc không thể được mượn theo cách có thể thay đổi (mutable). Mặt khác, chỉ cho phép mượn dữ liệu có thể thay đổi tại một thời điểm. Dữ liệu gốc chỉ có thể được mượn lại chỉ sau khi tham chiếu có thể thay đổi được được sử dụng lần cuối trong mã.
struct Point { x: i32, y: i32, z: i32 } fn main() { let mut point = Point { x: 0, y: 0, z: 0 }; let borrowed_point = &point; let another_borrow = &point; // Dữ liệu có thể được truy cập thông qua các tham chiếu và chủ sở hữu gốc println!("Point has coordinates: ({}, {}, {})", borrowed_point.x, another_borrow.y, point.z); // Lỗi! Không thể mượn `point` theo kiểu có thể thay đổi bởi vì hiện tại nó đã được mượn theo kiểu không thể thay đổi // let mutable_borrow = &mut point; // TODO ^ Thử uncommenting dòng phía trên // Các giá trị được mượn được sử dụng lại ở đây println!("Point has coordinates: ({}, {}, {})", borrowed_point.x, another_borrow.y, point.z); // Các tham chiếu không thể thay đổi không được sử dụng trong phần còn lại của mã nên có thể mượn lại với một tham chiếu có thể thay đổi. let mutable_borrow = &mut point; // Thay đổi dữ liệu thông qua tham chiếu có thể thay đổi mutable_borrow.x = 5; mutable_borrow.y = 2; mutable_borrow.z = 1; // Lỗi! Không thể mượn `point` theo kiểu không thể thay đổi bởi vì // hiện tại nó đã được mượn theo kiểu có thể thay đổi // let y = &point.y; // TODO ^ Thử uncommenting dòng phía trên // Lỗi! Không thể in ra màn hình bởi vì `println!` yêu cầu một tham chiếu không thể thay đổi tới `point` // println!("Point Z coordinate is {}", point.z); // TODO ^ Thử uncommenting dòng phía trên // Ok! Tham chiếu có thể thay đổi có thể được truyền như là không thể thay đổi cho `println!` println!("Point has coordinates: ({}, {}, {})", mutable_borrow.x, mutable_borrow.y, mutable_borrow.z); // Tham chiếu có thể thay đổi không được sử dụng trong phần còn lại của mã // nên có thể mượn lại được let new_borrowed_point = &point; println!("Point now has coordinates: ({}, {}, {})", new_borrowed_point.x, new_borrowed_point.y, new_borrowed_point.z); }
The ref pattern
Khi thực hiện matching hoặc destructuring thông qua let
, từ khóa ref
có thể được sử dụng lấy tham chiếu của các trường trong một biến kiểu struct/tuple. Ví dụ dưới đây là một vài tình huống có thể hữu ích:
#[derive(Clone, Copy)] struct Point { x: i32, y: i32 } fn main() { let c = 'Q'; // Từ khóa `ref` ở vế trái dòng lệnh có ý nghĩa tương tự với // với dấu `&` ở vế phải. let ref ref_c1 = c; let ref_c2 = &c; println!("ref_c1 equals ref_c2: {}", *ref_c1 == *ref_c2); let point = Point { x: 0, y: 0 }; // `ref` cũng có thể được dùng trong trường hợp cần trích xuất giá trị từ biến kiểu struct let _copy_of_x = { // `ref_to_x` là một tham chiếu đến trường `x` bên trong biến `point`. let Point { x: ref ref_to_x, y: _ } = point; // Trả về bản sao của trường `x` trong biến `point`. *ref_to_x }; // Một bản sao có thể thay đổi giá trị của biến `point` let mut mutable_point = point; { // Từ khóa `ref` có thể đi cùng từ khóa `mut` để tạo ra một tham chiếu có thể thay đổi giá trị. let Point { x: _, y: ref mut mut_ref_to_y } = mutable_point; // Thay đổi trường `y` của `mutable_point` thông qua tham chiếu có thể thay đổi giá trị (vừa được tạo ở trên). *mut_ref_to_y = 1; } println!("point is ({}, {})", point.x, point.y); println!("mutable_point is ({}, {})", mutable_point.x, mutable_point.y); // Một biến tuple có thể thay đổi giá trị chứa một Pointer (điểm) let mut mutable_tuple = (Box::new(5u32), 3u32); { // Destructure biến `mutable_tuple` để thay đổi giá trị của `last`. let (_, ref mut last) = mutable_tuple; *last = 2u32; } println!("tuple is {:?}", mutable_tuple); }
Lifetimes
Lifetimes (Thời gian tồn tại) là một cấu trúc mà trình biên dịch (hay cụ thể hơn là trình kiểm tra mượn của nó) sử dụng để đảm bảo tất cả các borrow - mượn đều hợp lệ. Cụ thể hơn, thời gian tồn tại của một biến bắt đầu khi nó được tạo và kết thúc khi nó bị hủy. Mặc dù trong nhiều trường hợp, thời gian tồn tại (lifetimes) và scope thường được đề cập đến cùng nhau, tuy nhiên chúng không giống nhau.
Ví dụ, trong trường hợp chúng ta mượn một biến thông qua tham chiếu của nó (&
), thời hạn sử dụng của borrow
được xác định từ thời điểm nó được khai báo. Do đó, borrow
sẽ luôn có hiệu lực miễn là giá trị mà nó tham chiếu tới vẫn còn tồn tại. Tuy nhiên, scope
của chúng sẽ được xác định bởi cách mà tham chiếu được sử dụng.
Trong ví dụ dưới đây, chúng ta sẽ thấy được sự liên quan giữa lifetimes
và scope
, cũng như sự khác nhau của chúng:
// Lifetimes được chú thích bên dưới với các dòng biểu thị việc tạo // và hủy từng biến. // Giá trị `i` sẽ có thời gian tồn tại lâu nhất vì phạm vi của nó bao trùm toàn bộ // Đối với cả `borrow1` và `borrow2`, thời lượng của `borrow1` và `borrow2` là không liên quan // vì chúng không liên kết với nhau. fn main() { let i = 3; // Bắt đầu Lifetime của giá trị i. ─────────┐ // │ { // │ let borrow1 = &i; // `borrow1` lifetime bắt đầu. ─┐│ // ││ println!("borrow1: {}", borrow1); // ││ } // `borrow1` kết thúc. ─────────────────────────────┘│ // │ // │ { // │ let borrow2 = &i; // `borrow2` lifetime bắt đầu. ─┐│ // ││ println!("borrow2: {}", borrow2); // ││ } // `borrow2` kết thúc. ─────────────────────────────┘│ // │ } // Kết thúc Lifetime. ─────────────────────────────────┘
Cần lưu ý rằng các name
hoặc type
sẽ không có lifetimes
, điều này cũng sẽ đem lại những hạn chế về cách sử dụng lifetimes
, chúng ta sẽ thấy ở các phần tiếp theo.
Chú thích lifetime rõ ràng
Trình kiểm tra mượn sử dụng chú thích lifetime rõ ràng để xác định thời gian tham chiếu có hiệu lực. Trong những trường hợp mà việc xác định lifetime không được bỏ qua1, Rust yêu cầu chú thích rõ ràng để xác định thời gian tồn tại của tham chiếu. Cú pháp để chú thích lifetime rõ ràng sử dụng ký tự nháy đơn như sau:
foo<'a>
// `foo` có tham số lifetime `'a`
Tương tự như closures, việc sử dụng lifetime yêu cầu sử dụng generics. Ngoài ra, cú pháp này cho biết lifetime của foo
không thể vượt quá 'a
. Chú thích rõ ràng của một kiểu có dạng &'a T
với 'a
đã được giới thiệu trước đó.
Trong những trường hợp với nhiều lifetime, cú pháp tương tự như sau:
foo<'a, 'b>
// `foo` có tham số lifetime là `'a` và `'b`
Trong trường hợp này, lifetime của foo
không thể vượt quá 'a
hoặc 'b
.
Xem ví dụ sau để thấy cách sử dụng chú thích lifetime rõ ràng:
// `print_refs` nhận 2 tham chiếu đến biến kiểu `i32` // có các lifetime khác nhau là `'a` và `'b`. Hai lifetime này phải có lifetime // ít nhất là bằng với lifetime của hàm `print_refs`. fn print_refs<'a, 'b>(x: &'a i32, y: &'b i32) { println!("x is {} and y is {}", x, y); } // Một hàm không có đối số, nhưng có tham số lifetime `'a`. fn failed_borrow<'a>() { let _x = 12; // ERROR: `_x` không có lifetime đủ dài let y: &'a i32 = &_x; // Cố sử dụng lifetime `'a` như là một chú thích rõ ràng // bên trong hàm sẽ bị lỗi vì lifetime của `&_x` ngắn hơn // so với `y`. Một lifetime ngắn không thể được ép thành một lifetime dài hơn. } fn main() { // Tạo biến để có thể được mượn bên dưới. let (four, nine) = (4, 9); // Mượn (`&`) cả hai biến và truyền chúng vào hàm `print_refs`. print_refs(&four, &nine); // Bất cứ tham số đầu vào nào đuợc mượn phải có lifetime dài hơn thứ mượn nó (ở đây là `print_refs`) // Nói cách khác, lifetime của `four` và `nine` // phải dài hơn lifetime của `print_refs`. failed_borrow(); // Hàm `failed_borrow` không có tham chiếu nào để buộc `'a` phải // có lifetime dài hơn lifetime của hàm, nhưng `'a` vẫn có lifetime dài hơn. // Bởi vì lifetime không bị giới hạn, nó mặc định là `'static`. }
elision chú thích lifetime không rõ ràng và sự khác biệt.
Xem thêm:
Functions
Bỏ qua elision, mô tả hàm có chú thích lifetime có một vài ràng buộc:
- bất cứ tham chiếu nào phải có một chú thích lifetime.
- bất cứ tham chiếu nào được trả về phải có cùng một lifetime với một tham số đầu vào hoặc là
static
.
Ngoài ra, lưu ý rằng trả về tham chiếu mà không có tham số đầu vào là không được phép nếu nó sẽ dẫn đến việc trả về tham chiếu đến dữ liệu không hợp lệ. Ví dụ sau đây cho thấy một số hình thức hợp lệ đối với các hàm có lifetime:
// Một tham chiếu đầu vào với lifetime `'a` phải có lifetime // ít nhất là cho đến khi hàm kết thúc. fn print_one<'a>(x: &'a i32) { println!("`print_one`: x is {}", x); } // Các tham chiếu có thể sửa đổi (mutable) cũng có thể có lifetime. fn add_one<'a>(x: &'a mut i32) { *x += 1; } // Nhiều thành phần với các lifetime khác nhau. Trong trường hợp này, // nó sẽ không sao nếu cả hai có cùng một lifetime `'a`, nhưng // trong các trường hợp phức tạp hơn, các lifetime khác nhau có thể được yêu cầu. fn print_multi<'a, 'b>(x: &'a i32, y: &'b i32) { println!("`print_multi`: x is {}, y is {}", x, y); } // Trả về các tham chiếu được truyền vào là hợp lệ. // Tuy nhiên, nó phải được trả về với đúng lifetime được truyền vào. fn pass_x<'a, 'b>(x: &'a i32, _: &'b i32) -> &'a i32 { x } //fn invalid_output<'a>() -> &'a String { &String::from("foo") } // Hàm trên không hợp lệ: `'a` phải có lifetime lâu hơn hàm. // Ở đây, `&String::from("foo")` sẽ tạo ra một `String`, theo sau bởi một // tham chiếu. Sau đó dữ liệu sẽ bị hủy đi khi thoát khỏi phạm vi, để lại // một tham chiếu đến dữ liệu không hợp lệ để được trả về. fn main() { let x = 7; let y = 9; print_one(&x); print_multi(&x, &y); let z = pass_x(&x, &y); print_one(z); let mut t = 3; add_one(&mut t); print_one(&t); }
Xem thêm:
Phương thức
Các phương thức được chú thích tương tự như các hàm:
struct Owner(i32); impl Owner { // Chú thích về lifetime được sử dụng tương tự như đối với một hàm độc lập. fn add_one<'a>(&'a mut self) { self.0 += 1; } fn print<'a>(&'a self) { println!("`print`: {}", self.0); } } fn main() { let mut owner = Owner(18); owner.add_one(); owner.print(); }
Xem thêm:
Structs
Chú thích về thời gian tồn tại trong một structure cũng tương tự như với function:
// Một kiểu `Borrowed` chứa một tham chiếu tới `i32`. // Tham chiếu tới `i32` phải tồn tại lâu hơn `Borrowed`. #[derive(Debug)] struct Borrowed<'a>(&'a i32); // Tương tự, cả hai tham chiếu tới `i32` dưới đây phải tồn tại lâu hơn `NamedBorrowed`. #[derive(Debug)] struct NamedBorrowed<'a> { x: &'a i32, y: &'a i32, } // Một enum đồng thời có thể là `i32` hoặc tham chiếu tới `i32`. #[derive(Debug)] enum Either<'a> { Num(i32), Ref(&'a i32), } fn main() { let x = 18; let y = 15; let single = Borrowed(&x); let double = NamedBorrowed { x: &x, y: &y }; let reference = Either::Ref(&x); let number = Either::Num(y); println!("x is borrowed in {:?}", single); println!("x and y are borrowed in {:?}", double); println!("x is borrowed in {:?}", reference); println!("y is *not* borrowed in {:?}", number); }
Xem thêm:
Traits
Chú thích về thời gian tồn tại trong các phương thức trait về cơ bản cũng tương tự như trong function.
Lưu ý rằng impl
cũng có thể có thời gian tồn tại.
// Một struct với chú thích về thời gian tồn tại. #[derive(Debug)] struct Borrowed<'a> { x: &'a i32, } // Chú thích thời gian tồn tại cho impl. impl<'a> Default for Borrowed<'a> { fn default() -> Self { Self { x: &10, } } } fn main() { let b: Borrowed = Default::default(); println!("b is {:?}", b); }
Xem thêm:
Bounds
Tương tự như việc các kiểu generic có thể bị bounded (ràng buộc), lifetimes (bản thân nó cũng là generic) cũng
có thể sử dụng các bounds. Kí tự :
sẽ có một ý nghĩa hơi khác ở đây, còn kí tự +
thì vẫn có ý nghĩa như cũ.
Chú ý cách đọc của các cú pháp dưới đây như sau:
T: 'a
: Tất cả các tham chiếu trongT
phải tồn tại lâu hơn lifetime'a
1.T: Trait + 'a
: KiểuT
phải implement traitTrait
và tất cả các tham chiếu trongT
phải phải tồn tại lâu hơn lifetime'a
.
Ví dụ dưới đây sẽ áp dụng biểu diễn cách các cú pháp phía trên được sử dụng ở sau từ khoá where
use std::fmt::Debug; // Trait dùng để bound. #[derive(Debug)] struct Ref<'a, T: 'a>(&'a T); // Struct `Ref` bao gồm 1 tham chiếu đến kiểu generic `T` với lifetime // không xác định là `'a`. `T` bị bounded sao cho *các tham chiếu* trong `T` // phải tồn tại lâu hơn `'a`. Thêm vào đó, lifetime // của `Ref` không được vượt quá `'a`. // Hàm generic thực thi lệnh in sử dụng trait `Debug` fn print<T>(t: T) where T: Debug { println!("`print`: t is {:?}", t); } // Dưới đây là một tham chiếu đến `T`, trong đó `T` implements `Debug` // và tất cả *các tham chiếu* trong `T` phải tồn tại lâu hơn `'a`. Hơn nữa, // `'a` phải tồn tại lâu hơn lifetime của hàm. fn print_ref<'a, T>(t: &'a T) where T: Debug + 'a { println!("`print_ref`: t is {:?}", t); } fn main() { let x = 7; let ref_x = Ref(&x); print_ref(&ref_x); print(ref_x); }
Đọc thêm:
generics, bounds trong generics, and kết hợp nhiều bounds trong generics
Lời người dịch:
Đây là một ràng buộc về lifetime trong Rust, được áp dụng cho một kiểu T và một lifetime 'a.
Nó đảm bảo rằng nếu T chứa bất kỳ tham chiếu nào, thì tham chiếu đó phải tồn tại trong lifetime của 'a.
Ép kiểu (coercion)
Một lifetime dài hơn có thể được ép kiểu thành một lifetime ngắn hơn để nó có thể được dùng trong phạm vi mà thông thường nó không thể được sử dụng. Điều này được thực hiện thông qua quá trình ép kiểu tự suy luận bởi trình biên dịch Rust, và cũng có thể được khai báo thông qua sự khác biệt về lifetime:
// Ở đây, Rust sẽ tự suy ra một lifetime ngắn nhất có thể. // Hai tham chiếu này sau đó sẽ bị ép kiểu thành lifetime đó. fn multiply<'a>(first: &'a i32, second: &'a i32) -> i32 { first * second } // `<'a: 'b, 'b>` được đọc như sau: lifetime `'a` tối thiểu dài ngang lifetime `'b`. // Ở đây, chúng ta nhận vào một `&'a i32` và trả về a `&'b i32` như là một kết quả của sự ép kiểu. fn choose_first<'a: 'b, 'b>(first: &'a i32, _: &'b i32) -> &'b i32 { first } fn main() { let first = 2; // Lifetime dài hơn { let second = 3; // Lifetime ngắn hơn println!("The product is {}", multiply(&first, &second)); println!("{} is the first", choose_first(&first, &second)); }; }
Static
Trong Rust có một vài tên lifetime đặc biệt. Một trong số đó là 'static
.
Bạn có thể gặp nó trong hai tình huống:
// Một tham chiếu với 'static lifetime: let s: &'static str = "hello world"; // 'static được sử dụng làm phần của trait bound: fn generic<T>(x: T) where T: 'static {}
Cả hai trường hợp đều liên quan nhưng khác nhau một chút và đây chính là một nguồn cơn phổ biến gây ra vài sự nhẫm lẫn khi học Rust.
Dưới đây là một số ví dụ cho mỗi tình huống:
Reference lifetime
Dưới dạng lifetime của tham chiếu, 'static
chỉ ra rằng dữ liệu được trỏ tới bởi
tham chiếu tồn tại trong toàn bộ vòng đời của chương trình đang chạy.
Nó vẫn có thể được ép thành một lifetime ngắn hơn.
Có hai cách để tạo một biến với lifetime 'static
,
và cả hai đều được lưu trữ trong bộ nhớ chỉ đọc của tệp thực thi (binary):
- Khai báo một hằng số với từ khóa
static
. - Tạo một
string
có kiểu dữ liệu là:&'static str
.
Xem ví dụ sau để thấy cách sử dụng từng phương pháp:
// Tạo một hằng số với lifetime `'static`. static NUM: i32 = 18; //Trả về một tham chiếu tới NUM mà lifetime `'static` //của nó bị ép thành lifetime của đối số đầu vào. fn coerce_static<'a>(_: &'a i32) -> &'a i32 { &NUM } fn main() { { // Tạo một `chuỗi` văn bản và in nó ra: let static_string = "I'm in read-only memory"; println!("static_string: {}", static_string); //Khi `static_string` ra khỏi phạm vi, tham chiếu không thể được sử dụng nữa, //Nhưng dữ liệu vẫn tồn tại trong tệp thực thi (binary) của chương trình. } { // Tạo một số nguyên để sử dụng cho `coerce_static`: let lifetime_num = 9; // Ép NUM thành lifetime của `lifetime_num`: let coerced_static = coerce_static(&lifetime_num); println!("coerced_static: {}", coerced_static); } println!("NUM: {} stays accessible!", NUM); }
Trait bound
Khi dùng làm một ràng buộc trait, có nghĩa là kiểu dữ liệu không chứa bất kỳ tham chiếu non-static nào. Ví dụ, người nhận có thể giữ kiểu đó cho đến bất cứ khi nào họ muốn và nó sẽ luôn hợp lệ cho đến khi họ loại bỏ nó.
Quan trọng là bạn hiểu rằng điều này có nghĩa là bất kỳ dữ liệu nào được sở hữu
luôn luôn đáp ứng được ràng buộc lifetime 'static
, nhưng một tham chiếu đến
dữ liệu được sở hữu đó thì không đáp ứng điều này:
use std::fmt::Debug; fn print_it( input: impl Debug + 'static ) { println!( "'static value passed in is: {:?}", input ); } fn main() { //Biến i được sở hữu và không chứa bất kỳ tham chiếu nào, do đó nó là 'static: let i = 5; print_it(i); //oops, tham chiếu &i chỉ có lifetime được xác định bởi phạm vi của hàm main(), //nên nó không phải là 'static: print_it(&i); }
Trình biên dịch sẽ báo lỗi với bạn :
error[E0597]: `i` không tồn tại trong khoảng thời gian đủ lâu.
--> src/lib.rs:15:15
|
15 | print_it(&i);
| ---------^^--
| | |
| | giá trị được mượn không tồn tại đủ lâu
| đối số đòi hỏi `i` được mượn với lifetime `'static`
16 | }
| - `i` bị drop ở đây trong khi nó vẫn được mượn
Xem thêm:
Elision
Trường hợp các biến tồn tại suốt vòng đời (của một hàm) là rất phổ biến và vì thế trình kiểm tra mượn (borrow checker) cho phép bạn bỏ qua việc đánh dấu lifetime để tiết kiệm thời gian nhập và để dễ đọc hơn. Điều này được gọi là Elision. Cơ chế Elision tồn tại trong Rust bởi vì các trường hợp như thế này quá phổ biến.
Đoạn code dưới đây sễ đưa ra một vài ví dụ về cơ chế Elision. Để hiểu rõ hơn về cơ chế này, bạn có thể đọc tại đây:
// `elided_input` và `annotated_input` có mô tả (function signatures) giống nhau // vì lifetime của `elided_input` được trình biên dịch xác định: fn elided_input(x: &i32) { println!("`elided_input`: {}", x); } fn annotated_input<'a>(x: &'a i32) { println!("`annotated_input`: {}", x); } // Tương tự, `elided_pass` và `annotated_pass` cũng có những mô tả giống nhau // vì lifetime được ngầm định thêm vào `elided_pass`: fn elided_pass(x: &i32) -> &i32 { x } fn annotated_pass<'a>(x: &'a i32) -> &'a i32 { x } fn main() { let x = 3; elided_input(&x); annotated_input(&x); println!("`elided_pass`: {}", elided_pass(&x)); println!("`annotated_pass`: {}", annotated_pass(&x)); }
Xem thêm tại đây:
Traits
Trait
là tập hợp các phương thức được định nghĩa cho một loại không xác định: Self
. Chúng có thể truy cập các phương thức khác được khai báo trong cùng một trait.
Các trait có thể được triển khai cho bất kỳ loại dữ liệu nào. Trong ví dụ bên dưới, chúng tôi định nghĩa Animal
, là một nhóm các phương thức. Sau đó, trait
Animal
được triển khai cho kiểu dữ liệu Sheep
, cho phép sử dụng các phương thức của Animal
với dữ liệu kiểu Sheep
.
struct Sheep { naked: bool, name: &'static str } trait Animal { // Mô tả hàm liên kết; `Self` đề cập đến kiểu được triển khai. fn new(name: &'static str) -> Self; // Mô tả phương thức; chúng sẽ trả về một chuỗi. fn name(&self) -> &'static str; fn noise(&self) -> &'static str; // Các Traits có thể cung cấp các định nghĩa phương thức mặc định. fn talk(&self) { println!("{} says {}", self.name(), self.noise()); } } impl Sheep { fn is_naked(&self) -> bool { self.naked } fn shear(&mut self) { if self.is_naked() { // Các phương thức của kiểu được triển khai có thể sử dụng các phương thức của trait mà nó triển khai. println!("{} is already naked...", self.name()); } else { println!("{} gets a haircut!", self.name); self.naked = true; } } } // Triển khai trait `Animal` cho `Sheep`. impl Animal for Sheep { // `Self` là kiểu được triển khai, ở đây là `Sheep`. fn new(name: &'static str) -> Sheep { Sheep { name: name, naked: false } } fn name(&self) -> &'static str { self.name } fn noise(&self) -> &'static str { if self.is_naked() { "baaaaah?" } else { "baaaaah!" } } // Các phương thức trait mặc định có thể được ghi đè. fn talk(&self) { // Ví dụ, chúng ta có thể thêm một vài khoảng pause println!("{} pauses briefly... {}", self.name, self.noise()); } } fn main() { // Chú thích kiểu dữ liệu là cần thiết trong trường hợp này. let mut dolly: Sheep = Animal::new("Dolly"); // TODO ^ Hãy thử bỏ chú thích dolly.talk(); dolly.shear(); dolly.talk(); }
Derive
Trình biên dịch Rust có thể cung cấp các triển khai cơ bản cho một số trait thông qua #[derive]
attribute. Những trait này vẫn có thể được triển khai thủ công nếu cần một hành vi phức tạp hơn.
Dưới đây là một danh sách các trait có thể được dẫn xuất:
- Các trait so sánh:
Eq
,PartialEq
,Ord
,PartialOrd
. Clone
, để tạo ra kiểuT
từ&T
thông qua bản sao.Copy
, để cho phép kiểu có 'copy semantics' thay vì 'move semantics'.Hash
, để tính toán hàm băm từ&T
.Default
, để tạo một thể hiện trống của kiểu dữ liệu.Debug
, để định dạng một giá trị sử dụng trình định dạng{:?}
.
// `Centimeters`, một cấu trúc tuple có thể so sánh #[derive(PartialEq, PartialOrd)] struct Centimeters(f64); // `Inches`, một cấu trúc tuple có thể in ra #[derive(Debug)] struct Inches(i32); impl Inches { fn to_centimeters(&self) -> Centimeters { let &Inches(inches) = self; Centimeters(inches as f64 * 2.54) } } // `Seconds`, một cấu trúc tuple không có thuộc tính bổ sung struct Seconds(i32); fn main() { let _one_second = Seconds(1); // Error! `Seconds` không thể được in ra; nó không triển khai trait `Debug` //println!("One second looks like: {:?}", _one_second); // TODO ^ thử bỏ chú thích dòng trên // Error! `Seconds` không thể được so sánh; nó không triển khai trait `PartialEq` //let _this_is_true = (_one_second == _one_second); // TODO ^ thử bỏ chú thích dòng trên let foot = Inches(12); println!("One foot equals {:?}", foot); let meter = Centimeters(100.0); let cmp = if foot.to_centimeters() < meter { "smaller" } else { "bigger" }; println!("One foot is {} than one meter.", cmp); }
Xem thêm:
Trả về kiểu Traits với từ khóa dyn
Trình biên dịch của Rust cần biết cần bao nhiêu bộ nhớ để chứa kiểu dữ liệu trả về của mỗi hàm. Điều đó có nghĩa là mọi hàm mà bạn viết phải trả về một kiểu cố định. Không như những ngôn ngữ lập trình khác, nếu bạn có một trait ví dụ Animal
, bạn không thể viết một hàm mà trả về kiểu Animal
, vì những triển khai (implementation) khác nhau của nó sẽ cần kích thước bộ nhớ khác nhau.
Tuy nhiên, có một cách giải quyết khác. Thay vì phải trả về trực tiếp một đối tượng trait, các hàm của ta sẽ trả về một Box
chứa một vài Animal
. Box
chỉ là một tham chiếu đến một vài vùng nhớ trên heap. Bởi vì một tham chiếu có một kích thước tĩnh cố định biết trước và trình biên dịch có thể đảm bảo tham chiếu đó trỏ đến vùng nhớ trên heap của Animal
, ta có thể trả về một trait từ hàm của mình!
Rust sẽ cố gắng tường minh nhất có thể quá trình nó cấp phát bộ nhớ trên heap. Vì vậy nếu hàm của bạn trả về một con trỏ đến trait trên vùng nhớ theo cách này, bạn cần khai báo kiểu trả về với từ khóa dyn
, ví dụ: Box<dyn Animal>
.
struct Sheep {} struct Cow {} trait Animal { // Đặc trưng phương thức của đối tượng (instance) fn noise(&self) -> &'static str; } // Implement `Animal` cho struct `Sheep`. impl Animal for Sheep { fn noise(&self) -> &'static str { "baaaaah!" } } // Implement trait `Animal` cho struct `Cow`. impl Animal for Cow { fn noise(&self) -> &'static str { "moooooo!" } } // Trả về một vài struct mà đã implement Animal, nhưng ta sẽ không biết cụ thể struct nào tại thời điểm biên dịch (compile time). fn random_animal(random_number: f64) -> Box<dyn Animal> { if random_number < 0.5 { Box::new(Sheep {}) } else { Box::new(Cow {}) } } fn main() { let random_number = 0.234; let animal = random_animal(random_number); println!("You've randomly chosen an animal, and it says {}", animal.noise()); }
Operator Overloading
Trong Rust, rất nhiều toán tử có thể được nạp chồng(overloaded) thông qua các trait. Nghĩa là, một số các toán tử có thể được sử dụng để thực hiện các nhiệm vụ khác nhau dựa trên các đối số đầu vào của chúng. Điều này có thể thực hiện được bởi vì các toán tử là cú pháp(syntactic sugar) cho các lời gọi phương thức. Ví dụ, toán tử +
trong phép tính a + b
gọi phương thức add
(tương đương với a.add(b)
). Phương thức add
này là một phần của trait Add
. Do đó, toán tử +
có thể được sử dụng bởi bất kì loại dữ liệu nào đã triển khai trait Add
.
Một danh sách các trait, ví dụ như Add
, toán tử nạp chồng có thể được tìm thấy trong core::ops
.
use std::ops; struct Foo; struct Bar; #[derive(Debug)] struct FooBar; #[derive(Debug)] struct BarFoo; // Trait `std::ops::Add` được sử dụng để chỉ định chức năng của toán tử `+` // Ở đây, chúng ta tạo ra `Add<Bar>` - trait cho phép cộng với RHS của kiểu `Bar`. // Khối lệnh sau đây thực hiện phép tính: Foo + Bar = FooBar impl ops::Add<Bar> for Foo { type Output = FooBar; fn add(self, _rhs: Bar) -> FooBar { println!("> Foo.add(Bar) was called"); FooBar } } // Bằng cách đảo ngược các kiểu dữ liệu, chúng ta thực hiện việc cộng không giao hoán(non-commutative). // Ở đây, chúng ta tạo ra `Add<Foo>` - trait cho phép cộng với RHS của kiểu `Foo`. // Khối lệnh sau đây thực hiện phép tính: Bar + Foo = BarFoo impl ops::Add<Foo> for Bar { type Output = BarFoo; fn add(self, _rhs: Foo) -> BarFoo { println!("> Bar.add(Foo) was called"); BarFoo } } fn main() { println!("Foo + Bar = {:?}", Foo + Bar); println!("Bar + Foo = {:?}", Bar + Foo); }
Xem thêm
Drop
Trait Drop
chỉ có một phương thức: drop
, phương thức này sẽ được gọi một cách tự động
khi một đối tượng bị ra khỏi scope. Công dụng chính của trait Drop
là để giải phóng tài nguyên bộ nhớ
mà đối tượng implement nó đang chiếm dụng.
Box
, Vec
, String
, File
, và Process
là một vài ví dụ về các kiểu có
implement trait Drop
để giải phóng tài nguyên. Trait Drop
cũng có thể được
implement cho bất kì các kiểu dữ liệu tùy chỉnh nào.
Ví dụ sau đây thêm vào hàm drop
chức năng in ra console để thông báo
mỗi khi nó được gọi.
struct Droppable { name: &'static str, } // Implementation này của `drop` thêm chức năng in ra console. impl Drop for Droppable { fn drop(&mut self) { println!("> Dropping {}", self.name); } } fn main() { let _a = Droppable { name: "a" }; // khối A { let _b = Droppable { name: "b" }; // khối B { let _c = Droppable { name: "c" }; let _d = Droppable { name: "d" }; println!("Exiting block B"); } println!("Just exited block B"); println!("Exiting block A"); } println!("Just exited block A"); // Biến có thể bị drop một cách thủ công sử dụng hàm `drop` drop(_a); // TODO ^ Thử biến dòng này thành comment println!("end of the main function"); // `_a` *sẽ không* bị `drop` một lần nữa ở đây vì nó đã bị // drop (bằng cách thủ công) }
Iterators
Trait Iterator
được dùng để triển khai bộ lặp vào các tập hợp như là các mảng.
Trait chỉ yêu cầu một phương thức để xác định phần tử next
,
thứ có thể được định nghĩa thủ công trong đoạn impl
hoặc tự động
được định nghĩa (như trong các mảng và dải).
Là một sự tiện lợi trong nhiều tính huống, cấu trúc for
biến các tập hợp thành iterators bằng phương thức .into_iter()
.
struct Fibonacci { curr: u32, next: u32, } // Cài `Iterator` cho `Fibonacci`. // `Iterator` trait chỉ yêu cầu một phương thức để xác định phần tử `next`. impl Iterator for Fibonacci { // Chúng ta có thể chỉ đến loại này bằng cách dùng Self::Item type Item = u32; // Tại đây, chúng ta xác định thứ tự bằng cách dùng `.curr` và `.next`. // Kiểu trả về là `Option<T>`: // * Khi `Iterator` kết thúc, biến thể `None` được trả về. // * Ngược lại, giá trị tiếp theo sẽ được bọc trong biến thể `Some` và được trả về. // Chúng ta dùng Self::Item là kiểu trả về , vì thế chúng ta có thể thay đổi // kiểu mà không cần cập nhật phương thức. fn next(&mut self) -> Option<Self::Item> { let current = self.curr; self.curr = self.next; self.next = current + self.next; // Vì không có điểm kết thúc của chuỗi Fibonacci, `Iterator` // sẽ không bao giờ trả về biến thể `None`, và `Some` luôn được trả về. Some(current) } } // Trả về trình tạo chuỗi Fibonacci fn fibonacci() -> Fibonacci { Fibonacci { curr: 0, next: 1 } } fn main() { // `0..3` là một `Iterator` mà tạo ra: 0, 1, and 2. let mut sequence = 0..3; println!("Four consecutive `next` calls on 0..3"); println!("> {:?}", sequence.next()); println!("> {:?}", sequence.next()); println!("> {:?}", sequence.next()); println!("> {:?}", sequence.next()); // `for` chạy qua từng phần tử trong `Iterator` đến khi nó trả về `None`. // Mỗi giá trị `Some` được unwrapped được gán vào một biến (ở đây là `i`). println!("Iterate through 0..3 using `for`"); for i in 0..3 { println!("> {}", i); } // Phương thức `take(n)` giúp tạo `Iterator` rút gọn với chỉ `n` giá trị đầu tiên của nó. println!("The first four terms of the Fibonacci sequence are: "); for i in fibonacci().take(4) { println!("> {}", i); } // Phương thức `skip(n)` giúp tạo `Iterator` rút gọn bằng cách bỏ qua `n` giá trị đầu tiên của nó. println!("The next four terms of the Fibonacci sequence are: "); for i in fibonacci().skip(4).take(4) { println!("> {}", i); } let array = [1u32, 3, 3, 7]; // Phương thức `iter` tạo ra một `Iterator` từ một chuỗi/lát cắt (array/slice). println!("Iterate the following array {:?}", &array); for i in array.iter() { println!("> {}", i); } }
impl Trait
impl Trait
có thể được dùng trong hai vai trò:
- như là một kiểu đối số
- như là một kiểu trả về
Dùng như là một kiểu đối số
Nếu hàm của bạn có chung một đặc điểm nhưng bạn không quan tâm đến kiểu cụ thể, thì bạn có thể đơn giản hóa việc khai báo hàm bằng cách sử dụng impl Trait
làm kiểu đối số.
Ví dụ, hãy xem xét đoạn mã dưới đây:
fn parse_csv_document<R: std::io::BufRead>(src: R) -> std::io::Result<Vec<Vec<String>>> { src.lines() .map(|line| { // Với từng dòng trong file line.map(|line| { // Nếu dòng được đọc thành công, xử lý nó, nếu không thì trả về lỗi line.split(',') // Tách dòng thành những phần nhỏ hơn được chia tách bởi dấu phẩy .map(|entry| String::from(entry.trim())) // Xóa khoảng trắng trước và sau .collect() // Tổng hợp các chuỗi của dòng thành một Vec<String> }) }) .collect() // Tổng hợp các dòng thành một Vec<Vec<String>> }
parse_csv_document
là một hàm tổng quát, cho phép nó nhận bất kỳ kiểu gì có cài BufRead, như là BufReader<File>
hay [u8]
,
mà không quan trọng R
là kiểu gì, và R
chỉ được dùng để định nghia kiểu của src
, vì thế phương thức có thể được viết:
fn parse_csv_document(src: impl std::io::BufRead) -> std::io::Result<Vec<Vec<String>>> { src.lines() .map(|line| { // Với từng dòng trong file line.map(|line| { // Nếu dòng được đọc thành công, xử lý nó, nếu không thì trả về lỗi line.split(',') // Tách dòng thành những phần nhỏ hơn được chia tách bởi dấu phẩy .map(|entry| String::from(entry.trim())) // Xóa khoảng trắng trước và sau .collect() // Tổng hợp các chuỗi của dòng thành một Vec<String> }) }) .collect() // Tổng hợp các dòng thành một Vec<Vec<String>> }
Lưu ý rằng sử dụng impl Trait
như là một kiểu tham số có nghĩa là bạn không thể nêu rõ hình thức phương thức được sử dụng, ví dụ parse_csv_document::<std::io::Empty>(std::io::empty())
sẽ không hoạt động với ví dụ thứ hai.
Dùng như một kiểu trả về
Nếu hàm của bạn trả về một kiểu có triển khai MyTrait
, bạn có thể viết kiểu trả về
của nó là -> impl MyTrait
. Điều này giúp đơn giản hóa kiểu trả về của bạn rất nhiều!
use std::iter; use std::vec::IntoIter; // Hàm này kết hợp hai `Vec<i32>` và trả về một iterator. // Hãy xem kiểu trả về của nó phức tạp thế nào! fn combine_vecs_explicit_return_type( v: Vec<i32>, u: Vec<i32>, ) -> iter::Cycle<iter::Chain<IntoIter<i32>, IntoIter<i32>>> { v.into_iter().chain(u.into_iter()).cycle() } // Đây là một hàm tương tự, nhưng kiểu trả về của nó sử dụng `impl Trait`. // Hãy xem nó đơn giản hơn đến mức nào! fn combine_vecs( v: Vec<i32>, u: Vec<i32>, ) -> impl Iterator<Item=i32> { v.into_iter().chain(u.into_iter()).cycle() } fn main() { let v1 = vec![1, 2, 3]; let v2 = vec![4, 5]; let mut v3 = combine_vecs(v1, v2); assert_eq!(Some(1), v3.next()); assert_eq!(Some(2), v3.next()); assert_eq!(Some(3), v3.next()); assert_eq!(Some(4), v3.next()); assert_eq!(Some(5), v3.next()); println!("all done"); }
Quan trọng hơn nữa, một số kiểu trong Rust không thể ghi ra. Ví dụ, mỗi closure đều
có kiểu trả về của nó. Trước cú pháp impl Trait
, bạn phải cấp phát trên heap để
trả về một closure. Nhưng giờ bạn có thể làm điều đó một cách tĩnh, như thế này:
// Trả về một hàm cho cộng thêm `y` vào đầu vào fn make_adder_function(y: i32) -> impl Fn(i32) -> i32 { let closure = move |x: i32| { x + y }; closure } fn main() { let plus_one = make_adder_function(1); assert_eq!(plus_one(2), 3); }
Bạn có thể dùng impl Trait
để trả về một iterator sử dụng các closure map
hoặc
filter
! Điều này khiến việc dùng map
và filter
dễ hơn. Nhưng các kiểu closure
không có tên, bạn không thể ghi ra kiểu trả về một cách rõ ràng nếu hàm của bạn trả về một bộ lặp các closures
. Nhưng với impl Trait
bạn có thể làm điều này một cách dễ dàng:
fn double_positives<'a>(numbers: &'a Vec<i32>) -> impl Iterator<Item = i32> + 'a { numbers .iter() .filter(|x| x > &&0) .map(|x| x * 2) } fn main() { let singles = vec![-3, -2, 2, 3]; let doubles = double_positives(&singles); assert_eq!(doubles.collect::<Vec<i32>>(), vec![4, 6]); }
Clone
Khi làm việc với các tài nguyên, hành vi mặc định là chuyển chúng trong quá trình gán hoặc gọi hàm. Tuy nhiên, đôi khi chúng ta cũng cần phải tạo một bản sao của tài nguyên.
Trait Clone
giúp chúng ta thực hiện được điều này. Thông thường, chúng ta có thể sử dụng phương thức .clone()
được xác định bởi trait Clone
.
// Một cấu trúc Unit không có tài nguyên #[derive(Debug, Clone, Copy)] struct Unit; // Cấu trúc tuple với các tài nguyên triển khai trait `Clone` #[derive(Clone, Debug)] struct Pair(Box<i32>, Box<i32>); fn main() { // Khởi tạo `Unit` let unit = Unit; // Sao chép `Unit`, không có tài nguyên nào để di chuyển let copied_unit = unit; // Cả hai `Unit` đều có thể được sử dụng độc lập println!("original: {:?}", unit); println!("copy: {:?}", copied_unit); // Khởi tạo `Pair` let pair = Pair(Box::new(1), Box::new(2)); println!("original: {:?}", pair); // Chuyển `pair` vào `moved_pair`, đồng thời cũng di chuyển tài nguyên của nó let moved_pair = pair; println!("moved: {:?}", moved_pair); // Lỗi! `pair` đã mất tài nguyên của nó do bị move //println!("original: {:?}", pair); // TODO ^ Hãy thử chạy dòng lệnh trên // Sao chép `moved_pair` vào `cloned_pair` (bao gồm tài nguyên) let cloned_pair = moved_pair.clone(); // Xóa pair gốc bằng cách sử dụng std::mem::drop drop(moved_pair); // Lỗi! `moved_pair` đã bị loại bỏ //println!("copy: {:?}", moved_pair); // TODO ^ Hãy thử chạy dòng lệnh trên // Kết quả từ .clone() vẫn có thể được sử dụng! println!("clone: {:?}", cloned_pair); }
Supertraits
Rust không có "kế thừa", nhưng bạn có thể định nghĩa một trait như là một superset của một trait khác. Ví dụ:
trait Person { fn name(&self) -> String; } // Person là một supertrait của Student. // Thực hiện triển khai Student yêu cầu bạn cũng phải triển khai(impl) Person. trait Student: Person { fn university(&self) -> String; } trait Programmer { fn fav_language(&self) -> String; } // CompSciStudent (Sinh viên khoa học máy tính) là một subtrait của cả Programmer // và Student. Thực hiện triển khai CompSciStudent yêu cầu bạn triển khai(impl) cả 2 supertrait trên. trait CompSciStudent: Programmer + Student { fn git_username(&self) -> String; } fn comp_sci_student_greeting(student: &dyn CompSciStudent) -> String { format!( "My name is {} and I attend {}. My favorite language is {}. My Git username is {}", student.name(), student.university(), student.fav_language(), student.git_username() ) } fn main() {}
Xem thêm:
The Rust Programming Language chapter on supertraits
Phân biệt các traits được nạp chồng
Một kiểu có thể triển khai nhiều trait khác nhau. Vậy nếu hai trait đều yêu cầu cùng một tên phương thức thì sao? Ví dụ, nhiều trait có thể có một phương thức có tên get()
. Chúng có thể có kiểu trả về khác nhau!
Tin tốt là: vì mỗi triển khai trait có một khối impl
của riêng nó, nên bạn sẽ biết rõ rằng đang triển khai phương thức get
của trait nào.
Còn khi bạn muốn gọi các phương thức đó thì sao? Để phân biệt giữa chúng, chúng ta phải sử dụng cú pháp đầy đủ(Fully Qualified Syntax).
trait UsernameWidget { // Lấy ra tên của người được chọn fn get(&self) -> String; } trait AgeWidget { // Lấy ra tuổi được chọn fn get(&self) -> u8; } // Một dạng có cả UsernameWidget và AgeWidget struct Form { username: String, age: u8, } impl UsernameWidget for Form { fn get(&self) -> String { self.username.clone() } } impl AgeWidget for Form { fn get(&self) -> u8 { self.age } } fn main() { let form = Form { username: "rustacean".to_owned(), age: 28, }; // Nếu bạn bỏ comment dòng này, bạn sẽ nhận được một lỗi nói rằng // "nhiều phương thức `get` được tìm thấy". Vì thế, sau tất cả những gì đã làm ở trên, chúng ta đang có nhiều phương thức có tên `get`. // println!("{}", form.get()); let username = <Form as UsernameWidget>::get(&form); assert_eq!("rustacean".to_owned(), username); let age = <Form as AgeWidget>::get(&form); assert_eq!(28, age); }
Xem thêm:
The Rust Programming Language chapter on Fully Qualified syntax
macro_rules!
Rust cung cấp một hệ thống marco mạnh mẽ cho phép thực hiện lập trình meta. Như bạn
đã thấy ở chương trước, macro cũng giống một hàm, ngoại trừ việc tên của nó
kết thúc với dấu !
, nhưng thay vì tạo ra một lệnh gọi hàm, macro được
chèn vào trong mã nguồn và được biên dịch với phần còn lại của chương trình.
Tuy nhiên, khác với macro trong C và những ngôn ngữ khác, macro của Rust được chèn
vào cây cú pháp trừu tượng, thay vì tiền xử lý chuỗi, vì thế bạn không gặp
phải những lỗi về độ ưu tiên.
Macro đƯợc tạo bằng cách dùng macro macro_rules!
.
// Đây là một macro đơn giản tên là `say_hello`. macro_rules! say_hello { // `()` cho thấy macro này không cần bất kỳ tham số nào. () => { // Macro này sẽ chèn vào nội dung của khối lệnh này. println!("Hello!") }; } fn main() { // Đoạn mã này sẽ dẫn đến lệnh `println!("Hello")` say_hello!() }
Vậy tại sao marco lại hữu dụng?
-
Tránh lặp lại các mã tương tự nhau. Có nhiều trường hợp bạn có thể cần những chức năng tương tự nhau tại nhiều nơi nhưng dưới các dạng khác nhau. Thông thường, viết macro là cách hữu dụng nhất để tránh lặp mã nguồn. (Điều này sẽ được bàn thêm sau)
-
Ngôn ngữ miền chuyên biệt. Macro cho phép bạn định nghĩa một cú pháp đặc biệt cho một mục đích chuyên biệt. (Điều này sẽ được bàn thêm sau)
-
Các interface đa dạng. Đôi lúc bạn muốn định nghĩa một phương thức có số lượng tham số linh hoạt. Một ví dụ là
println!
có thể nhận bất kỳ số lượng tham số nào, tùy thuộc vào định dạng của chuỗi. (Điều này sẽ được bàn thêm sau)
Syntax
Trong các tiểu mục sau đây, chúng tôi sẽ trình bày cách để định nghĩa marcros trong Rust. Có ba ý tưởng cơ bản.
Định danh (designators)
Các đối số của một macro sẽ được đánh dấu bằng tiền tố dấu đô la $
và chú thích với kiểu bằng designator.
macro_rules! create_function { // Macro này nhận đối số với designator là `ident` và // tạo ra một hàm có tên là `$func_name` // Designator `ident` được dùng cho tên của biến hoặc là tên của hàm ($func_name:ident) => { fn $func_name() { // Macro `stringify!` chuyển đổi một biến với định danh là `ident` thành chuỗi string. println!("You called {:?}()", stringify!($func_name)); } }; } // Tạo ra một hàm tên là `foo` và `bar` với macro được định nghĩa ở trên. create_function!(foo); create_function!(bar); macro_rules! print_result { // Macro này sẽ nhận vào một biểu thức (expression) với kiểu là `expr` và sẽ in // biểu thức đó ra dưới dạng chuỗi string và kèm theo đó là kết quả của nó // Designator `expr` được dùng cho các biểu thức (expressions) ($expression:expr) => { // Macro `stringify!` sẽ chuyển biểu thức (expression) thành chuỗi string println!("{:?} = {:?}", stringify!($expression), $expression); }; } fn main() { foo(); bar(); print_result!(1u32 + 1); // Nhắc lại là các blocks cũng là các biểu thức (expressions) print_result!({ let x = 1u32; x * x + 2 * x - 1 }); }
Dưới đây là một vài Designator có sẵn:
block
expr
được dùng cho các biểu thức (expressions)ident
được dùng cho tên của biến hoặc hàmitem
literal
được dùng cho các hằng số literal (literal constants)pat
(pattern)path
stmt
(statement)tt
(token tree)ty
(type)vis
(visibility qualifier)
Nếu muốn coi một danh sách đầy đủ của các Designator, hãy xem ở đây Rust Reference
Overload
Macros có thể được nạp chồng để chấp nhận những kiểu kết hợp khác nhau của các đối số.
Khi đó, macro_rules!
hoạt động tương tự như là một khối match
:
// `test!` sẽ so sánh `$left` and `$right` // theo những cách khác nhau tùy thuộc vào cách bạn gọi nó: macro_rules! test { // Các đối số không nhất thiết phải được tách ra bằng dấu phẩy // Có thể sử dụng bất cứ mẫu nào cũng được ($left:expr; and $right:expr) => { println!("{:?} and {:?} is {:?}", stringify!($left), stringify!($right), $left && $right) }; // ^ mỗi nhánh phải được kết thúc bằng dấu chấm phẩy. ($left:expr; or $right:expr) => { println!("{:?} or {:?} is {:?}", stringify!($left), stringify!($right), $left || $right) }; } fn main() { test!(1i32 + 1 == 2i32; and 2i32 * 2 == 4i32); test!(true; or false); }
Repeat
Macros có thể sử dụng +
trong danh sách đối số để biểu thị rằng một đối số có thể lặp lại ít nhất một lần,
hoặc *
, để biểu thị rằng một đối số có thể lặp lại từ không cho đến nhiều lần.
Trong ví dụ sau đây, việc bao quanh bộ so khớp (matcher) bởi $(...),+
sẽ phù hợp với
một hoặc nhiều biểu thức, được cách nhau bởi dấu phẩy.
Lưu ý rằng dấu chấm phẩy (;) là tùy chọn cho trường hợp cuối cùng.
// `find_min!` sẽ tìm ra giá trị nhỏ nhất trong dãy đối số. macro_rules! find_min { // Trường hợp cơ bản: ($x:expr) => ($x); // `$x` theo sau bởi ít nhất một `$y,` ($x:expr, $($y:expr),+) => ( // Gọi `find_min!` trên các đối số còn lại `$y` std::cmp::min($x, find_min!($($y),+)) ) } fn main() { println!("{}", find_min!(1)); println!("{}", find_min!(1 + 2, 2)); println!("{}", find_min!(5, 2 * 3, 4)); }
DRY (Don't Repeat Yourself - Hạn chế lặp lại code)
Macros cho phép viết code ít lặp lại bằng cách lấy ra các phần chung của
function và/hoặc các bộ test. Đây là một ví dụ về việc triển khai và kiểm
thử các toán tử +=
, *=
và -=
trên Vec<T>
:
use std::ops::{Add, Mul, Sub}; macro_rules! assert_equal_len { // `tt` (token tree) designator được sử dụng cho toán tử và token. ($a:expr, $b:expr, $func:ident, $op:tt) => { assert!($a.len() == $b.len(), "{:?}: dimension mismatch: {:?} {:?} {:?}", stringify!($func), ($a.len(),), stringify!($op), ($b.len(),)); }; } macro_rules! op { ($func:ident, $bound:ident, $op:tt, $method:ident) => { fn $func<T: $bound<T, Output=T> + Copy>(xs: &mut Vec<T>, ys: &Vec<T>) { assert_equal_len!(xs, ys, $func, $op); for (x, y) in xs.iter_mut().zip(ys.iter()) { *x = $bound::$method(*x, *y); // *x = x.$method(*y); } } }; } // Triển khai các function `add_assign`, `mul_assign`, và `sub_assign`. op!(add_assign, Add, +=, add); op!(mul_assign, Mul, *=, mul); op!(sub_assign, Sub, -=, sub); mod test { use std::iter; macro_rules! test { ($func:ident, $x:expr, $y:expr, $z:expr) => { #[test] fn $func() { for size in 0usize..10 { let mut x: Vec<_> = iter::repeat($x).take(size).collect(); let y: Vec<_> = iter::repeat($y).take(size).collect(); let z: Vec<_> = iter::repeat($z).take(size).collect(); super::$func(&mut x, &y); assert_eq!(x, z); } } }; } // Kiểm thử `add_assign`, `mul_assign`, và `sub_assign`. test!(add_assign, 1u32, 2u32, 3u32); test!(mul_assign, 2u32, 3u32, 6u32); test!(sub_assign, 3u32, 2u32, 1u32); }
$ rustc --test dry.rs && ./dry
running 3 tests
test test::mul_assign ... ok
test test::add_assign ... ok
test test::sub_assign ... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured
Domain Specific Languages (DSLs)
Một DSL là một "ngôn ngữ mini" được nhúng vào một macro. Chúng hoàn toàn là hợp lệ trong Rust bởi vì hệ thống macro mở rộng thành cấu trúc Rust bình thường, nhưng chúng trông giống một "ngôn ngữ nhỏ". Điều này cho phép bạn xác định cú pháp ngắn gọn hoặc trực quan cho một số chức năng đặc biệt (trong giới hạn).
Giả sử khi chúng ta muốn tạo ra một API dùng để tính toán. Chúng ta muốn cung cấp một phép toán và kết quả sẽ được in ra console.
macro_rules! calculate { (eval $e:expr) => { { let val: usize = $e; // Băt buộc kiểu dữ liệu phải là số nguyên println!("{} = {}", stringify!{$e}, val); } }; } fn main() { calculate! { eval 1 + 2 // `eval` không phải là một từ khóa của Rust! } calculate! { eval (1 + 2) * (3 / 4) } }
Kết quả:
1 + 2 = 3
(1 + 2) * (3 / 4) = 0
Đây là một ví dụ rất đơn giản, nhưng nhiều interface phức tạp hơn đang được phát
triển như là lazy_static
hoặc
clap
.
Ngoài ra, cần lưu ý hai cặp dấu ngoặc nhọn trong macro. Cặp bên ngoài là
một phần của cú pháp của macro_rules!
, bên cạnh ()
hoặc []
.
Variadic Interfaces
Một variadic interface có số lượng đối số tùy ý. Ví dụ println!
có thể nhận số lượng đối số tùy ý, như được xác định bởi định dạng string.
Chúng ta có thể mở rộng macro calculate!
từ phần trước thành variadic.
macro_rules! calculate { // pattern cho một `eval` (eval $e:expr) => { { let val: usize = $e; // Buộc các kiểu phải là số nguyên println!("{} = {}", stringify!{$e}, val); } }; // Phân rã nhiều `eval` bằng cách đệ quy. (eval $e:expr, $(eval $es:expr),+) => {{ calculate! { eval $e } calculate! { $(eval $es),+ } }}; } fn main() { calculate! { // Nhìn ma! Variadic `calculate!`! eval 1 + 2, eval 3 + 4, eval (2 * 3) + 1 } }
Output:
1 + 2 = 3
3 + 4 = 7
(2 * 3) + 1 = 7
Error handling
Xử lý lỗi là quá trình xử lý các khả năng gây ra thất bại của một chương trình. Ví dụ, nếu không thể đọc được một tệp tin và sau đó tiếp tục sử dụng lỗi đó làm đầu vào, rõ ràng sẽ gây ra vấn đề. Phát hiện và quản lý rõ ràng các lỗi đó giúp bảo vệ chương trình từ những mối nguy hiểm khác.
Có nhiều cách khác nhau để xử lý lỗi trong Rust, được mô tả trong các mục con sau đây. Tất cả chúng đều có ít nhiều sự khác biệt và các trường hợp sử dụng khác nhau. Như một quy tắc chung:
Việc sử dụng panic
rõ ràng chỉ hữu ích cho việc kiểm thử(test) và xử lý các lỗi không thể khắc phục được. Trong quá trình tạo nguyên mẫu (prototype) thì nó có thể rất hữu ích, ví dụ như khi xử lý các hàm chưa được triển khai, nhưng trong các trường hợp này thì unimplemented
cung cấp thông tin mô tả rõ ràng hơn. Trong các bài kiểm thử, panic
là một cách hợp lý để thể hiện sự thất bại rõ ràng.
Kiểu Option
được sử dụng khi giá trị là tùy chọn hay trong trường hợp thiếu giá trị thì có thể không phải là lỗi. Ví dụ như thư mục gốc - /
và C:
không có một thư mục cha nào. Khi xử lý với kiểu Option
, unwrap
là hợp lý trong quá trình tạo mẫu và các trường hợp mà chắc chắn sẽ có một giá trị hợp lệ. Tuy nhiên expect
sẽ hữu ích hơn vì nó cho phép bạn chỉ định một thông báo lỗi trong trường hợp có lỗi xảy ra.
Khi có khả năng xảy ra lỗi và bạn muốn người gọi phải xử lý vấn đề, hãy sử dụng Result
. Bạn cũng có thể sử dụng unwrap
và expect
(tuy nhiên đừng làm điều này trừ khi đó là một bài kiểm thử hoặc một nguyên mẫu nhanh(quick prototype)).
Để biết thêm thông tin chi tiết về xử lý lỗi, xin tham khảo phần xử lý lỗi trong official book.
panic
Panic
chính là cơ chế xử lí lỗi đơn giản nhất mà ta sắp đề cập tới.
Nó in ra thông điệp lỗi, huỷ bỏ các hoạt động đang thực thi, giải phóng stack và thường sẽ
thoát chương trình.
Bây giờ chúng ta sẽ gọi hàm panic
dựa trên câu điều kiện lỗi:
fn drink(beverage: &str) { // You shouldn't drink too much sugary beverages. if beverage == "lemonade" { panic!("AAAaaaaa!!!!"); } println!("Some refreshing {} is all I need.", beverage); } fn main() { drink("water"); drink("lemonade"); }
abort
và unwind
Phần trước đã mô tả cơ chế xử lí lỗi panic
. Các thành phần code khác nhau có thể được biên dịch một cách có điều kiện dựa trên các cài đặt panic
. Các giá trị có thể là unwind
và abort
.
Dựa trên ví dụ về nước chanh trước đó, chúng ta sử dụng các panic strategy để thực hiện các dòng code khác nhau.
fn drink(beverage: &str) { // Bạn không nên uống quá nhiều đồ uống có đường. if beverage == "lemonade" { if cfg!(panic="abort"){ println!("This is not your party. Run!!!!");} else{ println!("Spit it out!!!!");} } else{ println!("Some refreshing {} is all I need.", beverage); } } fn main() { drink("water"); drink("lemonade"); }
Đây là một ví dụ khác viết lại function drink()
và sử dụng từ khóa unwind
.
#[cfg(panic = "unwind")] fn ah(){ println!("Spit it out!!!!");} #[cfg(not(panic="unwind"))] fn ah(){ println!("This is not your party. Run!!!!");} fn drink(beverage: &str){ if beverage == "lemonade"{ ah();} else{println!("Some refreshing {} is all I need.", beverage);} } fn main() { drink("water"); drink("lemonade"); }
Panic strategy có thể được cài đặt từ command line.
rustc lemonade.rs -C panic=abort
Option & unwrap
Trong ví dụ trước, ta thấy rằng ta có thể tạo ra lỗi chương trình theo ý muốn.
Ta đã cho chương trình panic
nếu ta uống nước chanh có đường.
Nhưng nếu ta mong đợi một loại đồ uống nhưng lại không nhận được gì thì sao?
Trường hợp đó cũng tệ không kém, vì vậy nó cần phải được xử lý!
Chúng ta có thể kiểm tra điều này với empty string (""
) giống như ta đã làm với nước chanh.
Vì ta đang dùng Rust, hãy để compiler (trình biên dịch) chỉ ra các trường hợp không có đồ uống.
Một enum
tên là Option<T>
trong thư viện std
được sử dụng trong trường hợp có thể không có phần tử kiểu T.
Nó có dạng một trong hai "tuỳ chọn":
Some(T)
: Một phần tử của kiểuT
được tìm thấyNone
: Không tìm thấy phần tử nào.
Các trường hợp này có thể được xử lý một cách rõ ràng thông qua match
hoặc một cách ngầm định thông qua
unwrap
. Xử lý ngầm định sẽ trả về phần tử bên trong hoặc panic
.
Lưu ý rằng có thể tùy chỉnh panic
bằng cách sử dụng expect,
nhưng xử lý ngầm định (unwrap
) sẽ trả lại cho ta một kết quả ít có ý nghĩa hơn so với việc xử lý rõ ràng.
Trong ví dụ sau đây, việc xử lý rõ ràng giúp ta kiểm soát kết quả tốt hơn,
trong khi vẫn giữ tùy chọn panic
nếu cần.
// Người lớn (adult) thì cái gì cũng uống được. // Mọi loại đồ uống đều được xử lý rõ ràng bằng cách sử dụng `match`. fn give_adult(drink: Option<&str>) { // Mỗi đồ uống sẽ tương ứng với một hành động cụ thể. match drink { Some("lemonade") => println!("Yuck! Too sugary."), Some(inner) => println!("{}? How nice.", inner), None => println!("No drink? Oh well."), } } // Những người còn lại (không phải người lớn) sẽ `panic` khi uống một loại đồ uống có đường. // Mọi loại đồ uống đều được xử lý ngầm định bằng cách sử dụng `unwrap`. fn drink(drink: Option<&str>) { // `unwrap` trả lại `panic` khi nó nhận được `None`. let inside = drink.unwrap(); if inside == "lemonade" { panic!("AAAaaaaa!!!!"); } println!("I love {}s!!!!!", inside); } fn main() { let water = Some("water"); let lemonade = Some("lemonade"); let void = None; give_adult(water); give_adult(lemonade); give_adult(void); let coffee = Some("coffee"); let nothing = None; drink(coffee); drink(nothing); }
Unpacking options with ?
Bạn có thể unpack Option
bằng cách sử dụng câu lệnh match
, nhưng thường thì sử dụng toán tử ?
sẽ dễ dàng hơn. Nếu x
là một Option
, thì khi đánh giá x?
sẽ trả về giá trị bên trong nếu x
là Some
, ngược lại nó sẽ kết thúc các hàm đang được thực thi và trả về None
.
fn next_birthday(current_age: Option<u8>) -> Option<String> { // Nếu `current_age` là `None`, hàm này sẽ trả về `None`. // Nếu `current_age` là `Some`, giá trị `u8` ở trong sẽ được gán cho `next_age` let next_age: u8 = current_age? + 1; Some(format!("Next year I will be {}", next_age)) }
Bạn có thể nối tiếp nhiều ?
lại với nhau để làm cho mã của bạn trở nên dễ đọc hơn.
struct Person { job: Option<Job>, } #[derive(Clone, Copy)] struct Job { phone_number: Option<PhoneNumber>, } #[derive(Clone, Copy)] struct PhoneNumber { area_code: Option<u8>, number: u32, } impl Person { // Trả về mã vùng của số điện thoại công việc của người này, nếu nó tồn tại. fn work_phone_area_code(&self) -> Option<u8> { // Việc này sẽ cần nhiều câu lệnh `match` lồng nhau nếu không sử dụng toán tử `?`. // Bạn sẽ cần viết nhiều mã hơn - hãy thử viết lại nó và xem cái nào dễ hơn. self.job?.phone_number?.area_code } } fn main() { let p = Person { job: Some(Job { phone_number: Some(PhoneNumber { area_code: Some(61), number: 439222222, }), }), }; assert_eq!(p.work_phone_area_code(), Some(61)); }
Combinators: map
match
là một phương thức xử lý hợp lệ của Option
. Tuy nhiên, bạn có thể cảm thấy chán khi sử dụng chúng nhiều, đặc biệt là với các hoạt động chỉ đánh giá cho một đầu vào. Trong những trường hợp này, combinators có thể được sử dụng để quản lý luồng điều khiển một cách linh hoạt.
Option
có sẵn một phương thức gọi là map()
, một combinator cho việc đơn giản hóa việc ánh xạ Some -> Some
và None -> None
. Nhiều lần gọi map()
có thể được nối với nhau để có tăng thêm tính linh hoạt.
Trong ví dụ sau, process()
thay thế tất cả các hàm trước nó trong khi vẫn giữ nguyên tính gọn gàng.
#![allow(dead_code)] #[derive(Debug)] enum Food { Apple, Carrot, Potato } #[derive(Debug)] struct Peeled(Food); #[derive(Debug)] struct Chopped(Food); #[derive(Debug)] struct Cooked(Food); // Bóc vỏ đồ ăn. Nếu không có thì trả về `None`. // Ngược lại, trả về đồ ăn đã được bóc vỏ. fn peel(food: Option<Food>) -> Option<Peeled> { match food { Some(food) => Some(Peeled(food)), None => None, } } // Cắt đồ ăn. Nếu không có thì trả về `None`. // Ngược lại, trả về đồ ăn đã được cắt. fn chop(peeled: Option<Peeled>) -> Option<Chopped> { match peeled { Some(Peeled(food)) => Some(Chopped(food)), None => None, } } // Nấu đồ ăn. Ở đây, chúng ta dùng `map()` thay vì `match` cho các trường hợp xử lý. fn cook(chopped: Option<Chopped>) -> Option<Cooked> { chopped.map(|Chopped(food)| Cooked(food)) } // Một hàm để bóc vỏ, cắt và nấu đồ ăn cùng một lúc. // Chúng ta nối nhiều lần sử dụng `map()` để đơn giản hóa code. fn process(food: Option<Food>) -> Option<Cooked> { food.map(|f| Peeled(f)) .map(|Peeled(f)| Chopped(f)) .map(|Chopped(f)| Cooked(f)) } // Kiểm tra xem liệu có đồ ăn hay không trước khi thử ăn nó! fn eat(food: Option<Cooked>) { match food { Some(food) => println!("Mmm. I love {:?}", food), None => println!("Oh no! It wasn't edible."), } } fn main() { let apple = Some(Food::Apple); let carrot = Some(Food::Carrot); let potato = None; let cooked_apple = cook(chop(peel(apple))); let cooked_carrot = cook(chop(peel(carrot))); // Bây giờ, hãy thử hàm `process()` đơn giản hơn. // Let's try the simpler looking `process()` now. let cooked_potato = process(potato); eat(cooked_apple); eat(cooked_carrot); eat(cooked_potato); }
Xem thêm:
closures, Option
, Option::map()
Combinators: and_then
map()
được mô tả là một cách có thể xâu chuỗi để đơn giản hóa các câu lệnh match
. Tuy nhiên, việc sử dụng map()
trên một hàm trả về là Option<T>
sẽ dẫn đến kết quả là Option<Option<T>>
lồng nhau. Chuỗi nhiều các hàm gọi với nhau có thể trở nên khó hiểu. Đó là lúc một bộ kết hợp khác được gọi là and_then()
, được biết đến trong một số ngôn ngữ là flatmap xuất hiện.
and_then()
gọi đầu vào hàm của nó với giá trị được bọc(wrap) và trả về kết quả. Nếu Option
là None
, thì thay vào đó, nó sẽ trả về None
.
Trong ví dụ sau, cookable_v2()
trả về một kết quả là Option<Food>
. Sử dụng map()
thay vì and_then()
sẽ đưa ra Option<Option<Food>>
, đây là loại không hợp lệ cho eat()
.
#![allow(dead_code)] #[derive(Debug)] enum Food { CordonBleu, Steak, Sushi } #[derive(Debug)] enum Day { Monday, Tuesday, Wednesday } // Chúng tôi không có nguyên liệu để làm Sushi. fn have_ingredients(food: Food) -> Option<Food> { match food { Food::Sushi => None, _ => Some(food), } } // Chúng tôi có công thức cho mọi thứ trừ Cordon Bleu. fn have_recipe(food: Food) -> Option<Food> { match food { Food::CordonBleu => None, _ => Some(food), } } // Để làm một món ăn, chúng ta cần cả công thức và nguyên liệu. // Chúng ta có thể biểu diễn logic bằng một chuỗi matches: fn cookable_v1(food: Food) -> Option<Food> { match have_recipe(food) { None => None, Some(food) => have_ingredients(food), } } // Điều này có thể được viết lại một cách tiện lợi hơn với `and_then()`: fn cookable_v2(food: Food) -> Option<Food> { have_recipe(food).and_then(have_ingredients) } fn eat(food: Food, day: Day) { match cookable_v2(food) { Some(food) => println!("Yay! On {:?} we get to eat {:?}.", day, food), None => println!("Oh no. We don't get to eat on {:?}?", day), } } fn main() { let (cordon_bleu, steak, sushi) = (Food::CordonBleu, Food::Steak, Food::Sushi); eat(cordon_bleu, Day::Monday); eat(steak, Day::Tuesday); eat(sushi, Day::Wednesday); }
See also:
closures, Option
, and Option::and_then()
Result
Result
là phiên bản nâng cấp của kiểu Option
, mô tả lỗi có thể xảy ra thay vì chỉ mô tả việc kết quả có thể có hoặc không có.
Đúng với nhận định trên, Result<T, E>
trong Rust có thể có một trong hai kết quả:
Ok(T)
: Một thực thểT
được tìm thấyErr(E)
: Một lỗi được tìm thấy làE
Trong Rust, quy ước rằng kết quả mong đợi của một hàm là Ok
, trong khi kết quả không mong đợi là Err
.
Tương tự Option
, Result
đi kèm với rất nhiều phương thức. Ví dụ như unwrap()
,
sẽ trả về thực thể T
hoặc panic
. Tùy vấn đề cần xử lí mà ta kết hợp cả Result
và Option
.
Khi dùng Rust, bạn sẽ thường gặp các phương thức trả về kiểu dữ liệu Result
,
như là phương thức parse()
. Không phải lúc nào cũng có thể chuyển đổi
một chuỗi kí tự thành một kiểu dữ liệu khác nên parse()
sẽ trả về một
kiểu dữ liệu Result
cho biết có thể lỗi sẽ xảy ra.
Hãy xem xét những gì sẽ xảy ra khi chúng ta chuyển đổi thành công và không thành công một chuỗi bằng phương thức parse()
:
fn multiply(first_number_str: &str, second_number_str: &str) -> i32 { // Let's try using `unwrap()` to get the number out. Will it bite us? let first_number = first_number_str.parse::<i32>().unwrap(); let second_number = second_number_str.parse::<i32>().unwrap(); first_number * second_number } fn main() { let twenty = multiply("10", "2"); println!("double is {}", twenty); let tt = multiply("t", "2"); println!("double is {}", tt); }
Trong trường hợp không thành công, phương thức parse()
sẽ gây ra lỗi panic
. Ngoài ra, lỗi panic
cũng sẽ kết thúc chương trình và cung cấp một thông báo lỗi không mong muốn.
Để cải thiện chất lượng thông báo lỗi, chúng ta nên cụ thể hóa hơn về kiểu giá trị trả về và xử lí lỗi kĩ càng hơn
Using Result
in main
Kiểu dữ liệu Result
cũng có thể là kiểu dữ liệu trả về của hàm main
nếu được chỉ định rõ ràng. Thông thường, hàm main
sẽ có dạng:
fn main() { println!("Hello World!"); }
Tuy nhiên hàm main
cũng trả về kiểu Result
. Nếu một lỗi xảy ra trong hàm main
,
nó sẽ trả về mã lỗi và in ra một bản trình bày về lỗi (sử dụng trait Debug
).
Ví dụ sau đây là một trường hợp minh chứng và đề cập đến các khía cạnh được
nói qua ở [mục này].
use std::num::ParseIntError; fn main() -> Result<(), ParseIntError> { let number_str = "10"; let number = match number_str.parse::<i32>() { Ok(number) => number, Err(e) => return Err(e), }; println!("{}", number); Ok(()) }
map
for Result
Khi xảy ra lỗi trong hàm multiply
của ví dụ trước, sử dụng panic không tạo ra mã nguồn mạnh mẽ.
Thông thường, chúng ta muốn trả lại lỗi cho người gọi hàm để người gọi hàm quyết định cách phản hồi với lỗi đó.
Đầu tiên, chúng ta cần phải biết loại lỗi mà chúng ta đang gặp phải. Để xác định kiểu Err,
chúng ta tìm kiểu lỗi trong parse()
, Được thực hiện bằng cách triển khai trait
FromStr
cho i32
. Kết quả là, kiểu Err
được xác định là kiểu ParseIntError
.
Trong ví dụ dưới đây, câu lệnh match
trực tiếp dẫn đến mã nguồn tổng thể phức tạp hơn.
use std::num::ParseIntError; // Với kiểu trả về được viết lại, chúng ta sử dụng phân tích mẫu mà không có `unwrap()`. fn multiply(first_number_str: &str, second_number_str: &str) -> Result<i32, ParseIntError> { match first_number_str.parse::<i32>() { Ok(first_number) => { match second_number_str.parse::<i32>() { Ok(second_number) => { Ok(first_number * second_number) }, Err(e) => Err(e), } }, Err(e) => Err(e), } } fn print(result: Result<i32, ParseIntError>) { match result { Ok(n) => println!("n is {}", n), Err(e) => println!("Error: {}", e), } } fn main() { // Điều này vẫn cho phép trả về một câu trả lời hợp lý. let twenty = multiply("10", "2"); print(twenty); // Bây giờ đoạn mã sau đây cung cấp một thông báo lỗi hữu ích hơn nhiều. let tt = multiply("t", "2"); print(tt); }
May mắn thay, các phương thức Option
's map
, and_then
và nhiều phương thức khác cũng được cài đặt cho Result
.
Result
chứa một danh sách đầy đủ.
use std::num::ParseIntError; // Giống với `Option`, chúng ta có thể sử dụng các phương thức kết hợp như `map()`. // Hàm này tương tự như hàm trên và có chức năng: // Nhân nếu cả hai giá trị có thể được phân tích từ chuỗi, nếu không truyền lỗi cho phía gọi hàm xử lý. fn multiply(first_number_str: &str, second_number_str: &str) -> Result<i32, ParseIntError> { first_number_str.parse::<i32>().and_then(|first_number| { second_number_str.parse::<i32>().map(|second_number| first_number * second_number) }) } fn print(result: Result<i32, ParseIntError>) { match result { Ok(n) => println!("n is {}", n), Err(e) => println!("Error: {}", e), } } fn main() { // Điều này vẫn cho phép trả về một câu trả lời hợp lý. let twenty = multiply("10", "2"); print(twenty); // Bây giờ đoạn mã sau đây cung cấp một thông báo lỗi hữu ích hơn nhiều. let tt = multiply("t", "2"); print(tt); }
aliases for Result
Khi chúng ta muốn tái sử dụng một kiểu Result
cụ thể nhiều lần thì làm như thế nào?
Hãy nhớ rằng Rust cho phép chúng ta tạo aliases.
Một cách tiện lợi, chúng ta có thể định nghĩa một bí danh cho kiểu Result cụ thể đó.
Ở mức độ module, việc tạo bí danh có thể rất hữu ích. Các lỗi được tìm thấy trong một module cụ thể thường có cùng kiểu Err
,
do đó một bí danh duy nhất có thể ngắn gọn định nghĩa tất cả các Result
liên quan.
Việc này rất hữu ích đến mức thư viện std
cung cấp sẵn một bí danh cho việc này: io::Result
!
Đây là một ví dụ nhanh để minh họa cú pháp:
use std::num::ParseIntError; // Định nghĩa một bí danh chung cho `Result` với kiểu lỗi `ParseIntError`. type AliasedResult<T> = Result<T, ParseIntError>; // Sử dụng bí danh trên để tham chiếu đến kiểu `Result` cụ thể của chúng ta. fn multiply(first_number_str: &str, second_number_str: &str) -> AliasedResult<i32> { first_number_str.parse::<i32>().and_then(|first_number| { second_number_str.parse::<i32>().map(|second_number| first_number * second_number) }) } // Ở đây, bí danh lại cho phép chúng ta tiết kiệm không gian. fn print(result: AliasedResult<i32>) { match result { Ok(n) => println!("n is {}", n), Err(e) => println!("Error: {}", e), } } fn main() { print(multiply("10", "2")); print(multiply("t", "2")); }
See also:
Kĩ thuật early returns
Trong các ví dụ trước, bạn đã được giới thiệu về cách xử lý lỗi một cách tường minh nhờ vào việc sử dụng các combinator.
Ngoài cách trên, bạn có thể phân tích và xử lý lỗi theo từng trường hợp bằng cách sử dụng câu lệnh match
và kĩ thuật early returns
Nhờ đó, bạn có thể dừng thực thi hàm đang chạy và trả về lỗi nếu có. Đối với một số người, kiểu code này có thể dễ hiểu và dễ viết hơn. Dưới đây là một phiên bản khác của ví dụ trước đó được viết lại sử dụng kĩ thuật early returns:
use std::num::ParseIntError; fn multiply(first_number_str: &str, second_number_str: &str) -> Result<i32, ParseIntError> { let first_number = match first_number_str.parse::<i32>() { Ok(first_number) => first_number, Err(e) => return Err(e), }; let second_number = match second_number_str.parse::<i32>() { Ok(second_number) => second_number, Err(e) => return Err(e), }; Ok(first_number * second_number) } fn print(result: Result<i32, ParseIntError>) { match result { Ok(n) => println!("n is {}", n), Err(e) => println!("Error: {}", e), } } fn main() { print(multiply("10", "2")); print(multiply("t", "2")); }
Tới đây, bạn đã được học về cách xử lý lỗi một cách tường minh bằng cách sử dụng combinators và kĩ thuật early returns. Mặc dù chúng ta thường sẽ tránh việc sử dụng panic khi gặp lỗi, nhưng đôi khi phải xử lý tường minh tất cả các lỗi có thể xảy ra dẫn đến việc viết code rất lằng nhằng và phiền toái.
Ở phần tiếp theo, bạn sẽ được giới thiệu toán tử ?
để áp dụng vào những trường hợp
mà chúng ta chỉ cần unwrap
là có thể giải quyết được vấn đề mà không dẫn đến panic
.
Toán tử ?
Thỉnh thoảng, chúng ta chỉ muốn đơn giản là sử dụng hàm unwrap
mà bỏ qua khả năng panic
. Hiện tại, unwrap
buộc chúng ta phải viết các mã lồng vào nhau sâu hơn
để lấy giá trị ra trong khi thứ mà ta muốn chỉ là lấy ra giá trị trả về.
Đây là lý do mà toán tử ?
được tạo ra.
Khi gặp phải một đối tượng Err
, có hai cách xử lý:
panic!
, chúng ta nên tránh làm thế này nếu có thể.return
vìErr
nghĩa là chương trình không thể xử lý được tiếp tục nữa.
Toán tử ?
gần tương đương nhất với1 hàm unwrap
vì hàm unwrap
sẽ return
các Err
s
thay vì panic
cả chương trình.
Hãy xem cách ta có thể đơn giản hoá ví dụ xử lý lỗi sử dụng combinator ở phần trước:
use std::num::ParseIntError; fn multiply(first_number_str: &str, second_number_str: &str) -> Result<i32, ParseIntError> { let first_number = first_number_str.parse::<i32>()?; let second_number = second_number_str.parse::<i32>()?; Ok(first_number * second_number) } fn print(result: Result<i32, ParseIntError>) { match result { Ok(n) => println!("n is {}", n), Err(e) => println!("Error: {}", e), } } fn main() { print(multiply("10", "2")); print(multiply("t", "2")); }
Macro try!
Trước khi toán tử ?
được giới thiệu ở phiên bản sau này, ta dùng macro try!
để làm được chức năng tương tự như ?
.
Hiện nay, người ta khuyến khích sử dụng toán tử ?
nhưng bạn có thể đôi khi tìm thấy macro try!
được sử dụng khi đọc những code cũ của các phiên bản trước.
Hàm multiply
được ví dụ ở các ví dụ trước sẽ nhìn giống như thế này nếu dùng macro try!
:
// Để có thể biên dịch và chạy ví dụ này thành công với Cargo, hãy thay đổi giá trị của // trường `edition` trong phần `[package]` ở file `Cargo.toml` thành "2015" use std::num::ParseIntError; fn multiply(first_number_str: &str, second_number_str: &str) -> Result<i32, ParseIntError> { let first_number = try!(first_number_str.parse::<i32>()); let second_number = try!(second_number_str.parse::<i32>()); Ok(first_number * second_number) } fn print(result: Result<i32, ParseIntError>) { match result { Ok(n) => println!("n is {}", n), Err(e) => println!("Error: {}", e), } } fn main() { print(multiply("10", "2")); print(multiply("t", "2")); }
Đọc re-enter ? để biết thêm.
Multiple error types
Các ví dụ trước luôn rất thuận tiện; những Result
tương tác với
những Result
, trong khi những Option
cũng tương tác với những Option
.
Đôi khi Option
lại cần tương tác với Result
, hoặc
Result<T, Error1>
cần tương tác với Result<T, Error2>
. Trong những trường hợp đó,
ta muốn quản lý các loại lỗi khác nhau sao cho những lỗi đó có thể kết hợp được với nhau và dễ dàng tương tác.
Trong đoạn code sau, 2 trường hợp gọi unwrap
trả về 2 loại lỗi khác nhau.
Vec::first
trả về Option
, trong khi parse::<i32>
trả về
Result<i32, ParseIntError>
:
fn double_first(vec: Vec<&str>) -> i32 { let first = vec.first().unwrap(); // Trả về loại lỗi 1 2 * first.parse::<i32>().unwrap() // Trả về loại lỗi 2 } fn main() { let numbers = vec!["42", "93", "18"]; let empty = vec![]; let strings = vec!["tofu", "93", "18"]; println!("The first doubled is {}", double_first(numbers)); println!("The first doubled is {}", double_first(empty)); // Loại lỗi 1: vectơ đầu vào rỗng println!("The first doubled is {}", double_first(strings)); // Loại lỗi 2: phần tử không thể chuyển đổi thành một số }
Trong các phần tiếp theo, chúng ta sẽ thấy một vài phương pháp để xử lý những vấn đề như như trên.
Lấy Result
ra khỏi Option
Cách cơ bản nhất để xử lý loại lỗi hỗn hợp là nhúng chúng vào với nhau.
use std::num::ParseIntError; fn double_first(vec: Vec<&str>) -> Option<Result<i32, ParseIntError>> { vec.first().map(|first| { first.parse::<i32>().map(|n| 2 * n) }) } fn main() { let numbers = vec!["42", "93", "18"]; let empty = vec![]; let strings = vec!["tofu", "93", "18"]; println!("The first doubled is {:?}", double_first(numbers)); println!("The first doubled is {:?}", double_first(empty)); // Error 1: Input vector rỗng. println!("The first doubled is {:?}", double_first(strings)); // Error 2: phần tử không thể chuyển thành một số. }
Có những lúc chúng ta muốn dừng việc xử lý khi có lỗi xảy ra( giống như khi sử dụng ?
) nhưng vẫn tiếp tục xử lý khi Option
là None
. Mội vài combinator rất hữu ích để hoán đổi Result
và Option
.
use std::num::ParseIntError; fn double_first(vec: Vec<&str>) -> Result<Option<i32>, ParseIntError> { let opt = vec.first().map(|first| { first.parse::<i32>().map(|n| 2 * n) }); opt.map_or(Ok(None), |r| r.map(Some)) } fn main() { let numbers = vec!["42", "93", "18"]; let empty = vec![]; let strings = vec!["tofu", "93", "18"]; println!("The first doubled is {:?}", double_first(numbers)); println!("The first doubled is {:?}", double_first(empty)); println!("The first doubled is {:?}", double_first(strings)); }
Định nghĩa một kiểu dữ liệu lỗi
Đôi khi ta có thể đơn giản hóa đoạn mã bằng cách che giấu (masking) tất cả các kiểu dữ liệu lỗi khác nhau với một loại lỗi duy nhất. Ta có thể thể hiện ra bằng một lỗi đã được tùy chỉnh.
Rust cho phép chúng ta tự định nghĩa kiểu dữ liệu lỗi của riêng mình. Nhìn chung, một kiểu dữ liệu "tốt" phải:
- Đại diện cho nhiều lỗi với cùng một kiểu dữ liệu
- Thể hiện thông điệp lỗi cho người dùng một cách rõ ràng
- Dễ dàng so sánh với các kiểu dữ liệu lỗi khác
- Nên:
Err(EmptyVec)
- Không nên:
Err("Hãy sử dụng vector có chứa ít nhất 1 phần tử".to_owned())
- Nên:
- Có thể chứa thông tin về lỗi
- Nên:
Err(BadChar(c, position))
- Không nên:
Err("Không thể sử dụng kí tự + ở đây".to_owned())
- Nên:
- Kết hợp được với các kiểu lỗi khác
use std::fmt; type Result<T> = std::result::Result<T, DoubleError>; // Định nghĩa lỗi. Có thể được tùy biến tùy trường hợp xử lí lỗi riêng. // Giờ đây ta sẽ có thể lập trình các lỗi của mình, dựa theo một triển khai lỗi gốc, // hoặc xử lí trung gian. #[derive(Debug, Clone)] struct DoubleError; // Việc tạo lập một lỗi tách biệt hoàn toàn so với cách nó được hiển thị // Chúng ta không cần phải lo lắng về việc xáo trộn các logic phức tạp với cách hiển thị. // // Lưu ý rằng, ta đang không hề lưu bất cứ thông tin nào về các lỗi. Đồng nghĩa với việc, nếu không // chỉnh lại kiểu dữ liệu nhằm đưa ra thông tin, ta sẽ không thể chỉ ra chính xác chuỗi kí tự nào // đã thất bại trong việc hiển thị. impl fmt::Display for DoubleError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "invalid first item to double") } } fn double_first(vec: Vec<&str>) -> Result<i32> { vec.first() // Chuyển lỗi về cùng kiểu dữ liệu lỗi mới .ok_or(DoubleError) .and_then(|s| { s.parse::<i32>() // Update to the new error type here also. .map_err(|_| DoubleError) .map(|i| 2 * i) }) } fn print(result: Result<i32>) { match result { Ok(n) => println!("The first doubled is {}", n), Err(e) => println!("Error: {}", e), } } fn main() { let numbers = vec!["42", "93", "18"]; let empty = vec![]; let strings = vec!["tofu", "93", "18"]; print(double_first(numbers)); print(double_first(empty)); print(double_first(strings)); }
Box
ing lỗi
Box
là một cách để viết đoạn mã đơn giản mà vẫn bảo toàn được những lỗi ban đầu. Nhược điểm của cách làm này là kiểu dữ liệu lỗi bên trong chỉ có thể được xác định tại thời gian thực thi và không đượcxác định tĩnh (statically determined).
(Dịch giả: Điều này có nghĩa là khi sử dụng các kiểu lỗi động (dynamic error types) như Box trong Rust, kiểu lỗi cụ thể mà chương trình có thể gặp phải sẽ không được xác định tại thời điểm biên dịch (compile time), mà chỉ được biết đến khi chạy chương trình.)
Thư viện stdlib hỗ trợ boxing các lỗi của chúng ta bằng cách triển khai Box
và chuyển đổi bất cứ kiểu dữ liệu nào có trait Error
trở thành trait object Box<Error>
thông qua trait From
.
use std::error; use std::fmt; // Thay đổi alias thành `Box<error::Error>`. type Result<T> = std::result::Result<T, Box<dyn error::Error>>; #[derive(Debug, Clone)] struct EmptyVec; impl fmt::Display for EmptyVec { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "phần tử đầu tiên không hợp lệ để có thể nhân đôi") } } impl error::Error for EmptyVec {} fn double_first(vec: Vec<&str>) -> Result<i32> { vec.first() .ok_or_else(|| EmptyVec.into()) // Converts to Box .and_then(|s| { s.parse::<i32>() .map_err(|e| e.into()) // Converts to Box .map(|i| 2 * i) }) } fn print(result: Result<i32>) { match result { Ok(n) => println!("Phần tử đầu tiên được nhân đôi bằng {}", n), Err(e) => println!("Lỗi: {}", e), } } fn main() { let numbers = vec!["42", "93", "18"]; let empty = vec![]; let strings = vec!["tofu", "93", "18"]; print(double_first(numbers)); print(double_first(empty)); print(double_first(strings)); }
Tham khảo thêm:
Dynamic dispatch và Error
trait
Các công dụng khác của ?
Trong ví dụ trước hãy lưu ý rằng phản ứng tức thời của chúng tôi đối với việc gọi parse
là để map
lỗi từ một lỗi thư viện vào môt lỗi được đóng gói:
#![allow(unused)] fn main() { .and_then(|s| s.parse::<i32>() .map_err(|e| e.into()) }
Vì đây là một thao tác đơn giản và phổ biến, nên sẽ rất thuận tiện nếu nó có thể được bỏ qua. Than ôi, bởi vì and_then
không đủ linh hoạt, nên không thể. Tuy nhiên, thay vào đó chúng ta có thể sử dụng ?
.
Trước đó ?
đã được giải thích là unwrap
hoặc return Err(err)
. Điều này chỉ đúng phần nào, thực tế nó có nghĩa là unwrap
hoặc return Err(From::from(err)
. Do From::from
là một tiện ích để chuyển đổi giữa các kiểu khác nhau, điều này có nghĩa là nếu bạn sử dụng ?
lỗi có thể tự động chuyển đổi thành kiểu trả về .
Dưới đây, chúng tôi viết lại ví dụ trước bằng cách sử dụng ?
. Kết quả là, khi From::from
được thực thi nó làm biến mất lỗi map_err
.
use std::error; use std::fmt; // Thay đổi bí danh thành `Box<dyn error::Error>`. type Result<T> = std::result::Result<T, Box<dyn error::Error>>; #[derive(Debug)] struct EmptyVec; impl fmt::Display for EmptyVec { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "invalid first item to double") } } impl error::Error for EmptyVec {} // Cấu trúc giống như trước đây nhưng thay vì xâu chuỗi tất cả `Results` // và `Option` lại với nhau, chúng ta sử dụng `?` để lấy giá trị bên trong ngay lập tức. fn double_first(vec: Vec<&str>) -> Result<i32> { let first = vec.first().ok_or(EmptyVec)?; let parsed = first.parse::<i32>()?; Ok(2 * parsed) } fn print(result: Result<i32>) { match result { Ok(n) => println!("The first doubled is {}", n), Err(e) => println!("Error: {}", e), } } fn main() { let numbers = vec!["42", "93", "18"]; let empty = vec![]; let strings = vec!["tofu", "93", "18"]; print(double_first(numbers)); print(double_first(empty)); print(double_first(strings)); }
Điều này thực sự khá rõ ràng. So với panic
ban đầu, nó rất giống với việc thay thế các lời gọi unwrap
bằng ?
ngoại trừ các kiểu trả về là Result
. DO đó, chúng được phân giải ra ở cấp cao nhất.
Xem thêm
From::from và ?
Wrapping errors
Một phương pháp khác để xử lý các lỗi là bọc chúng trong một kiểu dữ liệu lỗi mà bạn tự định nghĩa.
use std::error; use std::error::Error; use std::num::ParseIntError; use std::fmt; type Result<T> = std::result::Result<T, DoubleError>; #[derive(Debug)] enum DoubleError { EmptyVec, // Chúng ta sẽ trì hoãn việc triển khai phân tích lỗi cú pháp đối với lỗi của chúng. // Việc cung cấp thông tin bổ sung yêu cầu thêm dữ liệu vào loại lỗi. Parse(ParseIntError), } impl fmt::Display for DoubleError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match *self { DoubleError::EmptyVec => write!(f, "please use a vector with at least one element"), // Lỗi được bọc(wrap) chứa thông tin bổ sung và có sẵn thông qua phương thức source(được triển khai phía dưới). DoubleError::Parse(..) => write!(f, "the provided string could not be parsed as int"), } } } impl error::Error for DoubleError { fn source(&self) -> Option<&(dyn error::Error + 'static)> { match *self { DoubleError::EmptyVec => None, // Nguyên nhân là việc thực hiện triển khai loại lỗi cơ bản. Được chuyển hoàn toàn sang trait object `&error::Error`. Điều này hoạt động được vì loại bên dưới đã triển khai trong `Error` trait. DoubleError::Parse(ref e) => Some(e), } } } // Thực hiện chuyển đổi từ `ParseIntError` thành `DoubleError`. // Điều này sẽ được gọi tự động bởi `?` nếu một `ParseIntError` cần được chuyển đổi thành một `DoubleError`. impl From<ParseIntError> for DoubleError { fn from(err: ParseIntError) -> DoubleError { DoubleError::Parse(err) } } fn double_first(vec: Vec<&str>) -> Result<i32> { let first = vec.first().ok_or(DoubleError::EmptyVec)?; // Ở đây chúng ta ngầm sử dụng triển khai `ParseIntError` của `From` (mà chúng ta đã định nghĩa ở trên) để tạo ra một `DoubleError`. let parsed = first.parse::<i32>()?; Ok(2 * parsed) } fn print(result: Result<i32>) { match result { Ok(n) => println!("The first doubled is {}", n), Err(e) => { println!("Error: {}", e); if let Some(source) = e.source() { println!(" Caused by: {}", source); } }, } } fn main() { let numbers = vec!["42", "93", "18"]; let empty = vec![]; let strings = vec!["100.4", "93", "18"]; print(double_first(numbers)); print(double_first(empty)); print(double_first(strings)); }
Điều này bổ sung thêm một chút mẫu để xử lý lỗi và có thể không cần thiết trong tất cả các ứng dụng. Có một số thư viện có thể đảm nhiệm việc xử lý mẫu lỗi cho bạn.
See also:
From::from
and Enums
Lặp qua các Result
Một Iter::map
có thể không thành công, ví dụ:
fn main() { let strings = vec!["tofu", "93", "18"]; let numbers: Vec<_> = strings .into_iter() .map(|s| s.parse::<i32>()) .collect(); println!("Results: {:?}", numbers); // Results: [Err(ParseIntError { kind: InvalidDigit }), Ok(93), Ok(18)] }
Hãy thông qua từng bước của chiến lược để xứ lý tình huống này.
Bỏ qua các phần tử bị lỗi bằng cách sử dụng filter_map()
filter_map
gọi một function và loại các các kêt quả bị lỗi.
fn main() { let strings = vec!["tofu", "93", "18"]; let numbers: Vec<_> = strings .into_iter() .filter_map(|s| s.parse::<i32>().ok()) .collect(); println!("Results: {:?}", numbers); // [93, 18] }
Thu thập các phần tử bị lỗi với map_err()
và filter_map()
map_err
gọi một function nhận vào một error, bằng cách thêm map_err
vào filter_map
chúng ta có thể lưu lại các lỗi xảy ra trong lúc thực hiện vòng lặp.
fn main() { let strings = vec!["42", "tofu", "93", "999", "18"]; let mut errors = vec![]; let numbers: Vec<_> = strings .into_iter() .map(|s| s.parse::<u8>()) .filter_map(|r| r.map_err(|e| errors.push(e)).ok()) .collect(); println!("Numbers: {:?}", numbers); println!("Errors: {:?}", errors); // Numbers: [42, 93, 18] // Errors: [ParseIntError { kind: InvalidDigit }, ParseIntError { kind: PosOverflow }] }
Kết thúc vòng lặp với collect()
Result
có triển khai FromIterator
, nên một vector chứa các kết quả (Vec<Result<T, E>>
)
có thể được chuyển thành một kết quả với vector(Result<Vec<T>, E>
). Khi một
Result::Err
được tìm thấy, vòng lặp sẽ bị hủy.
fn main() { let strings = vec!["tofu", "93", "18"]; let numbers: Result<Vec<_>, _> = strings .into_iter() .map(|s| s.parse::<i32>()) .collect(); println!("Results: {:?}", numbers); }
Một cơ chế khác có thể được sử dụng là Option
.
Thu thập các giá trị hợp lệ và các lỗi với partition()
fn main() { let strings = vec!["tofu", "93", "18"]; let (numbers, errors): (Vec<_>, Vec<_>) = strings .into_iter() .map(|s| s.parse::<i32>()) .partition(Result::is_ok); println!("Numbers: {:?}", numbers); println!("Errors: {:?}", errors); // Numbers: [Ok(93), Ok(18)] // Errors: [Err(ParseIntError { kind: InvalidDigit })] }
Khi nhìn vào các kết quả, bạn sẽ thấy rằng mọi thứ vẫn được bọc bởi Result
. Vậy nên cần xử lí thêm một chút.
fn main() { let strings = vec!["tofu", "93", "18"]; let (numbers, errors): (Vec<_>, Vec<_>) = strings .into_iter() .map(|s| s.parse::<i32>()) .partition(Result::is_ok); let numbers: Vec<_> = numbers.into_iter().map(Result::unwrap).collect(); let errors: Vec<_> = errors.into_iter().map(Result::unwrap_err).collect(); println!("Numbers: {:?}", numbers); println!("Errors: {:?}", errors); // Numbers: [93, 18] // Errors: [ParseIntError { kind: InvalidDigit }] }
Các kiểu của thư viện std
Thư viện std
cung cấp rất nhiều kiểu tuỳ chỉnh, những kiểu mà được mở rộng một cách đáng kể
dựa trên các kiểu nguyên thuỷ primitives
. Có thể kể đến như:
- Chuỗi
String
có thể mở rộng kích thước như: "hello world" - Các vector có thể mở rộng kích thước:
[1, 2, 3]
- Các kiểu tuỳ chọn:
Option<i32>
- Các kiểu để xử lý lỗi:
Result<i32, i32>
- Các con trỏ được cấp phát trên heap:
Box<i32>
Đọc thêm:
Box, stack và heap
Mặc định, tất cả các giá trị trong Rust đều được cấp phát trên stack. Tuy nhiên, giá trị có thể được đóng gói (boxed)
(nghĩa là được cấp phát trên heap) bằng cách tạo ra một Box<T>
. Box là một con trỏ thông minh (smart pointer) trỏ
tới một giá trị được cấp phát trên heap có kiểu dữ liệu T
. Khi một box ra khỏi scope, destructor của nó sẽ được gọi, đối tượng
được chứa bên trong sẽ bị xoá, tài nguyên trên heap sẽ được giải phóng.
Giá trị được boxed có thể được giải tham chiếu (dereferenced) bằng toán tử *
;
điều này giúp ta loại bớt một tầng gián đoạn (indirection)
use std::mem; #[allow(dead_code)] #[derive(Debug, Clone, Copy)] struct Point { x: f64, y: f64, } // Một hình chữ nhật có thể được xác định bởi vị trí của các đỉnh trên cùng bên trái và // dưới cùng bên phải của nó trong không gian #[allow(dead_code)] struct Rectangle { top_left: Point, bottom_right: Point, } fn origin() -> Point { Point { x: 0.0, y: 0.0 } } fn boxed_origin() -> Box<Point> { // Cấp phát vùng nhớ cho điểm này trên heap, và trả về một con trỏ tới nó. Box::new(Point { x: 0.0, y: 0.0 }) } fn main() { // (Tất cả các chú thích kiểu dữ liệu đều là dư thừa trong đoạn code này) // Các biến được cấp phát trên stack let point: Point = origin(); let rectangle: Rectangle = Rectangle { top_left: origin(), bottom_right: Point { x: 3.0, y: -4.0 } }; // Hình chữ nhật được cấp phát trên heap let boxed_rectangle: Box<Rectangle> = Box::new(Rectangle { top_left: origin(), bottom_right: Point { x: 3.0, y: -4.0 }, }); // Kết quả đầu ra của hàm cũng có thể được boxed let boxed_point: Box<Point> = Box::new(origin()); // Có hai tầng gián đoạn (indirection) dưới đây let box_in_a_box: Box<Box<Point>> = Box::new(boxed_origin()); println!("Point occupies {} bytes on the stack", mem::size_of_val(&point)); println!("Rectangle occupies {} bytes on the stack", mem::size_of_val(&rectangle)); // Kích thước của box = kích thước con trỏ println!("Boxed point occupies {} bytes on the stack", mem::size_of_val(&boxed_point)); println!("Boxed rectangle occupies {} bytes on the stack", mem::size_of_val(&boxed_rectangle)); println!("Boxed box occupies {} bytes on the stack", mem::size_of_val(&box_in_a_box)); // Sao chép dữ liệu chứa trong `boxed_point` đến `unboxed_point` let unboxed_point: Point = *boxed_point; println!("Unboxed point occupies {} bytes on the stack", mem::size_of_val(&unboxed_point)); }
Vectors
Các vectơ là các mảng có thể thay đổi kích thước. Giống như các lát cắt, kích thước của chúng không được biết tại thời điểm biên dịch, nhưng chúng có thể tăng hoặc giảm kích thước bất cứ lúc nào. Một vectơ được biểu diễn bằng 3 tham số:
- con trỏ đến dữ liệu
- chiều dài
- dung lượng
Dung lượng cho biết dung lượng bộ nhớ được dành riêng cho vectơ. Vectơ có thể phát triển miễn là chiều dài nhỏ hơn dung lượng. Khi cần vượt qua ngưỡng này, vectơ được cấp phát lại với dung lượng lớn hơn.
fn main() { // Iterators có thể được gom thành vector let collected_iterator: Vec<i32> = (0..10).collect(); println!("Collected (0..10) into: {:?}", collected_iterator); // Macro `vec!` có thể dùng để khởi tạo một vector let mut xs = vec![1i32, 2, 3]; println!("Initial vector: {:?}", xs); // Chèn phần tử mới vào cuối vector println!("Push 4 into the vector"); xs.push(4); println!("Vector: {:?}", xs); // Lỗi! Các vectơ bất biến không thể phát triển collected_iterator.push(0); // FIXME ^ Comment out this line // Phương thức `len` trả về số lượng phần tử hiện được lưu trữ trong một vector println!("Vector length: {}", xs.len()); // Việc lập chỉ mục được thực hiện bằng cách sử dụng dấu ngoặc vuông (việc lập chỉ mục bắt đầu từ 0) println!("Second element: {}", xs[1]); // `pop` loại bỏ phần tử cuối cùng khỏi vector và trả về nó println!("Pop last element: {:?}", xs.pop()); // Lập chỉ mục ngoài giới hạn kiến trả về lỗi println!("Fourth element: {}", xs[3]); // FIXME ^ Comment out this line // `Vector`s có thể dễ dàng lặp lại println!("Contents of xs:"); for x in xs.iter() { println!("> {}", x); } // Một `Vector` cũng có thể được lặp lại trong khi lặp lại // số đếm được liệt kê trong một biến riêng biệt (`i`) for (i, x) in xs.iter().enumerate() { println!("In position {} we have value {}", i, x); } // Nhờ có `iter_mut`, `Vector` có thể thay đổi cũng có thể được lặp lại // kết thúc theo cách cho phép sửa đổi từng giá trị for x in xs.iter_mut() { *x *= 3; } println!("Updated vector: {:?}", xs); }
Có thể tìm thấy nhiều phương thức Vec
hơn trong module
std::vec
Chuỗi
Có 2 kiểu chuỗi trong rust: String
và &str
.
Một String
sẽ được lưu trữ như là 1 vector dưới định dạng bytes (Vec<u8>
), luôn theo chuẩn UTF-8. String
được phân bổ theo vùng nhớ heap, có thể mở rộng và kết thúc không phải là kí tự null.
&str
là một dạng (&[u8]
) luôn trỏ đến một chuỗi UTF-8 hợp lệ và có thể được sử dụng để xem như 1 String
, giống như &[T]
được coi như Vec<T>
.
fn main() { // (tất cả các kiểu chú thích đều là thừa) // Tham chiếu đến một string được cấp phát trong bộ nhớ chỉ đọc let pangram: &'static str = "the quick brown fox jumps over the lazy dog"; println!("Pangram: {}", pangram); // Lặp lại các từ theo thứ tự ngược lại, không tạo thêm chuỗi mới println!("Words in reverse"); for word in pangram.split_whitespace().rev() { println!("> {}", word); } let mut chars: Vec<char> = pangram.chars().collect(); chars.sort(); chars.dedup(); // Tạo 1 `String` mới có thể mở rộng được let mut string = String::new(); for c in chars { // Thêm 1 char vào cuối chuỗi string string.push(c); // Thêm 1 string vào cuối chuỗi string string.push_str(", "); } // Chuỗi đã cắt là một cắt lát của chuỗi ban đầu, do đó không có cấp phát mới nào được thực hiện let chars_to_trim: &[char] = &[' ', ',']; let trimmed_str: &str = string.trim_matches(chars_to_trim); println!("Used characters: {}", trimmed_str); // Heap cấp phát một chuỗi let alice = String::from("I like dogs"); // Cấp phát bộ nhớ mới và lưu trữ chuỗi đã sửa đổi ở đó let bob: String = alice.replace("dog", "cat"); println!("Alice says: {}", alice); println!("Bob says: {}", bob); }
Có thể tìm thấy các phương thức str
/String
khác trong
std::str và
std::string
modules
Ký tự và chuỗi thoát
Có nhiều cách để viết chuỗi ký tự có ký tự đặc biệt trong đó. Tất cả đều dẫn đến một &str
giống nhau, vì vậy tốt nhất bạn nên sử dụng biểu mẫu thuận tiện nhất để viết. Tương tự, có nhiều cách để viết các ký tự chuỗi byte, tất cả đều có kết quả là &[u8; N]
.
Nói chung, các ký tự đặc biệt được thoát bằng ký tự gạch chéo ngược: \
. Bằng cách này, bạn có thể thêm bất kỳ ký tự nào vào chuỗi của mình, kể cả những ký tự không in được và những ký tự mà bạn không biết cách nhập. Nếu bạn muốn có dấu gạch chéo ngược đứng độc lập, hãy thoát(escape) nó bằng một dấu gạch chéo ngược khác: \\
Các dấu phân cách bằng chữ của chuỗi hoặc ký tự xuất hiện trong một chữ phải được thoát ra: "\""
, '\''
.
fn main() { // Bạn có thể sử dụng các dấu thoát để ghi byte theo giá trị thập lục phân của chúng... let byte_escape = "I'm writing \x52\x75\x73\x74!"; println!("What are you doing\x3F (\\x3F means ?) {}", byte_escape); // ...hoặc các điểm mã Unicode. let unicode_codepoint = "\u{211D}"; let character_name = "\"DOUBLE-STRUCK CAPITAL R\""; println!("Unicode character {} (U+211D) is called {}", unicode_codepoint, character_name ); let long_string = "String literals can span multiple lines. The linebreak and indentation here ->\ <- can be escaped too!"; println!("{}", long_string); }
Đôi khi có quá nhiều ký tự cần được thoát hoặc việc viết một chuỗi nguyên trạng sẽ thuận tiện hơn nhiều. Đây là nơi các chuỗi nguyên trạng phát huy tác dụng.
fn main() { let raw_str = r"Escapes don't work here: \x3F \u{211D}"; println!("{}", raw_str); // Nếu bạn cần trích dẫn trong một chuỗi dạng nguyên trạng, hãy thêm một cặp # let quotes = r#"And then I said: "There is no escape!""#; println!("{}", quotes); // Nếu bạn cần sử dụng "# trong chuỗi của mình, chỉ cần sử dụng nhiều kí tự # hơn trong dấu phân cách. // Bạn có thể sử dụng tối đa 65535 kí tự #. let longer_delimiter = r###"A string with "# in it. And even "##!"###; println!("{}", longer_delimiter); }
Bạn muốn một chuỗi không phải UTF-8? (Hãy nhớ rằng str
và String
phải là UTF-8 hợp lệ).
Hoặc có thể bạn muốn một mảng byte chứa chủ yếu là văn bản? Lúc đó chuỗi byte sẽ giúp ích!
use std::str; fn main() { // Lưu ý rằng đây không thực sự là `&str` let bytestring: &[u8; 21] = b"this is a byte string"; // Mảng byte không có thuộc tính `Display` nên việc in chúng hơi bị hạn chế println!("A byte string: {:?}", bytestring); // Chuỗi byte có thể có byte thoát... let escaped = b"\x52\x75\x73\x74 as bytes"; // ...nhưng không thoát unicode // let escaped = b"\u{211D} is not allowed"; println!("Some escaped bytes: {:?}", escaped); // Chuỗi byte nguyên trạng hoạt động giống như chuỗi nguyên trạng let raw_bytestring = br"\u{211D} is not escaped here"; println!("{:?}", raw_bytestring); // Chuyển đổi một mảng byte thành `str` có thể thất bại if let Ok(my_str) = str::from_utf8(raw_bytestring) { println!("And the same as text: '{}'", my_str); } let _quotes = br#"You can also use "fancier" formatting, \ like with normal raw strings"#; // Chuỗi byte không nhất thiết phải là UTF-8 let shift_jis = b"\x82\xe6\x82\xa8\x82\xb1\x82\xbb"; // "ようこそ" in SHIFT-JIS // Nhưng không phải lúc nào chúng cũng có thể được chuyển thành `str` match str::from_utf8(shift_jis) { Ok(my_str) => println!("Conversion successful: '{}'", my_str), Err(e) => println!("Conversion failed: {:?}", e), }; }
Để chuyển đổi giữa các ký tự mã hóa, hãy xem encoding.
Một danh sách chi tiết hơn về cách viết chuỗi ký tự và ký tự thoát được đưa ra trong chương 'Tokens' của Rust Reference.
Option
Đôi lúc, chúng ta muốn xử lý lỗi của một phần trong chương trình thay vì gọi panic!
; điều này có thể được thực hiện bằng cách sử dụng Option
.
Option<T>
có thể mang hai biến thể:
None
, biểu hiện cho việc thất bại khi thực thi hoặc không có giá trị vàSome(value)
, một kiểu tuple bọc ngoài giá trịvalue
với kiểu dữ liệu làT
.
// Một phép chia số nguyên không gây `panic!` fn checked_division(dividend: i32, divisor: i32) -> Option<i32> { if divisor == 0 { // Việc thất bại khi thực thi được biểu diễn bằng biến thể `None` None } else { // Kết quả được bọc bên trong biến thể `Some` Some(dividend / divisor) } } // Hàm này có thể không thực hiện thành công phép chia fn try_division(dividend: i32, divisor: i32) { // Giá trị của `Option` có thể được phân tích cú pháp theo match, tương tự như kiểu enum match checked_division(dividend, divisor) { None => println!("{} / {} failed!", dividend, divisor), Some(quotient) => { println!("{} / {} = {}", dividend, divisor, quotient) }, } } fn main() { try_division(4, 2); try_division(1, 0); // Gán `None` cho một biến cần xác định kiểu let none: Option<i32> = None; let _equivalent_none = None::<i32>; let optional_float = Some(0f32); // Unwrap biến thể `Some` sẽ trích xuất ra giá trị được bọc bên trong println!("{:?} unwraps to {:?}", optional_float, optional_float.unwrap()); // Unwrap biến thể `None` sẽ gây ra `panic!` println!("{:?} unwraps to {:?}", none, none.unwrap()); }
Result
Ta có thể thấy rằng enum Option
có thể được dùng như là giá trị trả về từ các hàm có thể có lỗi,
trong đó None
có thể được dùng để trả về để biểu thị rằng có lỗi xảy ra. Tuy nhiên đôi khi việc diễn tả tại sao
một thao tác bị lỗi lại quan trọng hơn. Để làm được điều này, ta đã có enum Result
.
Enum Result<T, E>
có hai biến thể:
Ok(value)
để biểu thị một thao tác đã thành công và kèm theo đó làvalue
được trả về bởi thao tác đó. (value
có kiểuT
)Err(why)
để biểu thị một thao tác đã thất bại và kèm theo đó làwhy
, thứ được kì vọng để giải thích nguyên nhân gây nên lỗi. (why
có kiểuE
)
mod checked { // "Các lỗi" toán học mà chúng ta muốn bắt #[derive(Debug)] pub enum MathError { DivisionByZero, NonPositiveLogarithm, NegativeSquareRoot, } pub type MathResult = Result<f64, MathError>; pub fn div(x: f64, y: f64) -> MathResult { if y == 0.0 { // Thao tác này sẽ `thất bại`, thay vào đó hãy trả về nguyên nhân của // sự thất bại được gói bên trong `Err` Err(MathError::DivisionByZero) } else { // Thao tác này là hợp lệ, trả về kết quả được gói bên trong `Ok` Ok(x / y) } } pub fn sqrt(x: f64) -> MathResult { if x < 0.0 { Err(MathError::NegativeSquareRoot) } else { Ok(x.sqrt()) } } pub fn ln(x: f64) -> MathResult { if x <= 0.0 { Err(MathError::NonPositiveLogarithm) } else { Ok(x.ln()) } } } // `op(x, y)` === `sqrt(ln(x / y))` fn op(x: f64, y: f64) -> f64 { // Dưới đây là ba tầng match lồng nhau! match checked::div(x, y) { Err(why) => panic!("{:?}", why), Ok(ratio) => match checked::ln(ratio) { Err(why) => panic!("{:?}", why), Ok(ln) => match checked::sqrt(ln) { Err(why) => panic!("{:?}", why), Ok(sqrt) => sqrt, }, }, } } fn main() { // Liệu thao tác có thất bại không? println!("{}", op(1.0, 10.0)); }
?
Việc xử lý một chuỗi các kết quả sử dụng match có thể làm cho đoạn mã của bạn trở nên khá lộn xộn và khó đọc; may mắn là, toán tử ?
có thể để làm cho mọi thứ trở nên đẹp đẽ và dễ đọc hơn. ?
được sử dụng ở cuối một biểu thức trả về là một Result
, và nó tương đương với một biểu thức match, trong đó nhánh Err(err)
mở rộng thành một return Err(From::from(err))
, và nhánh Ok(ok)
mở rộng thành một biểu thức ok
.
mod checked { #[derive(Debug)] enum MathError { DivisionByZero, NonPositiveLogarithm, NegativeSquareRoot, } type MathResult = Result<f64, MathError>; fn div(x: f64, y: f64) -> MathResult { if y == 0.0 { Err(MathError::DivisionByZero) } else { Ok(x / y) } } fn sqrt(x: f64) -> MathResult { if x < 0.0 { Err(MathError::NegativeSquareRoot) } else { Ok(x.sqrt()) } } fn ln(x: f64) -> MathResult { if x <= 0.0 { Err(MathError::NonPositiveLogarithm) } else { Ok(x.ln()) } } // Hàm trung gian fn op_(x: f64, y: f64) -> MathResult { // nếu `div` "lỗi", `DivisionByZero` sẽ được trả về let ratio = div(x, y)?; // nếu `ln` "lỗi", `NonPositiveLogarithm` sẽ được trả về let ln = ln(ratio)?; sqrt(ln) } pub fn op(x: f64, y: f64) { match op_(x, y) { Err(why) => panic!("{}", match why { MathError::NonPositiveLogarithm => "logarithm of non-positive number", MathError::DivisionByZero => "division by zero", MathError::NegativeSquareRoot => "square root of negative number", }), Ok(value) => println!("{}", value), } } } fn main() { checked::op(1.0, 10.0); }
Đảm bảo rằng bạn sẽ kiểm tra documentation, vì có nhiều phương pháp để map/compose Result
.
panic!
Macro panic!
có thể được sử dụng để gây ra tình huống panic và bắt đầu giải phóng stack của nó. Trong quá trình giải phóng, bộ thực thi sẽ chịu trách nhiệm giải phóng tất cả tài nguyên thuộc sở hữu của luồng bằng cách gọi hàm hủy (destructor) tất cả các đối tượng của nó.
Vì chúng ta đang xử lý các chương trình chỉ có một luồng, panic!
sẽ khiến chương trình in ra một thông báo kết thúc chương trình và dừng hoạt động.
// Thực hiện lại phép chia số nguyên (/) fn division(dividend: i32, divisor: i32) -> i32 { if divisor == 0 { // Phép chia cho 0 gây ra sự kết thúc cho chương trình panic!("division by zero"); } else { dividend / divisor } } // Hàm main fn main() { // Cấp phát một biến số nguyên ở Heap let _x = Box::new(0i32); // Thao tác này sẽ gây ra lỗi tác vụ division(3, 0); println!("This point won't be reached!"); // `_x` sẽ bị hủy tại đây }
Kiểm tra rằng panic!
không gây rò rỉ bộ nhớ.
$ rustc panic.rs && valgrind ./panic
==4401== Memcheck, a memory error detector
==4401== Copyright (C) 2002-2013, and GNU GPL'd, by Julian Seward et al.
==4401== Using Valgrind-3.10.0.SVN and LibVEX; rerun with -h for copyright info
==4401== Command: ./panic
==4401==
thread '<main>' panicked at 'division by zero', panic.rs:5
==4401==
==4401== HEAP SUMMARY:
==4401== in use at exit: 0 bytes in 0 blocks
==4401== total heap usage: 18 allocs, 18 frees, 1,648 bytes allocated
==4401==
==4401== All heap blocks were freed -- no leaks are possible
==4401==
==4401== For counts of detected and suppressed errors, rerun with: -v
==4401== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
HashMap
Trong khi vector lưu trữ giá trị bằng các chỉ mục số nguyên, thì HashMap
lưu trữ giá trị bằng key. Key của HashMap
có thể là booleans, integers, strings, hoặc bất kì kiểu dữ liệu nào khác mà được triển khai trait Eq
và Hash
. Chi tiết hơn về điều này sẽ được đề cập ở phần tiếp theo.
Tương tự như vector, HashMap
có thể mở rộng được, nhưng HashMaps cũng có thể thu nhỏ chính chúng khi chúng có quá nhiều không gian lưu trữ dư thừa. Bạn có thể tạo một HashMap với một dung lượng ban đầu nhất định bằng cách sử dụng HashMap::with_capacity(uint)
, hoặc sử dụng HashMap::new()
để tạo một HashMap với dung lượng khởi tạo mặc định(cách này được khuyến khích dùng).
use std::collections::HashMap; fn call(number: &str) -> &str { match number { "798-1364" => "We're sorry, the call cannot be completed as dialed. Please hang up and try again.", "645-7689" => "Hello, this is Mr. Awesome's Pizza. My name is Fred. What can I get for you today?", _ => "Hi! Who is this again?" } } fn main() { let mut contacts = HashMap::new(); contacts.insert("Daniel", "798-1364"); contacts.insert("Ashley", "645-7689"); contacts.insert("Katie", "435-8291"); contacts.insert("Robert", "956-1745"); // Lấy một tham chiếu và trả về Option<&V> match contacts.get(&"Daniel") { Some(&number) => println!("Calling Daniel: {}", call(number)), _ => println!("Don't have Daniel's number."), } // `HashMap::insert()` trả về `None` // nếu giá trị được thêm là mới, ngược lại trả về là `Some(value)` contacts.insert("Daniel", "164-6743"); match contacts.get(&"Ashley") { Some(&number) => println!("Calling Ashley: {}", call(number)), _ => println!("Don't have Ashley's number."), } contacts.remove(&"Ashley"); // `HashMap::iter()` trả về một bộ lặp (iterator) cho phép lấy ra từng cặp // (&'a key, &'a value) theo thứ tự bất kỳ. for (contact, &number) in contacts.iter() { println!("Calling {}: {}", contact, call(number)); } }
Để biết thêm thông tin về hashing và hash maps (đôi khi được gọi là hash tables), hãy xem Hash Table Wikipedia
Alternate/custom key types
Bất kì kiểu nào triển khai các trait Eq
và Hash
đều có thể là một key trong HashMap
. Bao gồm:
bool
(Mặc dù không hữu ích lằm vì chỉ có thể có hai key)int
,uint
và tất cả các biến thể của chúng.String
và&str
(protip: bạn có thể có HashMap được khóa bằngString
và gọi.get()
bằng&str
)
Lưu ý rằng f32
và f64
không triển khai Hash
, bời vì rất có thể Lỗi về độ chính xác trong dấu phẩy động
sẽ khiến việc sử dụng chúng làm các key hashmap rất dễ xảy ra lỗi.
Tất cả các lớp collection sẽ triển khai Eq
và Hash
nếu kiểu được chứa bên trong cũng được triển khai tương ứng Eq
và Hash
. Ví dụ, VecHash
nếu T triển khai Hash
.
Bạn có thể dễ dàng triển khai Eq
và Hash
cho một loại tùy chỉnh chỉ với một dòng: #[derive(PartialEq, Eq, Hash)]
Trình biên dịch sẽ làm phần còn lại. Nếu bạn muốn kiểm soát cụ thể hơn, bạn có thể tự triển khai Eq
và/hoặc Hash
. Hướng dẫn này không đề cập đến các chi tiết cụ thể của việc triển khai Hash
.
Để hiểu hơn về cách struct
được sử dụng trong HashMap
, hãy thử tạo một hệ thống đăng nhập đơn giản:
use std::collections::HashMap; // Eq yêu cầu bạn derive PartialEq trên type. #[derive(PartialEq, Eq, Hash)] struct Account<'a>{ username: &'a str, password: &'a str, } struct AccountInfo<'a>{ name: &'a str, email: &'a str, } type Accounts<'a> = HashMap<Account<'a>, AccountInfo<'a>>; fn try_logon<'a>(accounts: &Accounts<'a>, username: &'a str, password: &'a str){ println!("Username: {}", username); println!("Password: {}", password); println!("Attempting logon..."); let logon = Account { username, password, }; match accounts.get(&logon) { Some(account_info) => { println!("Successful logon!"); println!("Name: {}", account_info.name); println!("Email: {}", account_info.email); }, _ => println!("Login failed!"), } } fn main(){ let mut accounts: Accounts = HashMap::new(); let account = Account { username: "j.everyman", password: "password123", }; let account_info = AccountInfo { name: "John Everyman", email: "j.everyman@email.com", }; accounts.insert(account, account_info); try_logon(&accounts, "j.everyman", "psasword123"); try_logon(&accounts, "j.everyman", "password123"); }
HashSet
HashSet
được coi như là một HashMap
nếu chúng ta chỉ quan tâm đến các key( HashSet<T>
thực tế chỉ là một wrapper(bao bọc) xung quanh HashMap<T,()>
).
Bạn sẽ hỏi "Ý nghĩa của việc này là gì?". "Tôi có thể đơn giản là lưu trữ các key trong Vec".
Điểm đặc biệt của HashSet
là nó đảm bảo không có các phần tử trùng lặp. Đó là điều mà bất kì một collection set nào cũng đáp ứng.HashSet
chỉ là một triển khai của nó (collection). (Xem thêm: BTreeSet)
Nếu bạn thêm vào một giá trị đã tồn tại trong HashSet
, (Nghĩa là giá trị mới bằng với cũ và cả hai đều có cùng hàm hash), thì giá trị mới sẽ thay thế giá trị cũ.
Điều này thật hữu ích khi bạn không muốn có nhiều hơn một thứ gì đó hoặc khi bạn muốn biết liệu bạn đã có thứ gì đó chưa.
Nhưng các set còn có thể làm nhiều hơn thế. Các Set có 4 thao tác chính( tất cả các lệnh gọi sau đây đều trả về một iterator):
union
: lấy tất cả các phần tử chỉ xuất hiện một lần trong cả hai tập.difference
: lấy tất cả các phần tử có trong tập đầu tiên nhưng không có trong tập thứ hai.intersection
: lấy tất cả các phần tử chỉ xuất hiện trong cả hai tập.symmetric_difference
: lấy tất cả các phần tử chỉ xuất hiện trong tập này nhưng không xuất hiện trong tập kia.
Hãy thử tất cả những thao tác đó trong ví dụ sau:
use std::collections::HashSet; fn main() { let mut a: HashSet<i32> = vec![1i32, 2, 3].into_iter().collect(); let mut b: HashSet<i32> = vec![2i32, 3, 4].into_iter().collect(); assert!(a.insert(4)); assert!(a.contains(&4)); // `HashSet::insert()` trả về false nếu // đã có giá trị. assert!(b.insert(4), "Value 4 is already in set B!"); // FIXME ^ Comment out this line b.insert(5); // Nếu kiểu phần tử của một collection triển khai `Debug`, // thì collection đó cũng triển khai `Debug`. // Thông thường nó sẽ in các phần tử của nó theo định dạng [elem1, elem2, ...]` println!("A: {:?}", a); println!("B: {:?}", b); // In [1, 2, 3, 4, 5] theo thứ tự tùy ý println!("Union: {:?}", a.union(&b).collect::<Vec<&i32>>()); // Chỗ này sẽ in ra [1] println!("Difference: {:?}", a.difference(&b).collect::<Vec<&i32>>()); // In [2, 3, 4] theo thứ tự tùy ý. println!("Intersection: {:?}", a.intersection(&b).collect::<Vec<&i32>>()); // In [1, 5] println!("Symmetric Difference: {:?}", a.symmetric_difference(&b).collect::<Vec<&i32>>()); }
(Các ví dụ đã được điều chỉnh theo documentation.)
Rc
Khi ta cần có nhiều ownership, Rc
(Reference Counting - bộ đếm tham chiếu) có thể được sử dụng. Rc
sẽ theo dõi số lượng các tham chiếu, có nghĩa là số lượng các owners của một giá trị sẽ được chứa bên trong một Rc
.
Số lượng tham chiếu của một Rc
sẽ tăng lên 1 bất cứ khi nào Rc
đó bị sao chép (cloned), và giảm đi 1 khi có một bản sao của Rc
bị ra khỏi scope. Tại thời điểm mà số lượng tham chiếu của Rc
trở về 0 (nghĩa là không còn owners nào), cả Rc
và giá trị được tham chiếu sẽ bị huỷ.
Khi sao chép một Rc
sẽ không xảy ra deep copy. Nó chỉ tạo ra thêm một con trỏ khác đến giá trị được chứa bên trong và tăng thêm số lượng tham chiếu.
use std::rc::Rc; fn main() { let rc_examples = "Rc examples".to_string(); { println!("--- rc_a is created ---"); let rc_a: Rc<String> = Rc::new(rc_examples); println!("Reference Count of rc_a: {}", Rc::strong_count(&rc_a)); { println!("--- rc_a is cloned to rc_b ---"); let rc_b: Rc<String> = Rc::clone(&rc_a); println!("Reference Count of rc_b: {}", Rc::strong_count(&rc_b)); println!("Reference Count of rc_a: {}", Rc::strong_count(&rc_a)); // Hai `Rc` bằng nhau khi và chỉ khi các giá trị bên trong bằng nhau println!("rc_a and rc_b are equal: {}", rc_a.eq(&rc_b)); // Chúng ta có thể sử dụng các methods của giá trị bên trong một cách trực tiếp println!("Length of the value inside rc_a: {}", rc_a.len()); println!("Value of rc_b: {}", rc_b); println!("--- rc_b is dropped out of scope ---"); } println!("Reference Count of rc_a: {}", Rc::strong_count(&rc_a)); println!("--- rc_a is dropped out of scope ---"); } // Lỗi! `rc_examples` đã bị moved đến `rc_a` // Và sau đó khi `rc_a` bị huỷ, `rc_examples` cũng bị huỷ theo // println!("rc_examples: {}", rc_examples); // TODO ^ Hãy thử bỏ chú thích khỏi dòng này }
Đọc thêm:
std::rc and std::sync::arc.
Arc
Khi mà việc chia sẻ ownership giữa các luồng (thread) là cần thiết, ta có thể sử dụng
Arc
(Atomically Reference Counted). Struct này thông qua việc implement trait Clone
có thể tạo ra con trỏ tham chiếu (reference pointer) đến vị trí của một giá trị nằm trên heap và đồng thời làm tăng
bộ đếm tham chiếu (reference counter). Bởi vì nó chia sẻ ownership giữa các thread, nên khi
reference pointer cuối cùng của một biến bị ra khỏi scope, thì biến đó bị giải phóng.
use std::time::Duration; use std::sync::Arc; use std::thread; fn main() { // Phần khai báo biến dưới đây là nơi mà giá trị của nó được chỉ định. let apple = Arc::new("the same apple"); for _ in 0..10 { // Dưới đây không xảy ra sự gán giá trị vì đây là một con trỏ đến // một tham chiếu trên bộ nhớ heap. let apple = Arc::clone(&apple); thread::spawn(move || { // Vì sử dụng Arc, các thread con có thể được sinh ra sử dụng dữ liệu đã được cấp phát // ở tại vị trí con trỏ Arc trỏ đến. println!("{:?}", apple); }); } // Đảm bảo rằng tất cả các instances của Arc được in ra ở các thread con. thread::sleep(Duration::from_secs(1)); }
Std misc
Có nhiều kiểu khác được cung cấp bởi thư viện chuẩn std để hỗ trợ các thứ như:
- Threads
- Channels
- File I/O
Những kiểu này mở rộng chức năng hơn những gì các kiểu primitives cung cấp.
Xem thêm:
Threads
Rust cung cấp một cơ chế để sinh ra các luồng của hệ điều hành gốc thông qua hàm spawn
, đối số của hàm này là một bao đóng di động.
use std::thread; const NTHREADS: u32 = 10; // Đây là luồng `main` fn main() { // Tạo một vectơ để lưu trữ các luồng con được tạo ra let mut children = vec![]; for i in 0..NTHREADS { // Tạo ra 1 luồng khác children.push(thread::spawn(move || { println!("this is thread number {}", i); })); } for child in children { // Chờ một luồng kết thúc để trả về kết quả. let _ = child.join(); } }
Các luồng này sẽ được lên lịch bởi hệ điều hành.
Testcase: map-reduce
Rust khiến cho việc xử lý dữ liệu đa luồng trở nên rất dễ dàng mà không đối mặt với các rắc rối thường gặp khi xử lý so với các ngôn ngữ khác.
Thư viện tiêu chuẩn cung cấp các nguyên mẫu đa luồng tuyệt vời sẵn có. Kết hợp với khái niệm Sở hữu và quy tắc định danh của Rust, hiện tượng cạnh tranh dữ liệu sẽ tự động được ngăn chặn.
Các quy tắc định danh (một tham chiếu có thể ghi (writable reference) XOR nhiều tham chiếu chỉ đọc (readable references))
tự động ngăn bạn thay đổi trạng thái mà các luồng khác có thể nhìn thấy. (Khi cần đồng bộ hóa, ta có thể
dùng các nguyên tắc đồng bộ hóa như Mutex
hoặc Channel
.)
Trong ví dụ này, ta sẽ tính tổng của tất cả các chữ số trong một khối số. Ta sẽ làm điều này bằng cách chia khối thành các phần khác nhau trong các luồng khác nhau. Mỗi luồng sẽ tính tổng các chữ số trong khối nhỏ của nó, và sau đó ta sẽ tổng hợp các tổng được tạo ra bởi mỗi luồng.
Lưu ý rằng, mặc dù ta đang truyền tham chiếu qua ranh giới luồng, Rust hiểu rằng ta
chỉ truyền tham chiếu chỉ đọc(read-only) và do đó không có tình trạng không an toàn hoặc tranh chấp dữ liệu
nào xảy ra. Ngoài ra, vì tham chiếu ta đang truyền có tuổi thọ 'static
, Rust hiểu rằng dữ
liệu của ta sẽ không bị phá hủy trong khi các luồng này vẫn đang chạy.
(Khi cần chia sẻ dữ liệu không có tuổi thọ 'static
giữa các luồng, bạn có thể sử dụng con trỏ
thông minh như Arc
để giữ cho dữ liệu sống và tránh các tuổi thọ không phải là 'static
.)
use std::thread; // Luồng "main" fn main() { // Dữ liệu chúng ta sẽ xử lý // Chúng ta sẽ tính tổng của tất cả các chữ số thông qua thuật toán map-reduce trên nhiều luồng. // Mỗi phân đoạn được tách ra bằng dấu cách sẽ được xử lý trên một luồng khác nhau. // // TODO: hãy xem kết quả sẽ ra sao nếu bạn chèn thêm dấu cách! let data = "86967897737416471853297327050364959 11861322575564723963297542624962850 70856234701860851907960690014725639 38397966707106094172783238747669219 52380795257888236525459303330302837 58495327135744041048897885734297812 69920216438980873548808413720956532 16278424637452589860345374828574668"; // Tạo một vector để chứa các luồng con mà chúng ta sẽ khởi động. let mut children = vec![]; /************************************************************************* * Giai đoạn "Map" * * Chia dữ liệu của chúng ta thành các phân đoạn, và bắt đầu xử lý ************************************************************************/ // Tách dữ liệu của chúng ta thành các phân đoạn cho từng phần tính toán // Mỗi phân đoạn sẽ là một tham chiếu (&str) đến dữ liệu thực tế let chunked_data = data.split_whitespace(); // Chạy qua các phân đoạn dữ liệu. // .enumerate() thêm chỉ mục (index) của vòng lặp hiện tại vào bất cứ điều gì được chạy qua // bộ đôi kết quả "(chỉ mục, phần tử)" sau đó được tự động // "destructured" thành hai biến, "i" và "data_segment" với một // "destructuring assignment" for (i, data_segment) in chunked_data.enumerate() { println!("Phân đoạn dữ liệu {} là \"{}\"", i, data_segment); // Xử lý mỗi phân đoạn dữ liệu trong một luồng riêng biệt // // spawn() trả về một handle cho luồng mới, // mà chúng ta PHẢI giữ lại để truy cập giá trị trả về // // 'move || -> u32' là cú pháp cho một closure mà: // * không có đối số ('||') // * lấy sở hữu các biến bị chụp ('move') và // * trả về một số nguyên 32-bit không dấu ('-> u32') // // Rust đủ thông minh để có thể suy luận được '-> u32' // từ chính closure nên ta có thể bỏ nó đi. // // TODO: thử xóa 'move' và xem điều gì sẽ xảy ra children.push(thread::spawn(move || -> u32 { // Tính tổng trung gian của đoạn dữ liệu này: let result = data_segment // lặp qua các ký tự trong đoạn dữ liệu.. .chars() // chuyển ký tự sang kiểu số .map(|c| c.to_digit(10).expect("nên là chữ số")) // .. và tính tổng các số .sum(); // println! khóa stdout, để không có tình trạng văn bản xen kẽ nhau xảy ra println!("Đoạn dữ liệu {}, kết quả={}", i, result); // không cần "return", bởi vì Rust là một "ngôn ngữ biểu thức", // biểu thức được đánh giá cuối cùng trong mỗi khối sẽ tự động là giá trị của nó. result })); } /************************************************************************* * Giai đoạn "Reduce" * * Tổng hợp kết quả trung gian và kết hợp chúng thành kết quả cuối cùng ************************************************************************/ // Kết hợp kết quả trung gian của từng thread thành một tổng kết quả cuối cùng. // // chúng ta sử dụng "turbofish" ::<> để cung cấp cho sum() một gợi ý kiểu dữ liệu. // // TODO: hãy thử không sử dụng turbofish, thay vào đó chỉ rõ kiểu của final_result let final_result = children.into_iter().map(|c| c.join().unwrap()).sum::<u32>(); println!("Kết quả tổng cuối cùng: {}", final_result); }
Gán giá trị
Không nên để số lượng luồng phụ thuộc vào dữ liệu được nhập từ người dùng. Nếu người dùng quyết định nhập nhiều dấu cách, liệu chúng ta có muốn tạo ra 2,000 luồng không? Ta nên sửa đổi chương trình sao cho dữ liệu luôn được chia thành một số lượng nhỏ các phần, được xác định bởi một hằng số tĩnh ở đầu chương trình.
Xem thêm:
- Threads
- vectors và iterators
- closures, move semantics và
move
closures - destructuring gán giá trị
- turbofish notation để hỗ trợ suy luận kiểu
- unwrap vs. expect
- enumerate
Channels
Rust cung cấp các channels
bất đồng bộ để giao tiếp giữa các threads.
Channels cho phép một luồng thông tin hai chiều giữa hai điểm đầu cuối: Sender
và Receiver
.
use std::sync::mpsc::{Sender, Receiver}; use std::sync::mpsc; use std::thread; static NTHREADS: i32 = 3; fn main() { // Channels có hai điểm đầu cuối: `Sender<T>` và `Receiver<T>`, // trong đó `T` là kiểu của message được trao đổi // (ghi chú kiểu là dư thừa) let (tx, rx): (Sender<i32>, Receiver<i32>) = mpsc::channel(); let mut children = Vec::new(); for id in 0..NTHREADS { // Sender có thể được sao chép let thread_tx = tx.clone(); // Mỗi thread sẽ gửi id thông qua channel let child = thread::spawn(move || { // Thread dành quyền kiểm soát `thread_tx` // Mỗi thread thêm một message vào trong channel thread_tx.send(id).unwrap(); // Gửi message là một hành động non-blocking, thread sẽ tiếp tục // ngay lập tức sau khi gửi message println!("thread {} finished", id); }); children.push(child); } // Tại đây, tất cả các message được thu thập let mut ids = Vec::with_capacity(NTHREADS as usize); for _ in 0..NTHREADS { // Phương thức `recv` nhận từng message từ channel // `recv` sẽ block thread hiện tại nếu không có mesage nào có thể nhận ids.push(rx.recv()); } // Đợi cho các thread hoàn thành tất cả công việc còn lại for child in children { child.join().expect("oops! the child thread panicked"); } // Hiển thị thứ tự gửi đi của các message println!("{:?}", ids); }
Path
Cấu trúc Path
đại diện cho đường dẫn tới tệp tin trên hệ thống tệp (filesystem). Có hai loại Path
: posix::Path
dành cho các hệ thống UNIX, và windows::Path
dành cho hệ thống Windows. Prelude sẽ tự động thiết lập các phiên bản Path
phù hợp với nền tảng cụ thể.
Trong Rust, "Prelude" là một tập hợp các thư viện được tự động đưa vào phạm vi của một chương trình Rust mà không cần khai báo thêm. Tập hợp này bao gồm các thư viện cốt lõi của Rust cũng như một số thư viện phổ biến khác.
Một đối tượng Path
có thể được tạo ra từ một OsStr
, và cung cấp nhiều phương thức để lấy thông tin từ tệp tin/thư mục mà đường dẫn trỏ tới.
Path là một đối tượng không thể thay đổi (immutable
). Phiên bản Path
được sở hữu bởi (owned
) là PathBuf
. Mối quan hệ giữa Path
và PathBuf
tương tự như giữa str
và String
: một PathBuf
có thể được thay đổi trực tiếp (in-place), và có thể được giải tham chiếu (dereferenced) thành một Path
.
Lưu ý rằng một Path
không thể được biểu diễn bên dưới dạng một chuỗi UTF-8, mà sẽ được lưu trữ dưới dạng một OsString
. Do đó, không nên chuyển đổi một Path
thành một &str
vì tiến trình này rất dễ xảy ra lỗi (trả về một Option
). Tuy nhiên, một Path
có thể được tự do chuyển đổi thành một OsString
hoặc &OsStr
bằng cách sử dụng into_os_string
và as_os_str
."
use std::path::Path; fn main() { // Tạo một `Path` từ một `&'static str` let path = Path::new("."); // Phương thức `display` sẽ trả về một cấu trúc `Display`able let _display = path.display(); // `join` sẽ hợp nhất các phần của đường dẫn lại và thiết lập các dấu phân cách tùy thuộc vào hệ điều hành // sau đó sẽ trả về một `PathBuf` let mut new_path = path.join("a").join("b"); // `push` sẽ mở rộng `PathBuf` với một `&Path` new_path.push("c"); new_path.push("myfile.tar.gz"); // `set_file_name` sẽ cập nhật tên của tệp tại `PathBuf` new_path.set_file_name("package.tgz"); // Chuyển `PathBuf` sang một string slice (&str) match new_path.to_str() { None => panic!("new path is not a valid UTF-8 sequence"), Some(s) => println!("new path is {}", s), } }
Tùy vào trường hợp cụ thể, hãy chắc chắn rằng đã kiểm tra các phương thức khác của Path (posix::Path
hoặc windows::Path
- thuộc vào hệ điều hành) và cấu trúc Metadata - một đối tượng cung cấp thông tin khác ngoài nội dung của file hoặc thư mục.
Xem thêm:
File I/O
Cấu trúc File
đại diện cho một tập tin đã được mở (nó bao gồm một thành phần mô tả cho tập tin tập tin đó (file descriptor)),
và cung cấp quyền đọc và/hoặc ghi vào tập tin cơ sở.
Bởi vì có nhiều điều có thể xảy ra khi thực hiện I/O trên tập tin,
tất cả các phương thức của File
đều trả về kiểu io::Result<T>
,
đó là một tên viết tắt cho Result<T, io::Error>
.
Điều này khiến cho nguyên nhân thất bại của tất cả các hoạt động I/O trở nên rõ ràng(explicit). Nhờ điều này, lập trình viên có thể thấy được tất cả các đường dẫn file xảy ra lỗi và được khuyến khích xử lý chúng một cách chủ động.
open
Hàm open
có thể được sử dụng để mở một tập tin ở chế độ chỉ được đọc.
Một File
sở hữu một tài nguyên, đó là mô tả tập tin và đảm bảo file được đóng lại khi tài nguyên đó được giải phóng drop
.
use std::fs::File;
use std::io::prelude::*;
use std::path::Path;
fn main() {
// Tạo đường dẫn đến tập tin mong muốn
let path = Path::new("hello.txt");
let display = path.display();
// Mở đường dẫn theo chế độ chỉ đọc, trả về `io::Result<File>`
let mut file = match File::open(&path) {
Err(why) => panic!("couldn't open {}: {}", display, why),
Ok(file) => file,
};
// Đọc nội dung tập tin thành một chuỗi, trả về `io::Result<usize>`
let mut s = String::new();
match file.read_to_string(&mut s) {
Err(why) => panic!("couldn't read {}: {}", display, why),
Ok(_) => print!("{} contains:\n{}", display, s),
}
// `file` ra ngoài phạm vi, và tập tin sẽ bị đóng lại
}
Dưới đây là kết quả thành công mong đợi:
$ echo "Hello World!" > hello.txt
$ rustc open.rs && ./open
hello.txt contains:
Hello World!
(Khuyến khích bạn nên thử nghiệm ví dụ trên trong các điều kiện có thể xảy ra lỗi
khác nhau: hello.txt không tồn tại, hoặc hello.txt không đọc được, vv.)
create
Hàm create
sẽ mở tập tin ở chế độ chỉ ghi (write-only mode). Nếu tập tin này đã tồn tại, dữ liệu trong tập tin sẽ bị mất. Ở chiều ngược lại, nếu tập tin chưa tồn tại thì một tập tin mới sẽ được tạo ra.
static WHAT_IS_RUST: &str = "Rust là ngôn ngữ lập trình được tạo ra vào năm 2006 bởi Graydon Hoare như một dự án phụ khi đang là developer tại Mozilla. Rust pha trộn hiệu suất của các ngôn ngữ như C ++ với cú pháp thân thiện hơn, tập trung vào code an toàn và được thiết kế tốt giúp đơn giản hóa việc phát triển. Các phần của trình duyệt Firefox của Mozilla được viết bằng Rust và các nhà phát triển tại Microsoft được cho là sử dụng nó để mã hóa lại các phần của hệ điều hành Windows. "; use std::fs::File; use std::io::prelude::*; use std::path::Path; fn main() { let path = Path::new("what_is_rust.txt"); let display = path.display(); // Mở một file mới ở chế độ chỉ ghi, trả về `io::Result<File>` let mut file = match File::create(&path) { Err(why) => panic!("Không thể khởi tạo tập tin {}: {}", display, why), Ok(file) => file, }; // Ghi chuỗi `WHAT_IS_RUST` vào `file`, trả về `io::Result<()>` match file.write_all(WHAT_IS_RUST.as_bytes()) { Err(why) => panic!("Không thể ghi vào tập tin {}: {}", display, why), Ok(_) => println!("Ghi thành công vào tập tin {}", display), } }
Dưới đây sẽ là kết quả trả về trong trường hợp thực thi thành công:
$ rustc create.rs && ./create
Ghi thành công vào tập tin what_is_rust.txt
$ cat what_is_rust.txt
Rust là ngôn ngữ lập trình được tạo ra vào năm 2006 bởi Graydon Hoare như một dự án phụ khi đang là developer tại Mozilla. Rust pha trộn hiệu suất của các ngôn ngữ như C ++ với cú pháp thân thiện hơn, tập trung vào code an toàn và được thiết kế tốt giúp đơn giản hóa việc phát triển. Các phần của trình duyệt Firefox của Mozilla được viết bằng Rust và các nhà phát triển tại Microsoft được cho là sử dụng nó để mã hóa lại các phần của hệ điều hành Windows.
(Cũng tương tự như nhiều ví dụ trước đây, bạn nên thử lại ví dụ này trong các trường hợp xảy ra lỗi khác để hiểu rõ hơn về cách thức hoạt động của nó.)
Cấu trúc OpenOptions có thể được sử dụng để cấu hình cách một tập tin có thể được mở như thế nào.
read_lines
Một cách tiếp cận ngây thơ.
Đây có thể là một nỗ lực hợp lý đầu tiên cho việc đọc các dòng từ một tệp tin đối với một người mới bắt đầu.
#![allow(unused)] fn main() { use std::fs::read_to_string; fn read_lines(filename: &str) -> Vec<String> { let mut result = Vec::new(); for line in read_to_string(filename).unwrap().lines() { result.push(line.to_string()) } result } }
Vì phương thức lines()
trả về một bộ lặp qua các dòng trong tập tin,
chúng ta cũng có thể thực hiện một phép ánh xạ trực tiếp và thu thập các kết quả,
tạo ra một biểu thức ngắn gọn và trôi chảy hơn
#![allow(unused)] fn main() { use std::fs::read_to_string; fn read_lines(filename: &str) -> Vec<String> { read_to_string(filename) .unwrap() // panic khi có lỗi đọc tệp có thể xảy ra. .lines() // chia chuỗi thành một bộ lặp của các dòng .map(String::from) // chuyển mỗi phần của chuỗi thành một chuỗi mới .collect() // tập hợp chúng lại thành một vector } }
Lưu ý rằng trong cả hai ví dụ trên, chúng ta phải chuyển đổi tham chiếu &str
được trả về từ lines()
thành kiểu được sở hữu String
, sử dụng .to_string()
và
String::from
tương ứng.
Một cách tiếp cận hiệu quả hơn
Ở đây chúng ta truyền sở hữu của File
đã mở vào một struct BufReader
.
BufReader
sử dụng bộ đệm nội bộ để giảm thiểu các phân bổ trung gian.
Chúng ta cũng cập nhật read_lines
để trả về một bộ lặp thay vì
phân bổ các đối tượng String
mới trong bộ nhớ cho mỗi dòng.
use std::fs::File; use std::io::{self, BufRead}; use std::path::Path; fn main() { // Tệp tin hosts.txt phải tồn tại trong đường dẫn hiện tại. if let Ok(lines) = read_lines("./hosts.txt") { // Sử dụng iterator, trả về một Chuỗi (Tùy chọn) for line in lines { if let Ok(ip) = line { println!("{}", ip); } } } } // Kết quả được bọc trong một Result để cho phép phù hợp với các lỗi // Trả về một Iterator đến Reader của các dòng trong tập tin. fn read_lines<P>(filename: P) -> io::Result<io::Lines<io::BufReader<File>>> where P: AsRef<Path>, { let file = File::open(filename)?; Ok(io::BufReader::new(file).lines()) }
Chạy chương trình này đơn giản chỉ in các dòng một cách riêng lẻ.
$ echo -e "127.0.0.1\n192.168.0.1\n" > hosts.txt
$ rustc read_lines.rs && ./read_lines
127.0.0.1
192.168.0.1
(Lưu ý rằng vì File::open
mong đợi một generic AsRefread_lines()
với cùng một ràng buộc generic, sử dụng từ khóa where
.)
Quá trình này hiệu quả hơn việc tạo String
trong bộ nhớ với tất cả nội dung của tập tin.
Điều này đặc biệt có thể gây ra vấn đề hiệu suất khi làm việc với các tập tin lớn hơn.
Child processes
Cấu trúc process::Output
thể hiện kết quả trả về của một tiến trình con (child process) sau khi nó kết thúc. Trong khi đó cấu trúc process::Command
đóng vai trò là một trình xây dựng quá trình (process builder).
use std::process::Command; fn main() { // Khởi tạo một tiến trình con mới để thực thi lệnh `rustc --version` let output = Command::new("rustc") .arg("--version") .output().unwrap_or_else(|e| { panic!("Đã có lỗi xảy ra khi thực thi tiến trình: {}", e) }); if output.status.success() { let s = String::from_utf8_lossy(&output.stdout); print!("rustc: Thực thi thành công và giá trị stdout là:\n{}", s); } else { let s = String::from_utf8_lossy(&output.stderr); print!("rustc: Thực thi thất bại và giá trị stderr là:\n{}", s); } }
(Bạn nên thử lại ví dụ này trong các trường hợp xảy ra lỗi khác để hiểu rõ hơn về cách thức hoạt động của nó bằng cách truyền một cờ không chính xác tới rustc
- Rust Compiler.)
Pipes
std::Child
struct đại diện cho một tiến trình con đang chạy, và đưa ra các điều khiển
stdin
, stdout
và stderr
để tương tác với tiến trình thông qua pipes.
use std::io::prelude::*;
use std::process::{Command, Stdio};
static PANGRAM: &'static str =
"the quick brown fox jumped over the lazy dog\n";
fn main() {
// Khởi tạo câu lệnh `wc`
let process = match Command::new("wc")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn() {
Err(why) => panic!("couldn't spawn wc: {}", why),
Ok(process) => process,
};
// Truyền một chuỗi vào `stdin` của `wc`.
//
// `stdin` có kiểu là `Option<ChildStdin>`, nhưng vì chúng ta biết chắc rằng có một thể hiện của stdin
// tồn tại trong process, nên chúng ta có thể `unwrap` nó một cách trực tiếp.
match process.stdin.unwrap().write_all(PANGRAM.as_bytes()) {
Err(why) => panic!("couldn't write to wc stdin: {}", why),
Ok(_) => println!("sent pangram to wc"),
}
// Bởi vì `stdin` không tồn tại sau khi được gọi ở trên,
// nó được loại bỏ và pipe được đóng lại
//
// Điều này rất quan trọng, nếu không thì `wc` sẽ không bắt đầu xử lí dữ liệu được nhập vào
// `stdout` cũng có kiểu là `Option<ChildStdout>` nên cũng phải được unwrap.
let mut s = String::new();
match process.stdout.unwrap().read_to_string(&mut s) {
Err(why) => panic!("couldn't read wc stdout: {}", why),
Ok(_) => print!("wc responded with:\n{}", s),
}
}
Wait
Nếu bạn muốn đợi một process::Child
kết thúc, thì phải gọi
Child::wait
, nó sẽ trả về một process::ExitStatus
.
use std::process::Command;
fn main() {
let mut child = Command::new("sleep").arg("5").spawn().unwrap();
let _result = child.wait().unwrap();
println!("reached end of main");
}
$ rustc wait.rs && ./wait
# `wait` tiếp tục chạy thêm 5 giây cho tới khi câu lệnh `sleep 5` kết thúc
reached end of main
Filesystem Operations
Module std::fs
chứa một số hàm liên quan đến hệ thống tập tin.
use std::fs;
use std::fs::{File, OpenOptions};
use std::io;
use std::io::prelude::*;
use std::os::unix;
use std::path::Path;
//Một triển khai đơn giản của `% cat path`
fn cat(path: &Path) -> io::Result<String> {
let mut f = File::open(path)?;
let mut s = String::new();
match f.read_to_string(&mut s) {
Ok(_) => Ok(s),
Err(e) => Err(e),
}
}
// Một triển khai đơn giản của `% echo s > path`
fn echo(s: &str, path: &Path) -> io::Result<()> {
let mut f = File::create(path)?;
f.write_all(s.as_bytes())
}
// Một triển khai đơn giản của `% touch path` (bỏ qua các tệp hiện có)
fn touch(path: &Path) -> io::Result<()> {
match OpenOptions::new().create(true).write(true).open(path) {
Ok(_) => Ok(()),
Err(e) => Err(e),
}
}
fn main() {
println!("`mkdir a`");
// Tạo một thư mục, trả về `io::Result<()>`
match fs::create_dir("a") {
Err(why) => println!("! {:?}", why.kind()),
Ok(_) => {},
}
println!("`echo hello > a/b.txt`");
//Phương thức `unwrap_or_else` có thể được sử dụng để rút gọn cú pháp của match trước đó
echo("hello", &Path::new("a/b.txt")).unwrap_or_else(|why| {
println!("! {:?}", why.kind());
});
println!("`mkdir -p a/c/d`");
// Đệ quy tạo một thư mục, trả về returns `io::Result<()>`
fs::create_dir_all("a/c/d").unwrap_or_else(|why| {
println!("! {:?}", why.kind());
});
println!("`touch a/c/e.txt`");
touch(&Path::new("a/c/e.txt")).unwrap_or_else(|why| {
println!("! {:?}", why.kind());
});
println!("`ln -s ../b.txt a/c/b.txt`");
// Create a symbolic link, returns `io::Result<()>`
if cfg!(target_family = "unix") {
unix::fs::symlink("../b.txt", "a/c/b.txt").unwrap_or_else(|why| {
println!("! {:?}", why.kind());
});
}
println!("`cat a/c/b.txt`");
match cat(&Path::new("a/c/b.txt")) {
Err(why) => println!("! {:?}", why.kind()),
Ok(s) => println!("> {}", s),
}
println!("`ls a`");
// Đọc nội dung của một thư mục, trả về `io::Result<Vec<Path>>`
match fs::read_dir("a") {
Err(why) => println!("! {:?}", why.kind()),
Ok(paths) => for path in paths {
println!("> {:?}", path.unwrap().path());
},
}
println!("`rm a/c/e.txt`");
// Xóa một tập tin, trả về `io::Result<()>`
fs::remove_file("a/c/e.txt").unwrap_or_else(|why| {
println!("! {:?}", why.kind());
});
println!("`rmdir a/c/d`");
// Xóa một thư mục trống, trả về `io::Result<()>`
fs::remove_dir("a/c/d").unwrap_or_else(|why| {
println!("! {:?}", why.kind());
});
}
Dưới đây là kết quả thành công như mong đợi:
$ rustc fs.rs && ./fs
`mkdir a`
`echo hello > a/b.txt`
`mkdir -p a/c/d`
`touch a/c/e.txt`
`ln -s ../b.txt a/c/b.txt`
`cat a/c/b.txt`
> hello
`ls a`
> "a/b.txt"
> "a/c"
`rm a/c/e.txt`
`rmdir a/c/d`
Và trạng thái cuối cùng của thư mục a
là:
$ tree a
a
|-- b.txt
`-- c
`-- b.txt -> ../b.txt
1 directory, 2 files
Một cách khác để định nghĩa hàm cat
là sử dụng ký hiệu ?
:
fn cat(path: &Path) -> io::Result<String> {
let mut f = File::open(path)?;
let mut s = String::new();
f.read_to_string(&mut s)?;
Ok(s)
}
Xem thêm:
Program arguments
Thư viện tiêu chuẩn
Các đối số dòng lệnh có thể truy cập bằng cách sử dụng std::env::args
,
nó trả về một bộ lặp (iterator) mà mỗi lần lặp lại nó sẽ cung cấp một String
cho mỗi đối số:
use std::env; fn main() { let args: Vec<String> = env::args().collect(); // Đối số đầu tiên là đường dẫn được sử dụng để gọi chương trình. println!("Đường dẫn của tôi là {}.", args[0]); // Các đối số còn lại là các tham số được truyền từ dòng lệnh. // Gọi chương trình như sau: // $ ./args arg1 arg2 println!("Tôi nhận được {:?} đối số: {:?}.", args.len() - 1, &args[1..]); }
$ ./args 1 2 3
Đường dẫn của tôi là ./args.
Tôi nhận được 3 đối số: ["1", "2", "3"].
Crates
Một cách khác là sử dụng các crate để cung cấp các chức năng bổ sung khi tạo ứng dụng dòng lệnh.
Rust Cookbook trình bày các quy tắc tốt nhất về cách sử dụng một trong những crate
đối số dòng lệnh phổ biến nhất, là clap
.
Argument parsing
Matching (match
) có thể được sử dụng để phân tích cú pháp (parse) các tham số đơn giản (simple arguments)
use std::env; fn increase(number: i32) { println!("{}", number + 1); } fn decrease(number: i32) { println!("{}", number - 1); } fn help() { // Hiển thị thông báo hướng dẫn sử dụng: // Có 2 tính năng chính: // 1. Kiểm tra xem giá trị truyền vào có phải là number và là đáp án hay không, trong ví dụ là số 42 // 2. Tăng hoặc giảm giá trị truyền vào 1 đơn vị println!("usage: match_args <string> Kiểm tra xem giá trị truyền vào có đúng là đáp án hay không. match_args {{increase|decrease}} <integer> Tăng hoặc giảm giá trị truyền vào một đơn vị."); } fn main() { let args: Vec<String> = env::args().collect(); match args.len() { // Không có tham số nào được truyền vào 1 => { println!("Tên tôi là 'match_args'. Hãy thử truyền thêm các tham số vào câu lệnh!"); }, // Truyền vào 1 tham số đầu tiên 2 => { match args[1].parse() { Ok(42) => println!("Đáp án chính xác!"), _ => println!("Đây không phải đáp án."), } }, // 1 câu lệnh và 1 tham số được truyền vào 3 => { let cmd = &args[1]; let num = &args[2]; // Phân giải giá trị truyền vào -> check xem có phải number không let number: i32 = match num.parse() { Ok(n) => { n }, Err(_) => { eprintln!("Lỗi: Tham số truyền vào không phải là số nguyên (interger)"); help(); return; }, }; // Phân giải câu lệnh, kiểm tra tính đúng đắn match &cmd[..] { "increase" => increase(number), "decrease" => decrease(number), _ => { eprintln!("Lỗi: Câu lệnh không hợp lệ"); help(); }, } }, // Các trường hợp khác _ => { // Hiển thị thông báo hướng dẫn help(); } } }
$ ./match_args Rust
Đây không phải đáp án.
$ ./match_args 42
Đáp án chính xác!
$ ./match_args do something
Lỗi: Tham số truyền vào không phải là số nguyên (interger)
usage:
match_args <string>
Kiểm tra xem giá trị truyền vào có đúng là đáp án hay không.
match_args {increase|decrease} <integer>
Tăng hoặc giảm giá trị truyền vào một đơn vị.
$ ./match_args do 42
error: invalid command
usage:
match_args <string>
Kiểm tra xem giá trị truyền vào có đúng là đáp án hay không.
match_args {increase|decrease} <integer>
Tăng hoặc giảm giá trị truyền vào một đơn vị.
$ ./match_args increase 42
43
Foreign Function Interface
Rust cung cấp một Foreign Function Interface (FFI) tới các thư viện C. Các
hàm ngoại lai phải được khai báo trong một khối extern
được ghi chú với một
thuộc tính #[link]
chứa tên của thư viện bên ngoài.
use std::fmt;
// khối extern này liên kết tới thư viện libm
#[link(name = "m")]
extern {
// đây là một hàm ngoại lai tính căn bậc hai của một số phức
fn csqrtf(z: Complex) -> Complex;
fn ccosf(z: Complex) -> Complex;
}
// Khi việc gọi một hàm ngoại lai được coi là không an toàn,
// các wrapper sẽ được sử dụng để bọc các hàm đó.
fn cos(z: Complex) -> Complex {
unsafe { ccosf(z) }
}
fn main() {
// z = -1 + 0i
let z = Complex { re: -1., im: 0. };
// gọi một hàm ngoại lai là hành động không an toàn
let z_sqrt = unsafe { csqrtf(z) };
println!("the square root of {:?} is {:?}", z, z_sqrt);
// gọi một API an toàn bọc xung quanh một hành động không an toàn
println!("cos({:?}) = {:?}", z, cos(z));
}
// Một thiết lập tối thiểu của số phức
#[repr(C)]
#[derive(Clone, Copy)]
struct Complex {
re: f32,
im: f32,
}
impl fmt::Debug for Complex {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
if self.im < 0. {
write!(f, "{}-{}i", self.re, -self.im)
} else {
write!(f, "{}+{}i", self.re, self.im)
}
}
}
Kiểm thử (Testing)
Rust là một ngôn ngữ lập trình rất chú trọng đến tính đúng đắn và nó bao gồm sẵn các công cụ hỗ trợ cho việc viết các bài kiểm thử phần mềm.
Kiểm thử có 3 kiểu:
- Unit testing.
- Doc testing.
- Integration testing.
Ngoài ra Rust cũng hỗ trợ việc chỉ định các dependencies để phục vụ cho việc test:
Đọc thêm
- Chương về kiểm thử trong The Book
- Hướng dẫn về doc-testing trong API Guidelines
Kiểm thử đơn vị (Unit testing)
Các bài tests trong Rust là các hàm để kiểm tra xem các đoạn code chức năng có hoạt động giống như mong đợi hay không. Phần thân của các hàm test thường sẽ thực hiên một vài cài đặt, thực thi đoạn code mà chúng ta muốn kiểm thử, sau đó khẳng định xem kết quả thực thi có giống như chúng ta mong đợi hay không.
Hầu hết các bài unit tests thường sẽ đặt trong tests
mod với #[cfg(test)]
attribute.
Các hàm tests được đánh dấu bằng thuộc tính #[test]
.
Các bài tests sẽ thất bại khi có bất kì đoạn nào bên trong hàm test panics. Dưới đây là một số macros hỗ trợ:
assert!(expression)
- panics nếu có biểu thức được đánh giá làfalse
.assert_eq!(left, right)
vàassert_ne!(left, right)
- tương ứng với kiểm tra tính bằng nhau và khác nhau của biểu thức trái và phải.
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
// Dưới đây là một hàm cộng bị sai, mục đích nó là để chạy lỗi
// trong ví dụ này.
#[allow(dead_code)]
fn bad_add(a: i32, b: i32) -> i32 {
a - b
}
#[cfg(test)]
mod tests {
// Lưu ý kỹ thuật hữu ích này: import các module khác từ scope bên ngoài (để dùng cho các mod tests).
use super::*;
#[test]
fn test_add() {
assert_eq!(add(1, 2), 3);
}
#[test]
fn test_bad_add() {
// Macro assert sau sẽ khởi chạy và đoạn test sẽ thất bại
// Hãy lưu ý rằng, các hàm private cũng có thể được test!
assert_eq!(bad_add(1, 2), 3);
}
}
Các bài tests có thể được thực thi bằng lệnh cargo test
.
$ cargo test
running 2 tests
test tests::test_bad_add ... FAILED
test tests::test_add ... ok
failures:
---- tests::test_bad_add stdout ----
thread 'tests::test_bad_add' panicked at 'assertion failed: `(left == right)`
left: `-1`,
right: `3`', src/lib.rs:21:8
note: Run with `RUST_BACKTRACE=1` for a backtrace.
failures:
tests::test_bad_add
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out
Các bài tests và ?
Không có ví dụ về unit test nào phía trên có kiểu trả về. Tuy nhiên ở phiên bản Rust 2018,
các bài unit tests của bạn đã có thể trả về Return<()>
, thứ mà cho phép ta sử dụng ?
bên trong nó! Điều này
có thể khiến cho các bài tests trở nên ngắn gọn hơn.
fn sqrt(number: f64) -> Result<f64, String> { if number >= 0.0 { Ok(number.powf(0.5)) } else { Err("negative floats don't have square roots".to_owned()) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_sqrt() -> Result<(), String> { let x = 4.0; assert_eq!(sqrt(x)?.powf(2.0), x); Ok(()) } }
Đọc "The Edition Guide" để biết thêm thông tin chi tiết.
Testing panics (kiểm thử các panics)
Để kiểm tra các hàm nên panic trong một số trường hợp nhất định, sử dụng thuộc tính
#[should_panic]
. Thuộc tính này chấp nhận đối số tùy chọn expected =
với giá trị là
thông điệp khi panic. Nếu hàm của bạn có thể panic theo nhiều cách khác nhau, điều này giúp
đảm bảo rằng bài test đang kiểm thử chính xác lỗi panic.
pub fn divide_non_zero_result(a: u32, b: u32) -> u32 {
if b == 0 {
panic!("Divide-by-zero error");
} else if a < b {
panic!("Divide result is zero");
}
a / b
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_divide() {
assert_eq!(divide_non_zero_result(10, 2), 5);
}
#[test]
#[should_panic]
fn test_any_panic() {
divide_non_zero_result(1, 0);
}
#[test]
#[should_panic(expected = "Divide result is zero")]
fn test_specific_panic() {
divide_non_zero_result(1, 10);
}
}
Khi chạy các bài tests ta thu được:
$ cargo test
running 3 tests
test tests::test_any_panic ... ok
test tests::test_divide ... ok
test tests::test_specific_panic ... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Doc-tests tmp-test-should-panic
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Thực hiện các bài tests cụ thể
Để chạy các bài tests cụ thể, ta có thể sẽ phải chỉ định tên của bài test với lệnh cargo test
.
$ cargo test test_any_panic
running 1 test
test tests::test_any_panic ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out
Doc-tests tmp-test-should-panic
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Để chạy nhiều bài tests, ta có thể sẽ phải chỉ định một phần của tên bài test khớp với tất cả bài tests mà ta muốn chạy.
$ cargo test panic
running 2 tests
test tests::test_any_panic ... ok
test tests::test_specific_panic ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out
Doc-tests tmp-test-should-panic
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Phớt lờ (Ignoring) các bài tests
Các bài tests có thể được đánh dấu bằng thuộc tính #[ignore]
để có thể bỏ qua một vài bài tests. Hoặc là chạy
các tests đó với lệnh cargo test -- --ignored
.
#![allow(unused)] fn main() { pub fn add(a: i32, b: i32) -> i32 { a + b } #[cfg(test)] mod tests { use super::*; #[test] fn test_add() { assert_eq!(add(2, 2), 4); } #[test] fn test_add_hundred() { assert_eq!(add(100, 2), 102); assert_eq!(add(2, 100), 102); } #[test] #[ignore] fn ignored_test() { assert_eq!(add(0, 0), 0); } } }
$ cargo test
running 3 tests
test tests::ignored_test ... ignored
test tests::test_add ... ok
test tests::test_add_hundred ... ok
test result: ok. 2 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out
Doc-tests tmp-ignore
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
$ cargo test -- --ignored
running 1 test
test tests::ignored_test ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Doc-tests tmp-ignore
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Kiểm thử tài liệu (document testing)
Cách cơ bản nhất để tài liệu hoá một dự án Rust là thông qua việc chú thích ngay trên mã nguồn. Các chú thích tài liệu được viết theo chuẩn CommonMark Markdown specification và có hỗ trợ các khối code bên trong những chú thích đó. Rust sẽ quan tâm đến tính đúng đắn, do đó những khối code này sẽ được biên dịch và sử dụng như các bài kiểm thử đối với tài liệu (documentation tests)
/// Dòng đầu tiên là tóm tắt ngắn mô tả hàm.
///
/// Những dòng tiếp theo trình bày tài liệu chi tiết. Các khối code thường bắt đầu với
/// ba dấu nháy ngược (```) và sẽ tồn tại mặc định một cách không tường minh hàm `fn main()` bên trong
/// và `extern crate <cratename>`. Giả sử là chúng ta đang test crate `doccomments`:
///
/// ```
/// let result = doccomments::add(2, 3);
/// assert_eq!(result, 5);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
/// Thường các chú thích tài liệu sẽ bao gồm các phần "Examples", "Panics" và "Failures"
///
/// Hàm tiếp theo sẽ thực hiện phép chia hai số
///
/// # Examples
///
/// ```
/// let result = doccomments::div(10, 2);
/// assert_eq!(result, 5);
/// ```
///
/// # Panics
///
/// Hàm sẽ panic nếu đối số thứ hai là 0
///
/// ```rust,should_panic
/// // panic xảy ra khi chia cho số 0
/// doccomments::div(10, 0);
/// ```
pub fn div(a: i32, b: i32) -> i32 {
if b == 0 {
panic!("Divide-by-zero error");
}
a / b
}
Các khối code bên trong docs sẽ được kiểm thử một cách tự động
khi ta chạy lệnh cargo test
thông thường:
$ cargo test
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Doc-tests doccomments
running 3 tests
test src/lib.rs - add (line 7) ... ok
test src/lib.rs - div (line 21) ... ok
test src/lib.rs - div (line 31) ... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Động lực đằng sau các bài documentation tests
Mục đích chủ yếu của documentation tests là phục vụ như là các ví dụ minh họa cho
chức năng, đó là một trong những nguyên tắc quan trọng nhất
guidelines. Điều này cho phép sử dụng các ví dụ đưa ra từ trong docs
như là một đoạn code hoàn chỉnh. Tuy nhiên việc sử dụng ?
khiến cho việc biên dịch bị lỗi vì main
trả về unit
. Khả năng ẩn đi một số dòng mã nguồn trong tài liệu
sẽ là cứu tinh trong trường hợp này: ta có thể viết fn try_main() -> Result<(), ErrorType>
, ẩn nó đi
và unwrap
nó bên trong hàm main
đã bị ẩn. Nghe có vẻ phức tạp nhỉ? Sau đây là một ví dụ:
/// Sử dụng hàm `try_main` đã được ẩn trong các bài doc tests
///
/// ```
/// # // các dòng bị ẩn sẽ bắt đầu với kí hiệu `#`, tuy nhiên chúng vẫn được biên dịch!
/// # fn try_main() -> Result<(), String> { // dòng này sẽ bao lại quanh phần thân sẽ được hiển thị bên trong docs
/// let res = doccomments::try_div(10, 2)?;
/// # Ok(()) // trả về từ hàm try_main
/// # }
/// # fn main() { // Bắt đầu một hàm main mà nó sẽ thực thi unwrap()
/// # try_main().unwrap(); // gọi hàm try_main và thực thi việc unwrap
/// # // nhờ đó test sẽ panic khi gặp lỗi
/// # }
/// ```
pub fn try_div(a: i32, b: i32) -> Result<i32, String> {
if b == 0 {
Err(String::from("Divide-by-zero"))
} else {
Ok(a / b)
}
}
Đọc thêm
- RFC505 về các phong cách ghi tài liệu
- API Guidelines về các hướng dẫn ghi chú tài liệu
Kiểm thử tích hợp (Integration testing)
Các bài Unit tests thường kiểm thử từng module một tách biệt nhau: chúng thường nhỏ và có thể kiểm thử các đoạn private code1. Các bài integration tests thực hiện bên ngoài crate của bạn và chỉ sử dụng các public interface của crate đó như bất kỳ mã nguồn nào khác. Mục đích của chúng là kiểm tra xem các phần trong thư viện của bạn có hoạt động đúng cách với nhau hay không.
Cargo sẽ tìm những bài integration tests ở thư mục tests
gần bên cạnh thư mục src
.
File src/lib.rs
:
// Định nghĩa crate này tên là `adder`
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
File chứa bài test: tests/integration_test.rs
:
#[test]
fn test_add() {
assert_eq!(adder::add(3, 2), 5);
}
Thực thi các bài tests với lệnh cargo test
:
$ cargo test
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Running target/debug/deps/integration_test-bcd60824f5fbfe19
running 1 test
test test_add ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Doc-tests adder
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Mỗi file Rust bên trong thư mục tests
sẽ được biên dịch y như hệt một crate riêng biệt. Để mà có thể
chia sẻ các đoạn code giữa các bài integration tests ta có thể tạo ra một module với các hàm public,
import và sử dụng các hàm đó bên trong các bài tests.
File tests/common/mod.rs
:
pub fn setup() {
// một vài đoạn code của hàm setup, ví dụ như tạo ra các files/thư mục cần thiết,
// khởi chạy các servers, ...
}
File chứa bài test: tests/integration_test.rs
// import module common.
mod common;
#[test]
fn test_add() {
// sử dụng code của module common.
common::setup();
assert_eq!(adder::add(3, 2), 5);
}
Việc tạo module giống như là tests/common.rs
cũng sẽ hoạt động tương tự, nhưng cách làm này không được khuyến khích
vì công cụ thực thi test sẽ coi như file đó cũng là một crate để test và sẽ cố gắng chạy các bài tests bên trong nó.
Chú thích người dịch: private code là các các đoạn code không được khai báo với từ khóa là pub. Những đoạn code này không được truy cập từ bên ngoài module chứa chúng, mà chỉ có thể được sử dụng bên trong cùng một module đó
Development dependencies (dependencies dùng trong quá trình dev)
Thỉnh thoảng chúng ta sẽ có nhu cầu cài đặt các dependencies chỉ để cho test (hoặc viết các ví dụ, hoặc đo lường (benchmarks)).
Những dependencies như vậy được thêm vào Cargo.toml
ở phần [dev-dependencies]
. Những dependencies này sẽ không được
truyền tải đến các package khác phụ thuộc vào package hiện tại.
Một ví dụ điển hình là pretty_assertions
, thứ giúp mở rộng các macro tiêu chuẩn như assert_eq!
và assert_ne!
để cung cấp các phép so sánh dễ dàng quan sát với nhiều màu sắc.
File Cargo.toml
:
# dữ liệu về crate tiêu chuẩn được bỏ qua
[dev-dependencies]
pretty_assertions = "1"
File src/lib.rs
:
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq; // crate chỉ phục vụ cho mục đích test. Không thể dùng trong các đoạn code không phải test
#[test]
fn test_add() {
assert_eq!(add(2, 3), 5);
}
}
Đọc thêm
Cargo tài liệu về việc chỉ định dependencies.
Những hoạt động không an toàn
Như một lời giới thiệu cho phần này được mượn từ tài liệu chính thức, "hãy cố gắng giảm thiểu số lượng mã không an toàn trong khi viết mã." Với ý nghĩ đó, chúng ta hãy bắt đầu! Các chú thích không an toàn trong Rust được sử dụng để bỏ qua các biện pháp bảo vệ do trình biên dịch đưa ra; cụ thể, có bốn thao tác chính mà unsafe được sử dụng:
- dereferencing con trỏ thô
- gọi hàm hoặc phương thức
không an toàn
(bao gồm gọi hàm qua FFI, xem chương trước của sách) - truy cập hoặc sửa đổi các biến tĩnh có thể thay đổi
- thực hiện các đặc điểm không an toàn
Con trỏ thô
Con trỏ thô *
và tham chiếu &T
hoạt động tương tự nhau, nhưng các tham chiếu luôn an toàn vì chúng được đảm bảo trỏ đến dữ liệu hợp lệ nhờ trình kiểm tra mượn. Chỉ có thể thực hiện một con trỏ thô bên trong một block không an toàn.
fn main() { let raw_p: *const u32 = &10; unsafe { assert!(*raw_p == 10); } }
Gọi các hàm không an toàn
Một số hàm có thể được khai báo là không an toàn
, nghĩa là lập trình viên có trách nhiệm đảm bảo tính chính xác của nó thay vì của trình biên dịch. Một ví dụ về điều này là std::slice::from_raw_parts
sẽ tạo một đoạn dữ liệu mà dựa trên con trỏ tới phần tử đầu tiên và độ dài mong muốn.
use std::slice; fn main() { let some_vector = vec![1, 2, 3, 4]; let pointer = some_vector.as_ptr(); let length = some_vector.len(); unsafe { let my_slice: &[u32] = slice::from_raw_parts(pointer, length); assert_eq!(some_vector.as_slice(), my_slice); } }
Đối với slice::from_raw_parts
, một trong những giả định phải được duy trì là con trỏ được truyền vào phải trỏ đến vùng nhớ hợp lệ và bộ nhớ được trỏ tới là đúng kiểu. Nếu những giả định này không được duy trì thì hành vi của chương trình sẽ không được xác định và không biết điều gì sẽ xảy ra.
Inline assembly
Rust cung cấp hỗ trợ cho việc sử dụng inline assembly thông qua macro asm!
.
Nó có thể được dùng để nhúng mã assembly được viết bằng tay vào mã assembly được tạo ra bởi trình biên dịch.
Thường thì không cần phải dùng đến nó, nhưng nó có thể được dùng để đạt được hiệu năng hoặc thời gian thực thi mong muốn. Truy cập các thành phần cấp thấp của phần cứng, ví dụ trong kernel code, cũng có thể yêu cầu sử dụng tính năng này.
Ghi chú: các ví dụ ở đây được viết bằng assembly x86/x86-64, nhưng các kiến trúc khác cũng được hỗ trợ.
Inline assembly hiện được hỗ trợ trên các kiến trúc sau:
- x86 and x86-64
- ARM
- AArch64
- RISC-V
Cách dùng cơ bản
Hãy cùng bắt đầu với ví dụ đơn giản nhất:
#![allow(unused)] fn main() { #[cfg(target_arch = "x86_64")] { use std::arch::asm; unsafe { asm!("nop"); } } }
Đoạn mã sẽ chèn một lệnh NOP
(no operation) vào mã assembly được tạo ra bởi trình biên dịch.
Lưu ý rằng tất cả các lệnh asm!
phải được đặt trong một khối unsafe
, vì chúng có thể chèn các lệnh bất kỳ và làm phá hỏng chương trình của bạn. Các lệnh cần chèn vào được liệt kê trong tham số đầu tiên của macro asm!
dưới dạng một chuỗi ký tự.
Các đầu vào và đầu ra
Việc chèn thêm một lệnh mà không gì cả thì không có ý nghĩa gì. Hãy thử làm một chút điều gì đó có thể tác động lên dữ liệu:
#![allow(unused)] fn main() { #[cfg(target_arch = "x86_64")] { use std::arch::asm; let x: u64; unsafe { asm!("mov {}, 5", out(reg) x); } assert_eq!(x, 5); } }
Đoạn mã này sẽ ghi giá trị 5
vào biến u64
x
.
Bạn có thể thấy rằng chuỗi ký tự chúng ta sử dụng để chỉ định các lệnh thực sự là một chuỗi mẫu.
Nó được quản lý theo cùng quy tắc với chuỗi định dạng của Rust.
Các đối số được chèn vào chuỗi mẫu trông có vẻ khác một chút so với cái bạn đã có thể quen thuộc. Đầu tiên chúng ta cần chỉ định biến là đầu vào hay đầu ra của inline assembly. Trong trường hợp này nó là đầu ra. Chúng ta đã khai báo điều này bằng cách viết out
.
Chúng ta cũng cấn chỉ định kiểu của thanh ghi mà assembly mong đợi biến đó. Trong trường hợp này chúng ta đặt nó trong một thanh ghi bất kỳ bằng cách chỉ định reg
.
Trình biên dịch sẽ chọn một thanh ghi phù hợp để chèn vào mẫu và sẽ đọc biến từ đó sau khi inline assembly thực thi xong.
Cùng xem xét một ví dụ khác sử dụng đầu vào:
#![allow(unused)] fn main() { #[cfg(target_arch = "x86_64")] { use std::arch::asm; let i: u64 = 3; let o: u64; unsafe { asm!( "mov {0}, {1}", "add {0}, 5", out(reg) o, in(reg) i, ); } assert_eq!(o, 8); } }
Ví dụ này sẽ thêm 5
vào biến i
và ghi kết quả vào biến o
.
Cách assembly thực hiện điều này là đầu tiên sao chép giá trị từ i
đến đầu ra, và sau đó thêm 5
vào nó.
Ví dụ cho thấy một số điều:
Đầu tiên, chúng ta có thể thấy rằng asm!
cho phép sử dụng nhiều chuỗi mẫu; mỗi chuỗi được xem như một dòng mã assembly riêng rẽ, giống như nó được nối với nhau bởi dấu xuống dòng. Điều này làm cho việc định dạng mã assembly dễ dàng hơn.
Thứ hai, chúng ta có thể thấy rằng các đầu vào được khai báo bằng cách viết in
thay vì out
.
Thứ ba, chúng ta có thể thấy rằng chúng ta có thể chỉ định một số thứ khác như số thứ tự của đối số, hoặc tên như trong bất kỳ chuỗi định dạng nào. Đối với các mẫu inline assembly, điều này đặc biệt hữu ích vì các đối số thường được sử dụng nhiều lần. Đối với inline assembly phức tạp hơn, sử dụng cơ chế này được khuyến khích, vì nó cải thiện khả năng đọc và cho phép sắp xếp lại các lệnh mà không thay đổi thứ tự đối số.
Chúng ta có thể điều chỉnh ví dụ trên để bỏ qua lệnh move
#![allow(unused)] fn main() { #[cfg(target_arch = "x86_64")] { use std::arch::asm; let mut x: u64 = 3; unsafe { asm!("add {0}, 5", inout(reg) x); } assert_eq!(x, 8); } }
Chúng ta có thể thấy rằng inout
được sử dụng để chỉ định một đối số là cả đầu vào và đầu ra. Điều này khác với việc chỉ định đầu vào và đầu ra riêng biệt ở chỗ nó đảm bảo gán cả hai vào cùng một thanh ghi.
Nó cũng có thể được sử dụng để chỉ định các biến khác nhau cho phần đầu vào và đầu ra của một đối số inout
:
#![allow(unused)] fn main() { #[cfg(target_arch = "x86_64")] { use std::arch::asm; let x: u64 = 3; let y: u64; unsafe { asm!("add {0}, 5", inout(reg) x => y); } assert_eq!(y, 8); } }
Các toán hạng đầu ra trễ
Trình biên dịch của Rust cẩn thận với việc phân bổ các toán hạng. Nó được giả định rằng một out
có thể được ghi vào bất cứ lúc nào, và do đó không thể chia sẻ vị trí của nó với bất kỳ đối số nào khác. Tuy nhiên, để đảm bảo hiệu suất tối ưu, một điều quan trọng là sử dụng ít thanh ghi nhất có thể, vì vậy chúng sẽ không phải được lưu và tải lại xung quanh khối mã inline assembly được nhúng. Để đạt được điều này, Rust cung cấp một chỉ định lateout
. Cái này có thể được sử dụng trên bất kỳ đầu ra nào được ghi chỉ sau khi tất cả các đầu vào đã được tiêu thụ. Cũng có một phiên bản inlateout
của chỉ định này.
Đây là một ví dụ mà inlateout
không thể được sử dụng trong chế độ release
hoặc các trường hợp tối ưu hóa khác:
#![allow(unused)] fn main() { #[cfg(target_arch = "x86_64")] { use std::arch::asm; let mut a: u64 = 4; let b: u64 = 4; let c: u64 = 4; unsafe { asm!( "add {0}, {1}", "add {0}, {2}", inout(reg) a, in(reg) b, in(reg) c, ); } assert_eq!(a, 12); } }
Mã trên có thể làm việc tốt trong các trường hợp chưa được tối ưu hóa (Debug
mode), nhưng nếu bạn muốn hiệu suất tối ưu (release
mode hoặc các trường hợp tối ưu hóa khác), nó có thể không hoạt động.
Điều đó là bởi vì trong các trường hợp tối ưu hóa, trình biên dịch có thể tự do phân bổ cùng một thanh ghi cho các đầu vào b
và c
vì nó biết chúng có cùng giá trị. Tuy nhiên, nó phải phân bổ một thanh ghi riêng cho a
vì nó sử dụng inout
và không phải inlateout
. Nếu inlateout
được sử dụng, thì a
và c
có thể được phân bổ vào cùng một thanh ghi, trong trường hợp đó lệnh đầu tiên sẽ ghi đè giá trị của c
và làm cho mã assembly tạo ra kết quả sai.
Tuy nhiên, ví dụ sau có thể sử dụng inlateout
vì đầu ra chỉ được sửa đổi sau khi tất cả các đầu vào đã được đọc:
#![allow(unused)] fn main() { #[cfg(target_arch = "x86_64")] { use std::arch::asm; let mut a: u64 = 4; let b: u64 = 4; unsafe { asm!("add {0}, {1}", inlateout(reg) a, in(reg) b); } assert_eq!(a, 8); } }
Như bạn thấy, đoạn mã này vẫn hoạt động chính xác nếu a
và b
được gán cho cùng một thanh ghi.
Các toán hạng thanh ghi rõ ràng
Một vài lệnh yêu cầu rằng các toán hạng phải nằm trong một thanh ghi cụ thể. Do đó, Rust inline assembly cung cấp một số chỉ định ràng buộc cụ thể hơn. Trong khi reg
có sẵn trên bất kỳ kiến trúc nào, các thanh ghi rõ ràng chỉ có sẵn trên kiến trúc cụ thể. Ví dụ, cho x86, các thanh ghi chung eax
, ebx
, ecx
, edx
, ebp
, esi
, và edi
có thể được chỉ định bằng tên của chúng.
#![allow(unused)] fn main() { #[cfg(target_arch = "x86_64")] { use std::arch::asm; let cmd = 0xd1; unsafe { asm!("out 0x64, eax", in("eax") cmd); } } }
Trong ví dụ này chúng ta gọi lệnh out
để xuất nội dung của biến cmd
ra cổng 0x64
. Vì lệnh out
chỉ chấp nhận eax
(và các thanh ghi con của nó) làm toán hạng, chúng ta phải sử dụng chỉ định ràng buộc eax
.
Ghi chú: không giống với các kiểu toán hạng khác, các toán hạng thanh ghi rõ ràng không thể được sử dụng trong chuỗi mẫu: bạn không thể sử dụng
{}
thay vào đó nên viết tên thanh ghi trực tiếp. Ngoài ra, chúng phải xuất hiện ở cuối danh sách toán hạng sau tất cả các loại toán hạng khác.
Xem xét ví dụ này sử dụng lệnh mul
của x86:
#![allow(unused)] fn main() { #[cfg(target_arch = "x86_64")] { use std::arch::asm; fn mul(a: u64, b: u64) -> u128 { let lo: u64; let hi: u64; unsafe { asm!( // Lệnh mul của x86 nhận rax là một đầu vào ngầm định và ghi // kết quả 128-bit của phép nhân vào rax:rdx. "mul {}", in(reg) a, inlateout("rax") b => lo, lateout("rdx") hi ); } ((hi as u128) << 64) + lo as u128 } } }
Ở đây sử dụng lệnh mul
để nhân hai số 64-bit với kết quả 128-bit. Chỉ có một toán hạng rõ ràng là một thanh ghi, chúng ta lấy nó từ biến a
. Toán hạng thứ hai là ngầm định và phải là thanh ghi rax
, chúng ta lấy nó từ biến b
. 64-bit thấp của kết quả được lưu trong rax
từ đó chúng ta đưa vào biến lo
. 64-bit cao được lưu trong rdx
từ đó chúng ta đưa nó vào biến hi
.
Clobbered registers
Trong nhiều trường hợp, inline assembly sẽ sửa đổi trạng thái không cần thiết là đầu ra. Thông thường điều này là do chúng ta phải sử dụng một thanh ghi tạm trong assembly hoặc vì các lệnh sửa đổi trạng thái mà chúng ta không cần phải xem xét thêm. Trạng thái này thường được xem là "Bị xáo trộn" ("clobbered"
). Chúng ta cần phải thông báo cho trình biên dịch về điều này vì nó có thể cần phải lưu và khôi phục trạng thái này xung quanh khối inline assembly.
use std::arch::asm; #[cfg(target_arch = "x86_64")] fn main() { // three entries of four bytes each // Ba mục của bốn byte mỗi mục let mut name_buf = [0_u8; 12]; // Chuỗi được lưu trữ dưới dạng ascii trong ebx, edx, ecx theo thứ tự // Vì ebx được dành riêng, asm cần phải giữ giá trị của nó. // Vì vậy chúng ta push và pop nó xung quanh asm chính. // (trong chế độ 64 bit đối với các vi xử lý 64 bit, các xử lý 32 bit sẽ sử dụng ebx) unsafe { asm!( "push rbx", "cpuid", "mov [rdi], ebx", "mov [rdi + 4], edx", "mov [rdi + 8], ecx", "pop rbx", // Chúng ta sử dụng con trỏ đến một mảng cho việc lưu trữ các giá trị để đơn giản hóa // mã Rust nhưng điều này đòi hỏi phải thêm một số lệnh asm // Điều này được thể hiện rõ ràng hơn cách hoạt động của asm, khác với // các đầu ra thanh ghi rõ ràng như `out("ecx") val` // *Con trỏ chính nó* chỉ là đầu vào ngầm định mặc dù nó được ghi sau in("rdi") name_buf.as_mut_ptr(), // chọn cpuid 0, cũng chỉ định eax là bị xáo trộn inout("eax") 0 => _, // cpuid cũng làm xáo trộn các thanh ghi này out("ecx") _, out("edx") _, ); } let name = core::str::from_utf8(&name_buf).unwrap(); println!("CPU Manufacturer ID: {}", name); } #[cfg(not(target_arch = "x86_64"))] fn main() {}
Trong ví dụ trên, chúng ta sử dụng lệnh cpuid
để đọc ID của nhà sản xuất CPU. Lệnh này ghi vào eax
với tham số cpuid
tối đa được hỗ trợ và ebx
, edx
, và ecx
với ID của nhà sản xuất CPU là các byte ASCII theo thứ tự đó.
Mặc dù eax
không bao giờ được đọc chúng ta vẫn cần phải thông báo cho trình biên dịch rằng thanh ghi đã bị sửa đổi để trình biên dịch có thể lưu bất kỳ giá trị nào trong các thanh ghi này trước khi vào khối asm. Điều này được thực hiện bằng cách khai báo nó là một đầu ra nhưng với tên biến là _
thay vì tên biến, điều này chỉ ra rằng giá trị đầu ra sẽ bị bỏ đi.
Đoạn mã này cũng giải quyết giới hạn của ebx
là một thành ghi được dành riêng bởi LLVM. Điều đó có nghĩa là LLVM cho rằng nó có đầy đủ quyền kiểm soát thanh ghi và nó phải được khôi phục về trạng thái ban đầu trước khi thoát khỏi khối asm, vì vậy nó không thể được sử dụng như một input hoặc output trừ khi trình biên dịch sử dụng nó để gán cho một lớp thanh ghi chung (ví dụ: in(reg)
). Điều này làm cho các toán hạng reg
nguy hiểm khi sử dụng thanh ghi được dành riêng vì chúng ta có thể làm hỏng đầu vào hoặc đầu ra của mình mà không biết vì chúng chia sẻ cùng một thanh ghi.
Để làm việc với giới hạn này, chúng ta sử dụng rdi
để lưu trữ con trỏ đến mảng đầu ra, lưu ebx
thông qua push
, đọc từ ebx
bên trong khối asm vào mảng và sau đó khôi phục ebx
về trạng thái ban đầu thông qua pop
. push
và pop
sử dụng phiên bản 64 bit đầy đủ của rbx
để đảm bảo rằng toàn bộ thanh ghi được lưu. Trên các kiến trúc 32 bit, mã sẽ sử dụng ebx
trong push
/pop
.
Điều này có thể được sử dụng với một lớp thanh ghi chung để lấy một thanh ghi tạm thời sử dụng bên trong mã asm:
#![allow(unused)] fn main() { #[cfg(target_arch = "x86_64")] { use std::arch::asm; // Nhân x với 6 sử dụng shifts và adds let mut x: u64 = 4; unsafe { asm!( "mov {tmp}, {x}", "shl {tmp}, 1", "shl {x}, 2", "add {x}, {tmp}", x = inout(reg) x, tmp = out(reg) _, ); } assert_eq!(x, 4 * 6); } }
Các toán hạng biểu tượng và ABI clobber
Mặc định, asm!
giả định rằng bất kỳ thanh ghi nào không được chỉ định là đầu ra sẽ được giữ nguyên bởi mã assembly. Tham số clobber_abi
của asm!
cho trình biên dịch biết để tự động thêm các toán hạng clobber tương ứng với ABI gọi hàm đã cho: bất kỳ thanh ghi nào không được hoàn toàn giữ nguyên trong ABI đó sẽ được coi là bị xáo trộn (clobbered). Nhiều đối số clobber_abi
có thể được cung cấp và tất cả các clobber từ tất cả các ABI được chỉ định sẽ được thêm vào.
#![allow(unused)] fn main() { #[cfg(target_arch = "x86_64")] { use std::arch::asm; extern "C" fn foo(arg: i32) -> i32 { println!("arg = {}", arg); arg * 2 } fn call_foo(arg: i32) -> i32 { unsafe { let result; asm!( "call {}", // Con trỏ hàm để gọi in(reg) foo, // đối số thứ nhất trong rdi in("rdi") arg, // giá trị trả về trong rax out("rax") result, // Đánh dấu tất cả các thanh ghi không được giữ nguyên bởi lời gọi "C" // là bị xáo trộn. clobber_abi("C"), ); result } } } }
Register template modifiers
Trong một vài trường hợp, ta cần điều khiển cách mà tên các thanh ghi được định dạng khi chèn vào trong một chuỗi mẫu. Điều này là cần thiết khi một ngôn ngữ lập trình hợp ngữ của một kiến trúc có nhiều tên cho cùng một thanh ghi, mỗi tên thường là một "view" trên một tập con của thanh ghi (ví dụ: 32 bit thấp của một thanh ghi 64 bit).
Mặc định thì trình biên dịch sẽ luôn chọn tên mà tham chiếu đến kích thước đầy đủ của thanh ghi (ví dụ: rax
trên x86-64, eax
trên x86, v.v.).
Mặc định này có thể bị ghi đè bằng cách sử dụng các bộ điều khiển trên các toán hạng chuỗi mẫu, giống như bạn làm với chuỗi định dạng:
#![allow(unused)] fn main() { #[cfg(target_arch = "x86_64")] { use std::arch::asm; let mut x: u16 = 0xab; unsafe { asm!("mov {0:h}, {0:l}", inout(reg_abcd) x); } assert_eq!(x, 0xabab); } }
Trong ví dụ này, chúng ta sử dụng lớp thanh ghi reg_abcd
để hạn chế bộ phân bổ thanh ghi chỉ sử dụng 4 thanh ghi x86 cổ điển (ax
, bx
, cx
, dx
) mà mỗi thanh ghi có 2 byte đầu có thể được truy cập độc lập .
Chúng ta giả định rằng bộ phân bổ thanh ghi đã chọn để phân bổ x
trong thanh ghi ax
. Bộ điều khiển h
sẽ xuất tên thanh ghi cho byte cao của thanh ghi đó và bộ điều khiển l
sẽ xuất tên thanh ghi cho byte thấp của nó. Mã asm sẽ được mở rộng thành mov ah, al
để sao chép byte thấp của giá trị vào byte cao.
Nếu bạn sử dụng một kiểu dữ liệu nhỏ hơn (ví dụ: u16
) với một toán hạng và quên sử dụng các bộ điều khiển mẫu, trình biên dịch sẽ xuất cảnh báo và đề nghị sử dụng bộ điều khiển mẫu chính xác.
Các toán hạng địa chỉ bộ nhớ
Thỉnh thoảng các chỉ dẫn lập trình yêu cầu các toán hạng được truyền qua địa chỉ bộ nhớ hoặc vị trí bộ nhớ. Bạn phải tự sử dụng cú pháp địa chỉ bộ nhớ được chỉ định bởi kiến trúc đích. Ví dụ, trên x86/x86_64 sử dụng cú pháp lập trình hợp ngữ Intel, bạn nên bao các đầu vào/đầu ra trong []
để chỉ rõ chúng là các toán hạng bộ nhớ:
#![allow(unused)] fn main() { #[cfg(target_arch = "x86_64")] { use std::arch::asm; fn load_fpu_control_word(control: u16) { unsafe { asm!("fldcw [{}]", in(reg) &control, options(nostack)); } } } }
Các nhãn
Việc tái sử dụng một nhãn đã đặt tên, cục bộ hoặc không, có thể dẫn đến lỗi trình biên dịch hoặc liên kết hoặc có thể gây ra các hành vi khác. Việc tái sử dụng một nhãn có thể xảy ra mtheo các cách khác nhau bao gồm:
- explicitly: sử dụng một nhãn nhiều lần trong một khối
asm!
, hoặc nhiều lần trên nhiều khối. - implicitly thông qua inlining: trình biên dịch được phép tạo ra nhiều bản sao của một khối
asm!
, ví dụ khi hàm chứa nó được inlined vào nhiều nơi. - implicitly thông qua LTO: LTO có thể gây ra việc mã từ các crate khác được đặt trong cùng một đơn vị biên dịch, và do đó có thể đưa vào các nhãn bất kỳ.
Do đó, bạn chỉ nên sử dụng các [nhãn cụ bộ] số học của GNU assembler trong inline assembly. Định nghĩa các ký hiệu trong mã có thể dẫn đến lỗi trình biên dịch và/hoặc liên kết do định nghĩa các ký hiệu trùng lặp.
Hơn nữa, trên x86 khi sử dụng cú pháp Intel mặc định, do [một lỗi LLVM], bạn không nên sử dụng các nhãn chỉ bao gồm các chữ số 0
và 1
, ví dụ: 0
, 11
hoặc 101010
, vì chúng có thể được hiểu là các giá trị nhị phân. Sử dụng options(att_syntax)
sẽ giúp tránh được bất kỳ một sự hiểu lầm nào, nhưng điều đó ảnh hưởng đến cú pháp của toàn bộ khối asm!
. (Xem Options, bên dưới, để biết thêm về options
.)
#![allow(unused)] fn main() { #[cfg(target_arch = "x86_64")] { use std::arch::asm; let mut a = 0; unsafe { asm!( "mov {0}, 10", "2:", "sub {0}, 1", "cmp {0}, 3", "jle 2f", "jmp 2b", "2:", "add {0}, 2", out(reg) a ); } assert_eq!(a, 5); } }
Mã này sẽ giảm giá trị của thanh ghi {0}
từ 10 xuống 3, sau đó cộng 2 và lưu vào a
.
Ví dụ này cho thấy một vài điều:
- Đầu tiên, cùng một số có thể được sử dụng làm nhãn nhiều lần trong cùng một khối inline.
- Thứ hai, khi một nhãn số được sử dụng như một tham chiếu (ví dụ: như một toán hạng của một chỉ dẫn), bạn nên thêm các hậu tố
b
(backward
) hoặcf
(forward
) vào nhãn số. Nó sẽ tham chiếu đến nhãn gần nhất được định nghĩa bởi số này theo hướng này.
Các tùy chọn
Mặc định, một khối inline assembly được xem như một lời gọi hàm FFI bên ngoài với một quy ước gọi tùy chỉnh: nó có thể đọc/ghi bộ nhớ, có thể có các tác động phụ quan sát được, v.v. Tuy nhiên, trong nhiều trường hợp, việc cung cấp cho trình biên dịch thêm thông tin về những gì mã assembly đang làm để nó có thể tối ưu hóa tốt hơn.
Hãy xem xét ví dụ trước về một chỉ dẫn add
:
#![allow(unused)] fn main() { #[cfg(target_arch = "x86_64")] { use std::arch::asm; let mut a: u64 = 4; let b: u64 = 4; unsafe { asm!( "add {0}, {1}", inlateout(reg) a, in(reg) b, options(pure, nomem, nostack), ); } assert_eq!(a, 8); } }
Các lựa chọn có thể được cung cấp như một đối số cuối cùng tùy chọn cho macro asm!
. Chúng ta chỉ định ba tùy chọn ở đây:
pure
có nghĩa là mã assembly không có bất kỳ tác động phụ nào có thể quan sát được và đầu ra của nó chỉ phụ thuộc vào đầu vào. Điều này cho phép trình biên dịch tối ưu hóa để gọi mã assembly ít hơn hoặc thậm chí loại bỏ nó hoàn toàn.nomem
có nghĩa là mã assembly không đọc/ghi bộ nhớ. Theo mặc định, trình biên dịch sẽ giả định rằng mã assembly có thể đọc/ghi bất kỳ địa chỉ bộ nhớ nào mà nó có thể truy cập (ví dụ: thông qua một con trỏ được truyền như một toán hạng, hoặc một biến toàn cục).nostack
có nghĩa là mã assembly không đẩy bất kỳ dữ liệu nào lên stack. Điều này cho phép trình biên dịch sử dụng các tối ưu hóa như ngăn xếp vùng đỏ trên x86-64 để tránh điều chỉnh con trỏ ngăn xếp.
Những điều này cho phép trình biên dịch tối ưu hóa mã sử dụng asm!
tốt hơn, ví dụ như bằng cách loại bỏ các khối asm!
hoàn toàn không có đầu ra.
Xem reference để biết danh sách đầy đủ các tùy chọn và tác dụng của chúng.
Khả năng tương thích
Ngôn ngữ Rust đang phát triển nhanh, và do đó có thể xảy ra một số vấn đề về tương thích, mặc dù có nỗ lực để đảm bảo tương thích với các phiên bản trước đó.
Raw identifiers
Như các ngôn ngữ lập trình khác, Rust cũng có khái niệm "keywords" - từ khóa. Các từ khóa này có ý nghĩa riêng trong Rust, do đó chúng ta không thể sử dụng các từ khóa này để đặt tên cho biến, hàm,...
Raw identifiers cho phép bạn sử dụng các từ khóa này. Raw identifiers trong Rust là một chuỗi ký tự bắt đầu bằng từ khóa "r#". Điều này đặc biệt hữu ích khi Rust giới thiệu các từ khóa mới, và một thư viện sử dụng phiên bản Rust cũ hơn có một biến hoặc hàm có cùng tên với từ khóa được giới thiệu trong phiên bản Rust mới hơn.
Ví dụ, một crate được biên dịch với phiên bản 2015 và export một hàm được đặt tên là try
. Trong phiên bản 2018 của Rust giới thiệu try
là một từ khóa mới, thì chúng ta sẽ không thể sử dụng try
để đặt tên cho hàm nếu không có raw identifiers.
extern crate foo;
fn main() {
foo::try();
}
Đây là lỗi sẽ xảy ra:
error: expected identifier, found keyword `try`
--> src/main.rs:4:4
|
4 | foo::try();
| ^^^ expected identifier, found keyword
Chúng ta sẽ phải sử dụng raw identifier:
extern crate foo;
fn main() {
foo::r#try();
}
Meta
Vài chủ đề không liên quan đến cách lập trình nhưng cung cấp cho bạn các công cụ hoặc cơ sở hạ tầng giúp cho bạn làm mọi thứ trở nên tốt hơn cho mọi người. Những chủ đề này bao gồm:
- Tài liệu: Tạo ra tài liệu cho thư viện của bạn thông qua công cụ
rustdoc
được đính kèm sẵn. - Playground: Tích hợp trình Rust Playground vào tài liệu của bạn.
Documentation
Sử dụng cargo doc
để build tài iệu trong thư mục target/doc
.
Sử dụng cargo test
để chạy tất cả các phép kiểm thử (bao gồm cả các kiểm thử dành cho tài liệu), và cargo test --doc
để chạy chỉ các kiểm thử của tài liệu.
Các câu lệnh này sẽ gọi rustdoc
(và rustc
) theo yêu cầu một cách phù hợp.
Các ghi chú tài liệu
Các ghi chú tài liệu rất hữu ích cho những dự án lớn yêu cầu tài liệu mô tả. Khi chạy lệnh rustdoc
, tài liệu của dự án sẽ được xây dựng dựa vào các ghi chú này. Ghi chú được bắt đầu bằng một dấu ///
, và hỗ trợ định dạng Markdown.
#![crate_name = "doc"]
/// Cấu trúc đại diện cho một con người
pub struct Person {
/// Một người cần phải có tên, mặc cho Juliet có ghét nó
name: String,
}
impl Person {
/// Trả về một người cùng với tên của họ
///
/// # Các đối số
///
/// * `name` - Một chuỗi ký tự chứa tên của người
///
/// # Ví dụ
///
/// ```
/// // Bạn có thể đặt mã rust giữa các dấu rào (```) trong các ghi chú
/// // Nếu bạn truyền tham số --test đến `rustdoc`, nó sẽ kiểm tra chính tài liệu cho bạn!
/// use doc::Person;
/// let person = Person::new("name");
/// ```
pub fn new(name: &str) -> Person {
Person {
name: name.to_string(),
}
}
/// Gửi một lời chào thân thiện nào!
///
/// Nói "Hello, [name](Person::name)" với `Person` mà gọi nó.
pub fn hello(& self) {
println!("Hello, {}!", self.name);
}
}
fn main() {
let john = Person::new("John");
john.hello();
}
Để chạy các kiểm thử, phải build mã thành một thư viện trước, sau đó hãy chỉ cho rustdoct
biết nơi để tìm thư viện liên kết nó với mỗi một chường trình doctest:
$ rustc doc.rs --crate-type lib
$ rustdoc --test --extern doc="libdoc.rlib" doc.rs
Các thuộc tính tài liệu
Dưới đây là vài ví dụ về các thuộc tính #[doc]
thường được sử dụng phổ biến nhất với rustdoc
.
inline
Được dùng cho các tài liệu nội tuyến, thay vì liên kết ra ngoài một trang riêng biệt.
#[doc(inline)]
pub use bar::Bar;
/// tài liệu cho mod bar
mod bar {
/// tài liệu cho cấu trúc Bar
pub struct Bar;
}
no_inline
Dùng chặn các liên kết ra trang riêng hoặc nơi khác.
// Ví dụ từ libcore/prelude
#[doc(no_inline)]
pub use crate::mem::drop;
hidden
Dùng thuộc tính này để chỉ cho rustdoc
bỏ qua phần này trong tài liệu:
// Ví dụ từ thư viện futures-rs
#[doc(hidden)]
pub use self::async_await::*;
Đối với tài liệu, rustdoc
được cộng đồng sử dụng một cách rộng rãi. Nó được dùng để tạo ra tài liệu của thư viện std.
Xem thêm:
- The Rust Book: Making Useful Documentation Comments
- The rustdoc Book
- The Reference: Doc comments
- RFC 1574: API Documentation Conventions
- RFC 1946: Relative links to other items from doc comments (intra-rustdoc links)
- Is there any documentation style guide for comments? (reddit)
Playground
Rust Playground là cách để trải nghiệm viết mã Rust thông qua giao diện trình duyệt web.
Sử dụng với mdbook
Với mdbook
, bạn có thể viết các đoạn mã ví dụ mà có thể chạy và chỉnh sửa được.
fn main() { println!("Hello World!"); }
Điều này cho phép người đọc không những có thể chạy được đoạn mã mẫu của bạn, mà còn có thể sửa và tinh chỉnh nó. Chìa khóa ở đây là thêm từ editable
vào khối rào mã của bạn phân cách bởi dấu phẩy.
```rust,editable
//...đặt mã của bạn ở đây
```
Ngoài ra, bạn có thể thêm từ ignore
nếu bạn muốn mdbook
bỏ qua mã của bạn khi nó thực hiện build hoặc kiểm thử.
```rust,editable,ignore
//...place your code here
```
Sử dụng với tài liệu
Bạn có thể nhận ra rằng trong một số tài liệu chính thức của Rust xuất hiện một nút "chạy" ("Run"), có tác dụng mở mã mẫu trong một cửa sổ mới trên Rust Playground. Tính năng này được bật nếu bạn sử dụng thuộc tính #[doc]
gọi là html_playground_url
.