Macro
Chúng ta đã sử dụng các macro như println!
trong suốt cuốn sách này, nhưng
chúng ta chưa khám phá đầy đủ về macro là gì và cách nó hoạt động. Thuật ngữ
macro đề cập đến một họ các tính năng trong Rust: macro khai báo với
macro_rules!
và ba loại macro thủ tục:
- Macro
#[derive]
tùy chỉnh chỉ định mã được thêm vào với thuộc tínhderive
được sử dụng trên các struct và enum - Macro giống thuộc tính định nghĩa các thuộc tính tùy chỉnh có thể sử dụng trên bất kỳ mục nào
- Macro giống hàm trông giống như lời gọi hàm nhưng hoạt động trên các token được chỉ định làm đối số của chúng
Chúng ta sẽ nói về từng loại này lần lượt, nhưng trước tiên, hãy xem tại sao chúng ta thậm chí cần macro khi chúng ta đã có hàm.
Sự Khác Biệt Giữa Macro và Hàm
Về cơ bản, macro là một cách để viết mã tạo ra mã khác, điều này được gọi là
lập trình meta. Trong Phụ lục C, chúng ta thảo luận về thuộc tính derive
,
tạo ra một triển khai của các trait khác nhau cho bạn. Chúng ta cũng đã sử dụng
các macro println!
và vec!
trong suốt cuốn sách. Tất cả các macro này mở
rộng để tạo ra nhiều mã hơn so với mã bạn đã viết thủ công.
Lập trình meta hữu ích để giảm lượng mã bạn phải viết và bảo trì, đây cũng là một trong những vai trò của hàm. Tuy nhiên, macro có một số sức mạnh bổ sung mà hàm không có.
Chữ ký hàm phải khai báo số lượng và kiểu của các tham số mà hàm có. Ngược lại,
macro có thể nhận một số lượng tham số thay đổi: chúng ta có thể gọi
println!("hello")
với một đối số hoặc println!("hello {}", name)
với hai đối
số. Ngoài ra, macro được mở rộng trước khi trình biên dịch diễn giải ý nghĩa của
mã, vì vậy một macro có thể, ví dụ, triển khai một trait trên một kiểu nhất
định. Một hàm không thể làm điều này, vì nó được gọi trong thời gian chạy và một
trait cần được triển khai tại thời điểm biên dịch.
Nhược điểm của việc triển khai một macro thay vì một hàm là định nghĩa macro phức tạp hơn định nghĩa hàm vì bạn đang viết mã Rust để viết mã Rust. Do sự gián tiếp này, định nghĩa macro thường khó đọc, khó hiểu và khó bảo trì hơn định nghĩa hàm.
Một sự khác biệt quan trọng khác giữa macro và hàm là bạn phải định nghĩa macro hoặc đưa chúng vào phạm vi trước khi bạn gọi chúng trong một tệp, trái ngược với các hàm mà bạn có thể định nghĩa ở bất kỳ đâu và gọi ở bất kỳ đâu.
Macro Khai Báo với macro_rules!
cho Lập Trình Meta Tổng Quát
Hình thức macro được sử dụng rộng rãi nhất trong Rust là macro khai báo. Đôi
khi chúng còn được gọi là "macro bằng ví dụ," "macro macro_rules!
," hoặc đơn
giản là "macro." Về cơ bản, macro khai báo cho phép bạn viết một cái gì đó tương
tự như một biểu thức match
của Rust. Như đã thảo luận trong Chương 6, biểu
thức match
là cấu trúc điều khiển nhận một biểu thức, so sánh giá trị kết quả
của biểu thức với các mẫu, và sau đó chạy mã liên kết với mẫu phù hợp. Macro
cũng so sánh một giá trị với các mẫu được liên kết với mã cụ thể: trong tình
huống này, giá trị là mã nguồn Rust theo nghĩa đen được truyền cho macro; các
mẫu được so sánh với cấu trúc của mã nguồn đó; và mã liên kết với mỗi mẫu, khi
khớp, thay thế mã được truyền cho macro. Tất cả điều này diễn ra trong quá trình
biên dịch.
Để định nghĩa một macro, bạn sử dụng cấu trúc macro_rules!
. Hãy khám phá cách
sử dụng macro_rules!
bằng cách xem cách định nghĩa macro vec!
. Chương 8 đã
đề cập đến cách chúng ta có thể sử dụng macro vec!
để tạo một vector mới với
các giá trị cụ thể. Ví dụ, macro sau tạo một vector mới chứa ba số nguyên:
#![allow(unused)] fn main() { let v: Vec<u32> = vec![1, 2, 3]; }
Chúng ta cũng có thể sử dụng macro vec!
để tạo một vector của hai số nguyên
hoặc một vector của năm slice chuỗi. Chúng ta sẽ không thể sử dụng một hàm để
làm điều tương tự vì chúng ta sẽ không biết số lượng hoặc kiểu giá trị trước.
Listing 20-35 hiển thị một định nghĩa hơi đơn giản hóa của macro vec!
.
#[macro_export]
macro_rules! vec {
( $( $x:expr ),* ) => {
{
let mut temp_vec = Vec::new();
$(
temp_vec.push($x);
)*
temp_vec
}
};
}
Lưu ý: Định nghĩa thực tế của macro
vec!
trong thư viện chuẩn bao gồm mã để cấp phát trước đúng lượng bộ nhớ. Mã đó là một tối ưu hóa mà chúng ta không bao gồm ở đây, để làm cho ví dụ đơn giản hơn.
Chú thích #[macro_export]
chỉ ra rằng macro này nên được làm cho có sẵn bất cứ
khi nào crate mà macro được định nghĩa trong đó được đưa vào phạm vi. Nếu không
có chú thích này, macro không thể được đưa vào phạm vi.
Sau đó, chúng ta bắt đầu định nghĩa macro với macro_rules!
và tên của macro
chúng ta đang định nghĩa không có dấu chấm than. Tên, trong trường hợp này là
vec
, được theo sau bởi dấu ngoặc nhọn biểu thị thân của định nghĩa macro.
Cấu trúc trong thân vec!
tương tự như cấu trúc của một biểu thức match
. Ở
đây, chúng ta có một nhánh với mẫu ( $( $x:expr ),* )
, theo sau là =>
và
khối mã liên kết với mẫu này. Nếu mẫu khớp, khối mã liên kết sẽ được phát ra. Vì
đây là mẫu duy nhất trong macro này, chỉ có một cách hợp lệ để khớp; bất kỳ mẫu
nào khác sẽ dẫn đến lỗi. Các macro phức tạp hơn sẽ có nhiều hơn một nhánh.
Cú pháp mẫu hợp lệ trong định nghĩa macro khác với cú pháp mẫu được đề cập trong Chương 19 vì các mẫu macro được khớp với cấu trúc mã Rust chứ không phải giá trị. Hãy đi qua ý nghĩa của các phần mẫu trong Listing 20-29; để biết cú pháp mẫu macro đầy đủ, hãy xem Tài liệu tham khảo Rust.
Đầu tiên, chúng ta sử dụng một bộ dấu ngoặc đơn để bao quanh toàn bộ mẫu. Chúng
ta sử dụng dấu đô la ($
) để khai báo một biến trong hệ thống macro sẽ chứa mã
Rust khớp với mẫu. Dấu đô la làm rõ đây là một biến macro chứ không phải một
biến Rust thông thường. Tiếp theo là một bộ dấu ngoặc đơn bắt các giá trị khớp
với mẫu trong dấu ngoặc đơn để sử dụng trong mã thay thế. Trong $()
là
$x:expr
, khớp với bất kỳ biểu thức Rust nào và gán cho biểu thức tên $x
.
Dấu phẩy sau $()
chỉ ra rằng một ký tự phân cách dấu phẩy theo nghĩa đen phải
xuất hiện giữa mỗi thể hiện của mã khớp với mã trong $()
. Dấu *
chỉ định
rằng mẫu khớp với zero hoặc nhiều của bất cứ thứ gì đứng trước dấu *
.
Khi chúng ta gọi macro này với vec![1, 2, 3];
, mẫu $x
khớp ba lần với ba
biểu thức 1
, 2
, và 3
.
Bây giờ, hãy nhìn vào mẫu trong phần thân của mã liên kết với nhánh này:
temp_vec.push()
trong $()*
được tạo ra cho mỗi phần khớp với $()
trong mẫu
zero hoặc nhiều lần tùy thuộc vào số lần mẫu khớp. Biến $x
được thay thế bằng
mỗi biểu thức khớp. Khi chúng ta gọi macro này với vec![1, 2, 3];
, mã được tạo
ra thay thế lời gọi macro này sẽ là như sau:
{
let mut temp_vec = Vec::new();
temp_vec.push(1);
temp_vec.push(2);
temp_vec.push(3);
temp_vec
}
Chúng ta đã định nghĩa một macro có thể nhận bất kỳ số lượng đối số nào với bất kỳ kiểu nào và có thể tạo mã để tạo một vector chứa các phần tử được chỉ định.
Để tìm hiểu thêm về cách viết macro, hãy tham khảo tài liệu trực tuyến hoặc các tài nguyên khác, chẳng hạn như "The Little Book of Rust Macros" do Daniel Keep bắt đầu và Lukas Wirth tiếp tục.
Macro Thủ Tục cho Tạo Mã từ Thuộc Tính
Hình thức thứ hai của macro là macro thủ tục, hoạt động giống như một hàm (và là
một loại thủ tục). Macro thủ tục chấp nhận một số mã làm đầu vào, hoạt động
trên mã đó, và tạo ra một số mã làm đầu ra thay vì khớp với các mẫu và thay thế
mã bằng mã khác như macro khai báo. Ba loại macro thủ tục là derive
tùy chỉnh,
giống thuộc tính, và giống hàm, và tất cả đều hoạt động theo cách tương tự.
Khi tạo macro thủ tục, các định nghĩa phải nằm trong crate riêng của chúng với
một loại crate đặc biệt. Điều này là do lý do kỹ thuật phức tạp mà chúng tôi hy
vọng sẽ loại bỏ trong tương lai. Trong Listing 20-36, chúng tôi cho thấy cách
định nghĩa một macro thủ tục, trong đó some_attribute
là một placeholder cho
việc sử dụng một biến thể macro cụ thể.
use proc_macro;
#[some_attribute]
pub fn some_name(input: TokenStream) -> TokenStream {
}
Hàm định nghĩa một macro thủ tục nhận một TokenStream
làm đầu vào và tạo ra
một TokenStream
làm đầu ra. Kiểu TokenStream
được định nghĩa bởi crate
proc_macro
được bao gồm với Rust và đại diện cho một chuỗi các token. Đây là
cốt lõi của macro: mã nguồn mà macro đang hoạt động trên tạo thành TokenStream
đầu vào, và mã mà macro tạo ra là TokenStream
đầu ra. Hàm cũng có một thuộc
tính được gắn vào nó chỉ định loại macro thủ tục nào chúng ta đang tạo. Chúng ta
có thể có nhiều loại macro thủ tục trong cùng một crate.
Hãy xem xét các loại macro thủ tục khác nhau. Chúng ta sẽ bắt đầu với một macro
derive
tùy chỉnh và sau đó giải thích sự khác biệt nhỏ làm cho các hình thức
khác khác nhau.
Cách Viết Một Macro derive
Tùy Chỉnh
Hãy tạo một crate có tên hello_macro
định nghĩa một trait có tên HelloMacro
với một hàm liên kết có tên hello_macro
. Thay vì yêu cầu người dùng của chúng
ta triển khai trait HelloMacro
cho từng kiểu của họ, chúng ta sẽ cung cấp một
macro thủ tục để người dùng có thể chú thích kiểu của họ với
#[derive(HelloMacro)]
để có được một triển khai mặc định của hàm
hello_macro
. Triển khai mặc định sẽ in Hello, Macro! My name is TypeName!
trong đó TypeName
là tên của kiểu mà trait này đã được định nghĩa trên. Nói
cách khác, chúng ta sẽ viết một crate cho phép một lập trình viên khác viết mã
như Listing 20-37 sử dụng crate của chúng ta.
use hello_macro::HelloMacro;
use hello_macro_derive::HelloMacro;
#[derive(HelloMacro)]
struct Pancakes;
fn main() {
Pancakes::hello_macro();
}
Mã này sẽ in Hello, Macro! My name is Pancakes!
khi chúng ta hoàn thành. Bước
đầu tiên là tạo một crate thư viện mới, như sau:
$ cargo new hello_macro --lib
Tiếp theo, chúng ta sẽ định nghĩa trait HelloMacro
và hàm liên kết của nó:
pub trait HelloMacro {
fn hello_macro();
}
Chúng ta có một trait và hàm của nó. Tại thời điểm này, người dùng crate của chúng ta có thể triển khai trait để đạt được chức năng mong muốn, như trong Listing 20-39.
use hello_macro::HelloMacro;
struct Pancakes;
impl HelloMacro for Pancakes {
fn hello_macro() {
println!("Hello, Macro! My name is Pancakes!");
}
}
fn main() {
Pancakes::hello_macro();
}
Tuy nhiên, họ sẽ cần phải viết khối triển khai cho mỗi kiểu họ muốn sử dụng với
hello_macro
; chúng ta muốn giúp họ không phải làm công việc này.
Ngoài ra, chúng ta chưa thể cung cấp hàm hello_macro
với triển khai mặc định
sẽ in tên của kiểu mà trait được triển khai: Rust không có khả năng phản ánh, vì
vậy nó không thể tra cứu tên kiểu tại thời điểm chạy. Chúng ta cần một macro để
tạo mã tại thời điểm biên dịch.
Bước tiếp theo là định nghĩa macro thủ tục. Tại thời điểm viết bài này, macro
thủ tục cần phải nằm trong crate riêng của chúng. Cuối cùng, hạn chế này có thể
được dỡ bỏ. Quy ước cho việc cấu trúc các crate và crate macro là như sau: đối
với một crate có tên foo
, một crate macro thủ tục derive
tùy chỉnh được gọi
là foo_derive
. Hãy bắt đầu một crate mới có tên hello_macro_derive
bên trong
dự án hello_macro
của chúng ta:
$ cargo new hello_macro_derive --lib
Hai crate của chúng ta có liên quan chặt chẽ với nhau, vì vậy chúng ta tạo crate
macro thủ tục trong thư mục của crate hello_macro
. Nếu chúng ta thay đổi định
nghĩa trait trong hello_macro
, chúng ta cũng sẽ phải thay đổi triển khai của
macro thủ tục trong hello_macro_derive
. Hai crate sẽ cần được xuất bản riêng
biệt, và các lập trình viên sử dụng các crate này sẽ cần thêm cả hai làm phụ
thuộc và đưa cả hai vào phạm vi. Chúng ta có thể thay vào đó cho crate
hello_macro
sử dụng hello_macro_derive
làm phụ thuộc và tái xuất mã macro
thủ tục. Tuy nhiên, cách chúng ta đã cấu trúc dự án làm cho các lập trình viên
có thể sử dụng hello_macro
ngay cả khi họ không muốn chức năng derive
.
Chúng ta cần khai báo crate hello_macro_derive
là một crate macro thủ tục.
Chúng ta cũng sẽ cần chức năng từ các crate syn
và quote
, như bạn sẽ thấy
sau đây, vì vậy chúng ta cần thêm chúng làm phụ thuộc. Thêm phần sau vào tệp
Cargo.toml cho hello_macro_derive
:
[lib]
proc-macro = true
[dependencies]
syn = "2.0"
quote = "1.0"
Để bắt đầu định nghĩa macro thủ tục, đặt mã trong Listing 20-40 vào tệp
src/lib.rs của bạn cho crate hello_macro_derive
. Lưu ý rằng mã này sẽ không
biên dịch cho đến khi chúng ta thêm một định nghĩa cho hàm impl_hello_macro
.
use proc_macro::TokenStream;
use quote::quote;
#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
// Construct a representation of Rust code as a syntax tree
// that we can manipulate.
let ast = syn::parse(input).unwrap();
// Build the trait implementation.
impl_hello_macro(&ast)
}
Lưu ý rằng chúng ta đã chia mã thành hàm hello_macro_derive
, chịu trách nhiệm
phân tích TokenStream
, và hàm impl_hello_macro
, chịu trách nhiệm chuyển đổi
cây cú pháp: điều này làm cho việc viết một macro thủ tục thuận tiện hơn. Mã
trong hàm ngoài (trong trường hợp này là hello_macro_derive
) sẽ giống nhau cho
hầu hết mọi crate macro thủ tục mà bạn thấy hoặc tạo. Mã mà bạn chỉ định trong
thân hàm bên trong (trong trường hợp này là impl_hello_macro
) sẽ khác nhau tùy
thuộc vào mục đích của macro thủ tục của bạn.
Chúng ta đã giới thiệu ba crate mới: proc_macro
, syn
, và quote
. Crate
proc_macro
đi kèm với Rust, vì vậy chúng ta không cần thêm nó vào phụ thuộc
trong Cargo.toml. Crate proc_macro
là API của trình biên dịch cho phép chúng
ta đọc và thao tác mã Rust từ mã của chúng ta.
Crate syn
phân tích mã Rust từ một chuỗi thành một cấu trúc dữ liệu mà chúng
ta có thể thực hiện các thao tác. Crate quote
chuyển đổi các cấu trúc dữ liệu
syn
trở lại mã Rust. Các crate này làm cho việc phân tích bất kỳ loại mã Rust
nào mà chúng ta có thể muốn xử lý trở nên đơn giản hơn nhiều: viết một trình
phân tích cú pháp đầy đủ cho mã Rust không phải là một nhiệm vụ đơn giản.
Hàm hello_macro_derive
sẽ được gọi khi người dùng của thư viện của chúng ta
chỉ định #[derive(HelloMacro)]
trên một kiểu. Điều này có thể vì chúng ta đã
chú thích hàm hello_macro_derive
ở đây với proc_macro_derive
và chỉ định tên
HelloMacro
, khớp với tên trait của chúng ta; đây là quy ước mà hầu hết các
macro thủ tục tuân theo.
Hàm hello_macro_derive
đầu tiên chuyển đổi input
từ một TokenStream
thành
một cấu trúc dữ liệu mà sau đó chúng ta có thể giải thích và thực hiện các thao
tác. Đây là nơi syn
phát huy tác dụng. Hàm parse
trong syn
nhận một
TokenStream
và trả về một cấu trúc DeriveInput
đại diện cho mã Rust đã được
phân tích. Listing 20-41 hiển thị các phần liên quan của cấu trúc DeriveInput
mà chúng ta nhận được khi phân tích chuỗi struct Pancakes;
.
DeriveInput {
// --snip--
ident: Ident {
ident: "Pancakes",
span: #0 bytes(95..103)
},
data: Struct(
DataStruct {
struct_token: Struct,
fields: Unit,
semi_token: Some(
Semi
)
}
)
}
Các trường của cấu trúc này cho thấy rằng mã Rust mà chúng ta đã phân tích là
một struct đơn vị với ident
(định danh, nghĩa là tên) là Pancakes
. Có
nhiều trường hơn trên cấu trúc này để mô tả tất cả các loại mã Rust; kiểm tra
tài liệu syn
cho DeriveInput
để biết thêm thông tin.
Chẳng bao lâu nữa chúng ta sẽ định nghĩa hàm impl_hello_macro
, nơi mà chúng ta
sẽ xây dựng mã Rust mới mà chúng ta muốn bao gồm. Nhưng trước khi chúng ta làm
điều đó, hãy lưu ý rằng đầu ra cho macro derive
của chúng ta cũng là một
TokenStream
. TokenStream
trả về được thêm vào mã mà người dùng crate của
chúng ta viết, vì vậy khi họ biên dịch crate của họ, họ sẽ nhận được chức năng
bổ sung mà chúng ta cung cấp trong TokenStream
đã được sửa đổi.
Bạn có thể đã nhận thấy rằng chúng ta gọi unwrap
để khiến hàm
hello_macro_derive
panic nếu lời gọi hàm syn::parse
không thành công ở đây.
Cần thiết cho macro thủ tục của chúng ta panic về lỗi vì các hàm
proc_macro_derive
phải trả về TokenStream
chứ không phải Result
để phù hợp
với API macro thủ tục. Chúng ta đã đơn giản hóa ví dụ này bằng cách sử dụng
unwrap
; trong mã sản xuất, bạn nên cung cấp thông báo lỗi cụ thể hơn về những
gì đã xảy ra sai bằng cách sử dụng panic!
hoặc expect
.
Bây giờ chúng ta đã có mã để chuyển đổi mã Rust được chú thích từ một
TokenStream
thành một thể hiện DeriveInput
, hãy tạo mã triển khai trait
HelloMacro
trên kiểu được chú thích, như hiển thị trong Listing 20-42.
use proc_macro::TokenStream;
use quote::quote;
#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
// Construct a representation of Rust code as a syntax tree
// that we can manipulate
let ast = syn::parse(input).unwrap();
// Build the trait implementation
impl_hello_macro(&ast)
}
fn impl_hello_macro(ast: &syn::DeriveInput) -> TokenStream {
let name = &ast.ident;
let generated = quote! {
impl HelloMacro for #name {
fn hello_macro() {
println!("Hello, Macro! My name is {}!", stringify!(#name));
}
}
};
generated.into()
}
Chúng ta nhận được một thể hiện của cấu trúc Ident
chứa tên (định danh) của
kiểu được chú thích bằng cách sử dụng ast.ident
. Cấu trúc trong Listing 20-33
cho thấy rằng khi chúng ta chạy hàm impl_hello_macro
trên mã trong Listing
20-31, ident
mà chúng ta nhận được sẽ có trường ident
với giá trị là
"Pancakes"
. Do đó, biến name
trong Listing 20-34 sẽ chứa một thể hiện của
cấu trúc Ident
mà, khi được in ra, sẽ là chuỗi "Pancakes"
, tên của struct
trong Listing 20-37.
Macro quote!
cho phép chúng ta định nghĩa mã Rust mà chúng ta muốn trả về.
Trình biên dịch mong đợi một thứ khác với kết quả trực tiếp của thực thi macro
quote!
, vì vậy chúng ta cần chuyển đổi nó thành một TokenStream
. Chúng ta
làm điều này bằng cách gọi phương thức into
, tiêu thụ biểu diễn trung gian này
và trả về một giá trị của kiểu TokenStream
cần thiết.
Macro quote!
cũng cung cấp một số cơ chế mẫu rất tuyệt: chúng ta có thể nhập
#name
, và quote!
sẽ thay thế nó bằng giá trị trong biến name
. Bạn thậm chí
có thể làm một số lặp lại tương tự như cách hoạt động của các macro thông
thường. Kiểm tra tài liệu của crate quote
để có một giới thiệu
kỹ lưỡng.
Chúng ta muốn macro thủ tục của chúng ta tạo ra một triển khai của trait
HelloMacro
cho kiểu mà người dùng đã chú thích, đó là những gì chúng ta có thể
nhận được bằng cách sử dụng #name
. Việc triển khai trait có một hàm
hello_macro
, thân của nó chứa chức năng mà chúng ta muốn cung cấp: in
Hello, Macro! My name is
và sau đó là tên của kiểu được chú thích.
Macro stringify!
được sử dụng ở đây là được tích hợp sẵn trong Rust. Nó lấy
một biểu thức Rust, chẳng hạn như 1 + 2
, và vào thời điểm biên dịch chuyển đổi
biểu thức thành một chuỗi theo nghĩa đen, chẳng hạn như "1 + 2"
. Điều này khác
với format!
hoặc println!
, các macro đánh giá biểu thức và sau đó chuyển đổi
kết quả thành một String
. Có khả năng là đầu vào #name
có thể là một biểu
thức để in ra theo nghĩa đen, vì vậy chúng ta sử dụng stringify!
. Sử dụng
stringify!
cũng tiết kiệm một phân bổ bằng cách chuyển đổi #name
thành một
chuỗi theo nghĩa đen vào thời điểm biên dịch.
Tại thời điểm này, cargo build
nên hoàn thành thành công trong cả
hello_macro
và hello_macro_derive
. Hãy kết nối các crate này với mã trong
Listing 20-31 để xem macro thủ tục hoạt động! Tạo một dự án nhị phân mới trong
thư mục projects của bạn bằng cách sử dụng cargo new pancakes
. Chúng ta cần
thêm hello_macro
và hello_macro_derive
làm phụ thuộc trong crate pancakes
của Cargo.toml. Nếu bạn đang xuất bản phiên bản của hello_macro
và
hello_macro_derive
lên crates.io, chúng sẽ là phụ thuộc
thông thường; nếu không, bạn có thể chỉ định chúng như phụ thuộc path
như sau:
hello_macro = { path = "../hello_macro" }
hello_macro_derive = { path = "../hello_macro/hello_macro_derive" }
Đặt mã trong Listing 20-37 vào src/main.rs, và chạy cargo run
: nó sẽ in
Hello, Macro! My name is Pancakes!
Việc triển khai trait HelloMacro
từ macro
thủ tục đã được bao gồm mà không cần crate pancakes
phải triển khai nó;
#[derive(HelloMacro)]
đã thêm việc triển khai trait.
Tiếp theo, hãy khám phá cách các loại macro thủ tục khác khác với macro derive
tùy chỉnh.
Macro Giống Thuộc Tính
Macro giống thuộc tính tương tự như macro derive
tùy chỉnh, nhưng thay vì tạo
mã cho thuộc tính derive
, chúng cho phép bạn tạo thuộc tính mới. Chúng cũng
linh hoạt hơn: derive
chỉ hoạt động cho struct và enum; thuộc tính có thể được
áp dụng cho các mục khác, chẳng hạn như hàm. Đây là một ví dụ về việc sử dụng
một macro giống thuộc tính. Giả sử bạn có một thuộc tính tên là route
chú
thích các hàm khi sử dụng một framework ứng dụng web:
#[route(GET, "/")]
fn index() {
Thuộc tính #[route]
này sẽ được định nghĩa bởi framework như một macro thủ
tục. Chữ ký của hàm định nghĩa macro sẽ trông như thế này:
#[proc_macro_attribute]
pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {
Tại đây, chúng ta có hai tham số kiểu TokenStream
. Tham số đầu tiên là cho nội
dung của thuộc tính: phần GET, "/"
. Tham số thứ hai là cho phần thân của mục
mà thuộc tính được gắn vào: trong trường hợp này, fn index() {}
và phần còn
lại của thân hàm.
Ngoài ra, macro giống thuộc tính hoạt động theo cách tương tự như macro derive
tùy chỉnh: bạn tạo một crate với loại crate proc-macro
và triển khai một hàm
tạo ra mã bạn muốn!
Macro Giống Hàm
Macro giống hàm định nghĩa các macro trông giống như lời gọi hàm. Tương tự như
macro macro_rules!
, chúng linh hoạt hơn các hàm; ví dụ, chúng có thể nhận một
số lượng đối số không xác định. Tuy nhiên, macro macro_rules!
chỉ có thể được
định nghĩa bằng cách sử dụng cú pháp giống match mà chúng ta đã thảo luận trong
"Macro Khai Báo với macro_rules!
cho Lập Trình Meta Tổng
Quát" trước đó. Macro giống hàm nhận một tham số
TokenStream
và định nghĩa của chúng thao tác trên TokenStream
đó bằng mã
Rust giống như hai loại macro thủ tục khác. Một ví dụ về một macro giống hàm là
macro sql!
có thể được gọi như sau:
let sql = sql!(SELECT * FROM posts WHERE id=1);
Macro này sẽ phân tích câu lệnh SQL bên trong nó và kiểm tra xem nó có đúng cú
pháp hay không, đây là quá trình xử lý phức tạp hơn nhiều so với những gì một
macro macro_rules!
có thể làm. Macro sql!
sẽ được định nghĩa như sau:
#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream {
Định nghĩa này tương tự như chữ ký của macro derive
tùy chỉnh: chúng ta nhận
các token nằm trong dấu ngoặc đơn và trả về mã mà chúng ta muốn tạo ra.
Tóm Tắt
Whew! Bây giờ bạn đã có một số tính năng Rust trong hộp công cụ của mình mà bạn có thể sẽ không sử dụng thường xuyên, nhưng bạn sẽ biết chúng có sẵn trong những tình huống rất cụ thể. Chúng ta đã giới thiệu một số chủ đề phức tạp để khi bạn gặp chúng trong các gợi ý thông báo lỗi hoặc trong mã của người khác, bạn sẽ có thể nhận ra các khái niệm và cú pháp này. Sử dụng chương này như một tài liệu tham khảo để hướng dẫn bạn đến các giải pháp.
Tiếp theo, chúng ta sẽ đưa tất cả những gì chúng ta đã thảo luận trong suốt cuốn sách vào thực hành và thực hiện một dự án nữa!