- زمان مطالعه : 7 دقیقه
ماکروها (Macros) یکی از ویژگیهای قدرتمند زبان Rust هستند که به شما امکان میدهند کدهایی بنویسید که کدهای دیگری تولید کنند. این ویژگی در مواردی مانند حذف کد تکراری، ایجاد DSLهای خاصمنظوره، یا پیادهسازی metaprogramming patterns بسیار کاربردی است. در این مقاله با انواع ماکروها در Rust، مزایا، معایب و نحوهی استفادهی صحیح از آنها آشنا میشویم.
چرا ماکرو؟
ماکروها زمانی بهکار میآیند که کد شما دارای الگوهای تکراری زیادی باشد که با فانکشنها بهراحتی قابل بازنویسی نیستند. برخلاف توابع، ماکروها قبل از مرحلهی کامپایل اجرا میشوند و میتوانند ساختارهای مختلفی از کد را تولید کنند. بهعبارت دیگر، ماکروها به شما اجازه میدهند تا در مرحلهی کامپایل کد تولید کنید.
انواع ماکروها در Rust
Rust از دو نوع اصلی ماکرو پشتیبانی میکند:
1. Declarative Macros (با استفاده از macro_rules!)
این نوع ماکروها که با macro_rules!
تعریف میشوند، از الگوهای تطبیقی برای تولید کد استفاده میکنند. این همان چیزی است که بیشتر توسعهدهندگان Rust در ابتدا با آن برخورد میکنند.
macro_rules! say_hello {
() => {
println!("Hello!");
};
}
fn main() {
say_hello!();
}
ماکرو بالا هر بار که فراخوانی شود، کدی تولید میکند که println!
را اجرا میکند. کاربرد آن بسیار شبیه توابع است، اما با این تفاوت که در مرحلهی pre-processing اجرا میشود.
از قابلیتهای پیشرفتهتر macro_rules!
میتوان به الگوهای چندگانه، تکرار با *
و +
، و پترنمچینگ اشاره کرد. مثلاً:
macro_rules! create_functions {
($($name:ident),*) => {
$(
fn $name() {
println!("You called {:?}!", stringify!($name));
}
)*
}
}
create_functions!(foo, bar, baz);
2. Procedural Macros
ماکروهای پردازشی نوع پیشرفتهتری از ماکروها هستند که امکان اعمال تغییر روی AST (Abstract Syntax Tree) را فراهم میکنند. این ماکروها به سه دسته تقسیم میشوند:
- #[derive]
برای تولید کد بهصورت خودکار روی ساختارها. مثلاً:
#[derive(Debug, Clone, PartialEq)]
struct Point {
x: i32,
y: i32,
}
- Attribute-like Macros
ماکروهایی که با علامت #[]
روی آیتمهایی مثل تابع یا ساختار اعمال میشوند:
#[route(GET, "/")]
fn index() {}
- Function-like Macros
شبیه به macro_rules!
ولی با انعطاف بیشتر. مثلاً:
my_macro!(...);
این نوع ماکروها معمولاً در crate جداگانهای نوشته میشوند و از proc_macro
استفاده میکنند.
نحوهی نوشتن یک ماکرو Procedural ساده
نوشتن ماکروهای procedural نیاز به استفاده از crate proc-macro
دارد. بهطور معمول، این کدها در crate جداگانه با نوع proc-macro = true
تعریف میشوند.
extern crate proc_macro;
use proc_macro::TokenStream;
#[proc_macro]
pub fn make_answer(_item: TokenStream) -> TokenStream {
"fn answer() -> u32 { 42 }".parse().unwrap()
}
مزایای استفاده از ماکروها
کاهش کد تکراری: میتوانید با چند خط ماکرو، صدها خط کد مشابه تولید کنید.
افزایش خوانایی در برخی موارد: ماکروهای خوب طراحیشده میتوانند کدی شفافتر ارائه دهند.
ایجاد DSL: ماکروها امکان ساخت syntax سفارشی را فراهم میکنند.
معایب ماکروها
عیبیابی دشوار: خطاهای کامپایل در ماکروها ممکن است گنگ و پیچیده باشند.
کاهش وضوح کد: اگر بیشازحد از ماکرو استفاده شود، کد میتواند مبهم و غیرقابلخواندن شود.
افزایش زمان کامپایل: چون ماکروها قبل از کامپایل اجرا میشوند، میتوانند زمان build را افزایش دهند.
چه زمانی ماکرو ننویسیم؟
اگر میتوانید با یک تابع ساده یا generic مشکل را حل کنید، ماکرو ننویسید. ماکروها باید آخرین گزینه باشند، نه اولین. آنها ابزاری قدرتمند ولی دو لبهاند: هم میتوانند کدتان را زیباتر کنند، هم آن را به یک هیولای غیرقابل نگهداری تبدیل کنند.
مثال کاربردی: DSL برای تستها
فرض کنید میخواهید یک DSL برای تست بنویسید:
macro_rules! test_case {
($name:ident, $body:block) => {
#[test]
fn $name() $body
};
}
test_case!(simple_addition, {
assert_eq!(2 + 2, 4);
});
نتیجهگیری
ماکروها در Rust ابزاری بسیار قدرتمند برای تولید کد هستند که با درک صحیح و استفادهی هوشمندانه میتوانند کدهای پیچیده را ساده کنند. اما باید با احتیاط از آنها استفاده کرد، چرا که سوءاستفاده از آنها منجر به ایجاد کدی میشود که نه تنها نگهداری آن سخت است، بلکه ممکن است شما را در کامپایلهای نیمساعته غرق کند.
دیدگاههای پیشنهاد شده
دیدگاه خود را ارسال کنید
از استفاده از کلمات رکیک و خلاف قوانین و غیر مرتبط با موضوع خودداری کنید ...
توجه: strong> مطلب ارسالی شما پس از تایید مدیریت برای همه قابل رویت خواهد بود.