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

Macro trong Rust

Ở trang Cú pháp, bạn đã học một quy tắc dễ nhớ: dấu ! nghĩa là đây là một macro, không phải hàm bình thường, ví dụ println!, vec!, format!. Trang này cuối cùng sẽ giải thích điều đó thật sự có nghĩa là gì.

Vì sao println! không thể là một hàm bình thường

Phần tiêu đề “Vì sao println! không thể là một hàm bình thường”

Một hàm bình thường như fn add(a: i32, b: i32) -> i32 luôn nhận một số lượng đối số cố định, với kiểu cố định. Nhưng bạn có thể gọi println! với số lượng và kiểu đối số khác nhau hoàn toàn:

fn main() {
println!("không có đối số nào");
println!("{}", 42);
println!("{} và {}", "hai", 2);
}

Kết quả:

không có đối số nào
42
hai và 2
▶ Tự chạy thử

Không có chữ ký hàm thông thường nào nhận được tất cả những lời gọi đó. Macro hoạt động khác: chúng chạy trước khi code của bạn được biên dịch hoàn toàn, và sinh ra code Rust thật dựa trên cách chúng được gọi. println!("{} và {}", "hai", 2) thực chất được mở rộng (expand) thành code thật xử lý đúng hai đối số đó; mỗi lời gọi khác nhau cho ra một bản mở rộng khác nhau.

Bạn có thể tự định nghĩa macro bằng macro_rules!. Đây là một macro tính bình phương của một số:

macro_rules! square {
($x:expr) => {
$x * $x
};
}
fn main() {
println!("{}", square!(5));
}

Kết quả:

25
▶ Tự chạy thử

($x:expr) có nghĩa là “nhận một biểu thức, và gọi nó là $x”. Phần => { ... } là code mà macro mở rộng thành: ở mọi chỗ bạn viết square!(5), trình biên dịch thay bằng 5 * 5 trước khi thực sự biên dịch bất cứ thứ gì.

Macro có thể khớp những mẫu phức tạp hơn, kể cả một danh sách đối số lặp lại. Đây là điều mà hàm bình thường đơn giản là không làm được:

macro_rules! sum {
($($x:expr),*) => {
{
let mut total = 0;
$(total += $x;)*
total
}
};
}
fn main() {
println!("{}", sum!(1, 2, 3));
println!("{}", sum!(10, 20));
}

Kết quả:

6
30
▶ Tự chạy thử

$($x:expr),* nghĩa là “không hoặc nhiều biểu thức, cách nhau bằng dấu phẩy”. Đây chính là mẫu giúp cả vec![1, 2, 3] lẫn vec![1, 2, 3, 4, 5] cùng chạy được với một định nghĩa macro duy nhất.

Một macro có thể có nhiều quy tắc, cách nhau bằng dấu chấm phẩy. Rust thử chúng từ trên xuống dưới và dùng quy tắc đầu tiên khớp. Một quy tắc thậm chí có thể gọi lại chính macro đó. Đây là một max! chạy được với bất kỳ số lượng đối số nào:

macro_rules! max {
// Quy tắc 1: chỉ một giá trị. Giá trị lớn nhất chính là nó.
($x:expr) => {
$x
};
// Quy tắc 2: một giá trị, rồi thêm một hoặc nhiều giá trị khác.
($x:expr, $($rest:expr),+) => {
{
let first = $x;
let biggest_of_rest = max!($($rest),+);
if first > biggest_of_rest { first } else { biggest_of_rest }
}
};
}
fn main() {
println!("{}", max!(7));
println!("{}", max!(3, 9, 4));
println!("{}", max!(2.5, 1.0));
}

Kết quả:

7
9
2.5
▶ Tự chạy thử

Với max!(3, 9, 4), quy tắc 1 không khớp (có nhiều hơn một giá trị), nên quy tắc 2 khớp. Nó giữ 3 sang một bên và mở rộng max!(9, 4), lời gọi này lại mở rộng max!(4). Lời gọi cuối khớp quy tắc 1 và dừng lại. Thứ tự rất quan trọng: Rust luôn thử quy tắc 1 trước.

Có hai mẩu cú pháp nhỏ mới ở đây:

  • + nghĩa là một hoặc nhiều, còn * (trong sum!) nghĩa là không hoặc nhiều. Quy tắc 2 cần ít nhất một giá trị sau dấu phẩy, nên lời gọi max!(...) bên trong luôn có thứ gì đó để xử lý.
  • max! chạy được với cả số nguyên lẫn số thực, vì macro chỉ dán đoạn code first > biggest_of_rest vào. Một hàm sẽ cần generic để làm được như vậy.

expr không phải loại duy nhất mà macro có thể nhận. Một vài loại khác bạn sẽ gặp trong các macro thật:

Loại (fragment) Khớp với Ví dụ
expr bất kỳ biểu thức nào 2 + 3, name.len()
ident một cái tên count, Point
ty một kiểu i32, Vec<String>
literal một giá trị literal 42, "hi"

Bạn sẽ dùng macro liên tục (println!, vec!, format!, #[derive(Debug)]) mà không bao giờ cần tự viết. Viết macro riêng là một kỹ năng khá chuyên biệt, chủ yếu dành cho tác giả thư viện cần loại bỏ những đoạn code lặp đi lặp lại trong một codebase lớn. Biết rằng chúng tồn tại và đại khái hoạt động ra sao là đủ; đó không phải thứ nên dùng sớm.

Viết macro print_all! nhận bất kỳ số lượng giá trị nào, cách nhau bằng dấu phẩy, và in mỗi giá trị trên một dòng riêng. print_all!("táo", 42, 3.5) phải in ra ba dòng. Gợi ý: khớp $($x:expr),* giống như sum!, và lặp lại một println! cho mỗi giá trị bằng $( ... )*.

Xem lời giải
macro_rules! print_all {
($($x:expr),*) => {
$(println!("{}", $x);)*
};
}
fn main() {
print_all!("táo", 42, 3.5);
}

Kết quả:

táo
42
3.5
▶ Tự chạy thử

Macro mở rộng thành ba lời gọi println! riêng biệt, mỗi đối số một lời gọi. Mỗi giá trị có thể có kiểu khác nhau, điều mà một hàm bình thường không xử lý được.

Viết my_vec! hoạt động giống vec!. my_vec![] cho ra một Vec rỗng, còn my_vec![1, 2, 3] cho ra một Vec chứa các phần tử đó. Gợi ý: dùng hai quy tắc. Quy tắc đầu khớp với không có gì cả, (). Quy tắc thứ hai khớp $($x:expr),+, tạo một Vec::new(), rồi đẩy từng $x vào đó.

Xem lời giải
macro_rules! my_vec {
() => {
Vec::new()
};
($($x:expr),+) => {
{
let mut v = Vec::new();
$(v.push($x);)+
v
}
};
}
fn main() {
let empty: Vec<i32> = my_vec![];
let numbers = my_vec![1, 2, 3];
println!("{:?} {:?}", empty, numbers);
}

Kết quả:

[] [1, 2, 3]
▶ Tự chạy thử

Bạn có thể gọi bất kỳ macro nào bằng (), [] hoặc {}. Mọi người thường dùng ngoặc vuông cho các macro dạng danh sách như vec!, nhưng my_vec!(1, 2, 3) cũng chạy được. Vec rỗng cần kiểu : Vec<i32>, vì khi không có phần tử nào, Rust không biết nó sẽ chứa gì.

  • Macro được mở rộng thành code thật trước khi biên dịch; nhờ vậy println! có thể nhận số lượng và kiểu đối số khác nhau mỗi lần được gọi.
  • Dấu ! sau một cái tên luôn là dấu hiệu bạn đang gọi macro, không phải hàm.
  • macro_rules! cho phép bạn tự định nghĩa macro, khớp các mẫu như một biểu thức đơn ($x:expr) hoặc một danh sách lặp lại ($($x:expr),*).
  • Một macro có thể có nhiều quy tắc. Rust thử chúng từ trên xuống dưới, và một quy tắc có thể gọi lại chính macro đó.
  • Viết macro là kỹ năng chuyên biệt; dùng các macro có sẵn trong thư viện chuẩn đã đủ cho gần như mọi thứ bạn cần.

Kiểm tra nhanh

1. Vì sao println! không thể được viết như một hàm bình thường?

2. $x:expr có nghĩa gì trong một mẫu của macro_rules!?

3. Khi nào macro được mở rộng thành code thật?

Điểm: 0 / 3