Bỏ qua để đến nội dung

Ép kiểu trong Rust

Rust không tự động chuyển một kiểu thành kiểu khác cho bạn — nếu bạn trộn một i32 với một f64 trong cùng một biểu thức, code sẽ không biên dịch được. Bạn phải chuyển đổi một cách tường minh. Đây là chủ ý: việc chuyển đổi ngầm là nguồn gốc phổ biến của lỗi ở các ngôn ngữ khác.

Dùng từ khóa as để ép (cast) một kiểu số thành kiểu số khác:

fn main() {
let x: i32 = 10;
let y: f64 = x as f64; // i32 -> f64
println!("{y}");
}
▶ Tự chạy thử

Điều này quan trọng hơn vẻ ngoài của nó — hãy thử chia hai số nguyên mà không ép kiểu, bạn sẽ nhận được phép chia số nguyên thay vì kết quả thập phân mà có lẽ bạn mong muốn:

fn main() {
let a = 10;
let b = 3;
println!("{}", a / b); // 3 (chia số nguyên)
println!("{}", a as f64 / b as f64); // 3.3333333333333335
}
▶ Tự chạy thử

Ép xuống kiểu nhỏ hơn có thể mất dữ liệu

Phần tiêu đề “Ép xuống kiểu nhỏ hơn có thể mất dữ liệu”

Ép từ một kiểu lớn sang một kiểu nhỏ hơn (như i32 sang u8) không báo lỗi — nó chỉ cắt bớt, và kết quả có thể khiến bạn bất ngờ:

fn main() {
let big: i32 = 300;
let small = big as u8; // u8 chỉ chứa được 0-255
println!("{small}"); // 44, không phải 300 — nó đã quay vòng
}
▶ Tự chạy thử

as dùng được với nhiều kiểu hơn chứ không chỉ số nguyên. Mỗi loại có một quy tắc đáng biết:

fn main() {
println!("{}", 3.99_f64 as i32); // 3: phần thập phân bị cắt bỏ
println!("{}", -3.99_f64 as i32); // -3: cắt về phía 0, không phải làm tròn xuống
println!("{}", 3.99_f64.round() as i32); // 4: làm tròn trước nếu bạn muốn vậy
println!("{}", 300.0_f64 as u8); // 255: quá lớn, nên dừng ở giá trị tối đa
println!("{}", -5.0_f64 as u8); // 0: quá nhỏ, nên dừng ở giá trị tối thiểu
println!("{}", 'A' as u32); // 65: con số đứng sau một char
println!("{}", 97_u8 as char); // a: và chuyển ngược lại
println!("{}", true as i32); // 1 (false sẽ là 0)
}

Kết quả:

3
-3
4
255
0
65
a
1
▶ Tự chạy thử

Vài điều cần ghi nhớ:

  • Số thực sang số nguyên bỏ đi mọi thứ sau dấu thập phân. Nó không làm tròn. Hãy gọi .round() trước nếu bạn muốn số nguyên gần nhất.
  • Số thực vượt phạm vi bị kẹp về giá trị gần nhất vừa với kiểu đích (300.0 thành 255 trong một u8). Điều này khác với số nguyên, vốn quay vòng (300 đã thành 44 ở trên).
  • Ký tự có một con số đứng sau (mã Unicode của nó). 'A' as u32 cho 65. Chỉ u8 mới ép thẳng ngược về char được; với số lớn hơn bạn dùng char::from_u32, vì không phải con số nào cũng là một ký tự hợp lệ.
  • Boolean trở thành 1 hoặc 0. Chiều ngược lại thì không được: 1 as bool không biên dịch được. Hãy dùng một phép so sánh như n != 0 thay thế.

Ép kiểu bằng as chỉ dùng được giữa các kiểu số. Để biến văn bản thành một con số — chẳng hạn thứ người dùng gõ vào — hãy dùng .parse():

fn main() {
let text = "42";
let number: i32 = text.parse().expect("không phải số hợp lệ");
println!("{}", number + 8);
}

Kết quả:

50
▶ Tự chạy thử

.parse() có thể thất bại (lỡ văn bản hoàn toàn không phải số thì sao?), nên nó trả về một Result thay vì trả thẳng con số. Chúng ta chưa học Result — hiện tại, .expect("thông báo") là cách đơn giản để nói “nếu thất bại thì dừng chương trình với thông báo này”. Bạn sẽ thấy cách xử lý đúng đắn ở bài Result.

Chiều ngược lại đơn giản hơn — gần như mọi kiểu đều có thể chuyển thành String bằng .to_string():

fn main() {
let number = 42;
let text = number.to_string();
println!("{}", text + "!");
}
▶ Tự chạy thử

Lưu ba điểm kiểm tra dưới dạng giá trị i32: 7, 8 và 10. In tổng của chúng, rồi in điểm trung bình với hai chữ số thập phân. Gợi ý: nếu chia hai số nguyên bạn sẽ được 8, không phải 8.33, nên hãy ép tổng sang f64 trước. {:.2} in ra hai chữ số thập phân.

Xem lời giải
fn main() {
let a: i32 = 7;
let b: i32 = 8;
let c: i32 = 10;
let sum = a + b + c;
let average = sum as f64 / 3.0; // 25.0 / 3.0, không phải 25 / 3
println!("tổng: {sum}");
println!("trung bình: {:.2}", average);
}

Kết quả:

tổng: 25
trung bình: 8.33
▶ Tự chạy thử

Một cửa hàng đưa cho bạn giá dưới dạng văn bản, "4.50", và số lượng dưới dạng i32, 3. Hãy biến văn bản thành f64, nhân với số lượng, rồi in ra Tổng: 13.5. Gợi ý: bạn cần cả hai công cụ trong trang này: .parse() cho văn bản và as f64 cho số lượng.

Xem lời giải
fn main() {
let price_text = "4.50";
let quantity: i32 = 3;
let price: f64 = price_text.parse().expect("giá không hợp lệ");
let total = price * quantity as f64; // f64 * f64
println!("Tổng: {total}");
}

Kết quả:

Tổng: 13.5
▶ Tự chạy thử

Không có as f64, Rust sẽ từ chối nhân một f64 với một i32, vì nó không bao giờ tự trộn các kiểu số cho bạn.

  • Rust không bao giờ tự động chuyển kiểu — bạn luôn phải ép kiểu một cách tường minh.
  • Dùng as để chuyển giữa các kiểu số: x as f64, x as u8.
  • Ép xuống kiểu nhỏ hơn sẽ lặng lẽ cắt bớt — nó không báo lỗi, nên hãy cẩn thận.
  • Số thực sang số nguyên cắt bỏ phần thập phân (dùng .round() trước để làm tròn). char và bool cũng có thể ép sang số.
  • Dùng .parse() để biến văn bản thành số, và .to_string() cho chiều ngược lại.

Kiểm tra nhanh

1. Chuyện gì xảy ra khi bạn chia hai số nguyên như 10 / 3 trong Rust mà không ép kiểu?

2. Chuyện gì xảy ra khi bạn ép 300_i32 as u8?

3. Vì việc chuyển đổi có thể thất bại, .parse() trả về gì khi chuyển chuỗi thành số?

Điểm: 0 / 3