Enum trong Rust
Enum (viết tắt của enumeration, kiểu liệt kê) định nghĩa một kiểu có thể là một trong một tập khả năng cố định. Nếu struct dành cho “thứ này có tất cả các field sau”, thì enum dành cho “thứ này là đúng một trong các lựa chọn sau”.
Định nghĩa và dùng enum
Phần tiêu đề “Định nghĩa và dùng enum”enum Direction { North, South, East, West,}
fn main() { let heading = Direction::North;
match heading { Direction::North => println!("Đang đi về hướng bắc"), Direction::South => println!("Đang đi về hướng nam"), Direction::East => println!("Đang đi về hướng đông"), Direction::West => println!("Đang đi về hướng tây"), }}Kết quả:
Đang đi về hướng bắcĐể ý rằng match ở đây không cần nhánh bắt-tất-cả _ — vì mọi Direction có thể có đều đã được liệt kê, Rust biết ngay là phép match đã đầy đủ.
Enum có thể chứa dữ liệu
Phần tiêu đề “Enum có thể chứa dữ liệu”Đây là chỗ enum của Rust đi xa hơn enum thông thường ở các ngôn ngữ khác: mỗi biến thể (variant) có thể mang dữ liệu riêng của nó:
enum Shape { Circle(f64), // bán kính Rectangle(f64, f64), // rộng, cao}
fn area(shape: &Shape) -> f64 { match shape { Shape::Circle(radius) => 3.14159 * radius * radius, Shape::Rectangle(width, height) => width * height, }}
fn main() { let circle = Shape::Circle(3.0); let rectangle = Shape::Rectangle(4.0, 5.0);
println!("{:.2}", area(&circle)); println!("{:.2}", area(&rectangle));}Kết quả:
28.2720.00Shape::Circle mang một f64 (bán kính); Shape::Rectangle mang hai. match lấy thẳng dữ liệu đó ra cho bạn trong mỗi nhánh — không cần thêm bước nào để “mở gói” nó.
Enum cũng có thể có phương thức
Phần tiêu đề “Enum cũng có thể có phương thức”Giống như struct, enum có thể có một khối impl chứa phương thức. Bên trong phương thức, bạn thường match self để quyết định làm gì với từng biến thể:
#[derive(Debug)]enum Light { Red, Yellow, Green,}
impl Light { fn seconds(&self) -> u32 { match self { Light::Red => 30, Light::Yellow => 3, Light::Green => 25, } }
fn next(&self) -> Light { match self { Light::Red => Light::Green, Light::Green => Light::Yellow, Light::Yellow => Light::Red, } }}
fn main() { let mut light = Light::Red;
for _ in 0..4 { println!("{:?} trong {} giây", light, light.seconds()); light = light.next(); }}Kết quả:
Red trong 30 giâyGreen trong 25 giâyYellow trong 3 giâyRed trong 30 giâynext trả về một Light hoàn toàn mới, và light = light.next(); thay cái cũ bằng cái mới. Vì thế light được khai báo mut. #[derive(Debug)] hoạt động với enum giống như với struct, nên {:?} in ra tên của biến thể (ở đây là tên tiếng Anh trong code: Red là đèn đỏ, Yellow là đèn vàng, Green là đèn xanh).
Đặt logic vào phương thức giúp giữ nó ở ngay cạnh kiểu dữ liệu. Ai dùng Light cũng gọi được light.seconds() mà không cần biết thời gian được quyết định thế nào. Và nếu sau này bạn thêm một biến thể mới, như FlashingYellow, trình biên dịch sẽ chỉ ra mọi chỗ match bạn cần cập nhật.
Option: một enum bạn đã dùng từ trước
Phần tiêu đề “Option: một enum bạn đã dùng từ trước”Kiểu Option mà bạn sẽ học kỹ trong bài riêng của nó chỉ là một enum được định nghĩa trong thư viện chuẩn:
enum Option<T> { Some(T), None,}Khi đã hiểu enum, Option không còn trông như một cú pháp đặc biệt nữa — nó là một enum hoàn toàn bình thường với hai biến thể, trong đó một biến thể tình cờ mang theo một giá trị.
Sao không dùng struct với một field “loại”?
Phần tiêu đề “Sao không dùng struct với một field “loại”?”Bạn có thể nghĩ đến việc giả lập điều này bằng một struct và một field chuỗi như "circle" hay "rectangle" — nhưng khi đó chẳng có gì ngăn bạn lỡ gõ "circl", và bạn phải tự kiểm tra xem field nào thật sự có ý nghĩa với từng loại. Enum khiến những tổ hợp không hợp lệ không thể viết ra ngay từ đầu, và trình biên dịch buộc bạn xử lý mọi biến thể.
Luyện tập
Phần tiêu đề “Luyện tập”1. Đếm tiền xu
Phần tiêu đề “1. Đếm tiền xu”Tạo enum Coin với các biến thể Penny, Nickel, Dime và Quarter (các đồng xu Mỹ: 1, 5, 10 và 25 xu). Viết fn value_in_cents(coin: &Coin) -> u32, rồi cộng tổng một ví tiền gồm: một quarter, một dime, một nickel, một penny và thêm một quarter nữa. Gợi ý: lưu các đồng xu trong một mảng và duyệt qua &purse.
Xem lời giải
enum Coin { Penny, Nickel, Dime, Quarter,}
fn value_in_cents(coin: &Coin) -> u32 { match coin { Coin::Penny => 1, Coin::Nickel => 5, Coin::Dime => 10, Coin::Quarter => 25, }}
fn main() { let purse = [Coin::Quarter, Coin::Dime, Coin::Nickel, Coin::Penny, Coin::Quarter];
let mut total = 0; for coin in &purse { total += value_in_cents(coin); }
println!("Tổng cộng: {total} xu");}Kết quả:
Tổng cộng: 66 xu2. Thêm hình tam giác
Phần tiêu đề “2. Thêm hình tam giác”Lấy enum Shape trong bài này và thêm biến thể Triangle chứa độ dài ba cạnh. Sau đó viết hàm perimeter (chu vi, tức độ dài đi hết một vòng) dùng được cho mọi hình. Gợi ý: chu vi hình tròn là 2.0 * 3.14159 * radius. Chu vi tam giác là tổng ba cạnh.
Xem lời giải
enum Shape { Circle(f64), Rectangle(f64, f64), Triangle(f64, f64, f64),}
fn perimeter(shape: &Shape) -> f64 { match shape { Shape::Circle(radius) => 2.0 * 3.14159 * radius, Shape::Rectangle(width, height) => 2.0 * (width + height), Shape::Triangle(a, b, c) => a + b + c, }}
fn main() { let shapes = [ Shape::Circle(1.0), Shape::Rectangle(4.0, 5.0), Shape::Triangle(3.0, 4.0, 5.0), ];
for shape in &shapes { println!("{:.2}", perimeter(shape)); }}Kết quả:
6.2818.0012.00Thử xóa nhánh Shape::Triangle khỏi match. Rust sẽ từ chối biên dịch, vì phép match không còn bao phủ mọi biến thể. Đó là cách trình biên dịch đảm bảo bạn không bao giờ quên một trường hợp nào.
3. Nhiệt độ
Phần tiêu đề “3. Nhiệt độ”Tạo enum Temperature với hai biến thể, mỗi biến thể chứa một f64: Celsius và Fahrenheit. Thêm phương thức to_celsius(&self) -> f64, và in Celsius(25.0), Fahrenheit(212.0) và Fahrenheit(50.0) theo °C với một chữ số thập phân. Gợi ý: để đổi, dùng (f - 32.0) * 5.0 / 9.0.
Xem lời giải
enum Temperature { Celsius(f64), Fahrenheit(f64),}
impl Temperature { fn to_celsius(&self) -> f64 { match self { Temperature::Celsius(c) => *c, Temperature::Fahrenheit(f) => (f - 32.0) * 5.0 / 9.0, } }}
fn main() { let readings = [ Temperature::Celsius(25.0), Temperature::Fahrenheit(212.0), Temperature::Fahrenheit(50.0), ];
for t in &readings { println!("{:.1} °C", t.to_celsius()); }}Kết quả:
25.0 °C100.0 °C10.0 °CVì self được mượn, c và f là tham chiếu (&f64). Phép tính chạy được trực tiếp trên tham chiếu, nhưng để trả về giá trị độ C trần, bạn viết *c, nghĩa là “con số mà c trỏ tới”.
Tóm tắt
Phần tiêu đề “Tóm tắt”- Enum định nghĩa một kiểu là một trong nhiều biến thể có tên:
enum Direction { North, South, ... }. - Biến thể có thể mang dữ liệu riêng, và các biến thể khác nhau có thể mang dữ liệu khác nhau.
matchtách dữ liệu của enum một cách tự nhiên và bắt buộc phải xử lý mọi biến thể.- Enum có thể có phương thức trong khối
impl, giống như struct. Option(vàResult, sẽ học sau) chỉ là các enum trong thư viện chuẩn — không có gì thần bí cả.
Kiểm tra nhanh
Phần tiêu đề “Kiểm tra nhanh”Kiểm tra nhanh
1. Mỗi biến thể của enum trong Rust làm được gì mà enum thông thường ở ngôn ngữ khác không làm được?
2. Vì sao ví dụ match với Direction không cần nhánh bắt-tất-cả _?
3. Trong thư viện chuẩn, Option<T> thật ra được triển khai như thế nào?
Điểm: 0 / 3
Đọc thêm
Phần tiêu đề “Đọc thêm”- Defining an Enum — The Rust Book (tiếng Anh): biến thể có dữ liệu, phương thức trên enum, và vì sao Rust có
Optionthay vì null. - The
matchControl Flow Construct — The Rust Book (tiếng Anh): so khớp enum chuyên sâu, với ví dụ phân loại đồng xu và mẫu bắt-tất-cả. - Enums — Rust by Example (tiếng Anh): ví dụ chạy được, gồm cả biến thể có field đặt tên như
Click { x: i64, y: i64 }. - Enumerations — The Rust Reference (tiếng Anh): toàn bộ quy tắc, như việc gán giá trị số tường minh cho các biến thể.
Option— tài liệu thư viện chuẩn (tiếng Anh): định nghĩa thật sự của enum mà bạn sẽ dùng nhiều nhất, cùng mọi phương thức hỗ trợ của nó.