Tüm Rust rehberleri
TEKNİK REHBER İLERİ MAKRO 2026

Makrolar
macro_rules! ve türetme

Bildirimsel (macro_rules!) ve prosedürel makrolar, türetme (derive) makroları; cargo expand — C önişlemci #define ile karşılaştırmalı, hijyenik ve AST-farkında.

00 C #define'dan Rust makrolarına

C önişlemcisi metin üzerinde çalışır: derleyici makroyu görmez, sadece sonucunu görür. Rust makroları ise token akışı (token stream) ve AST düzeyinde, derleyicinin içinde çalışır. Bu tek fark, C'de bildiğin her makro tuzağını ortadan kaldırır.

#define bir metin değiştirme motorudur. Önişlemci tipleri, kapsamı, operatör önceliğini bilmez; sadece karakter dizilerini başka karakter dizileriyle değiştirir. Bundan dolayı klasik C makro tuzakları doğar: parantezlenmemiş argümanlar yüzünden bozulan operatör önceliği, argümanın iki kez yazılmasından doğan çift değerlendirme (double evaluation), ve makro içindeki geçici isimlerin çağıran kapsama sızması.

klasik_tuzak.c
// Üç tuzak birden: öncelik, çift değerlendirme, tip yok
#define SQUARE(x) x * x         // SQUARE(a+b) -> a + b*a + b  (öncelik bozuk)
#define MAX(a,b) ((a) > (b) ? (a) : (b))

int main(void) {
    int i = 1;
    // i++ iki kez değerlendirilir: tanımsız davranışa yakın
    int m = MAX(i++, 5);    // i beklenmedik biçimde artar
    return m;
}

Rust makroları bunların hiçbirini yaşamaz. Bir Rust makrosu lexer'dan geçmiş token ağacı (token tree) üzerinde çalışır, çıktısı tekrar derleyicinin sözdizimi denetiminden geçer. Argümanlar parantez/öncelik açısından bütünlüklü birer ifade olarak taşınır — $x:expr ile yakaladığın şey bir karakter dizisi değil, ayrıştırılmış bir ifadedir. Üstelik makrolar hijyeniktir (hygiene): makronun içinde tanımladığın bir let değişkeni, çağıran kodun değişkenleriyle asla çakışmaz.

C #define              Rust makro
─────────────          ─────────────
ham metin       →      token tree (lexed)
önişlemci       →      derleyici içi genişletme
kapsam sızar    →      hijyenik (kapsam korunur)
tip/AST yok     →      :expr :ty :ident  (AST farkında)
çıktı denetsiz  →      çıktı yeniden ayrıştırılır

Peki makro her zaman doğru araç mı? Hayır. Çalışma zamanı değeri alıp değer döndüren sıradan işler için fonksiyon, tip üzerinde soyutlama için generic tercih edilir — bunlar daha iyi hata mesajı, daha iyi IDE desteği verir. Makroya yalnızca dilin izin vermediğini yapman gerektiğinde uzan: değişken sayıda argüman (variadic), derleme zamanında format/tip denetimi, ya da boilerplate kod üretimi.

NOT

Pratik kural: "Bunu bir fonksiyon ya da generic ile yapabiliyor muyum?" sorusunun cevabı evet ise makro yazma. Makro, dilin sözdizimini genişletmek için son çaredir — daha güçlü ama hata ayıklaması ve okuması daha zordur.

Bu bölümde

  • C önişlemcisi metin değiştirir; tip, kapsam, öncelik bilmez
  • Klasik tuzaklar: öncelik bozulması, çift değerlendirme, kapsam sızıntısı
  • Rust makroları token/AST düzeyinde, hijyenik ve denetimli çalışır
  • Fonksiyon/generic yetmediğinde (variadic, kod üretimi) makroya geç

01 macro_rules! temel

macro_rules! bildirimsel (declarative) makro tanımlar: bir dizi kuraldan oluşur, her kural bir matcher (eşleştirici) ile bir transcriber (üretici) çiftidir. Çağrı bir desene uyarsa, ona karşılık gelen gövdeye genişler.

Yapı şudur: (desen) => { üretilecek_kod };. Sol taraf çağrıyı eşler, ok => ile ayrılır, sağ taraf genişletilecek koddur. Bu, C'deki #define ile yüzeysel benzer görünür ama tamamen farklıdır: desen tarafı tip-farkında token kalıplarıdır, üretici tarafı yeniden derlenecek gerçek Rust kodudur.

main.rs
// Tek kurallı en basit makro. $x bir ifadeyi yakalar.
macro_rules! kare {
    // matcher        =>  transcriber
    ($x:expr) => { $x * $x };
}

fn main() {
    // C SQUARE'in aksine: $x bütün bir ifade olarak taşınır
    let a = kare!(3);         // 9
    let b = kare!(2 + 1);     // 9, parantez sorunu YOK
    println!("{a} {b}");
}

Önemli ayrım: kare!(2 + 1) çağrısında $x tek bir ifade düğümü olarak (2 + 1) şeklinde yakalanır. Genişleme aslında (2 + 1) * (2 + 1) gibi davranır; C'deki 2 + 1 * 2 + 1 öncelik kazası olmaz. Çünkü :expr ile yakalanan parça artık bölünmez bir AST düğümüdür.

Makro derleme zamanında genişler. kare!(3)'ün yerine derleyici doğrudan 3 * 3 kodunu koyar; çalışma zamanında "makro" diye bir varlık kalmaz. Bu yüzden makro çağrısının çağrı maliyeti sıfırdır — fonksiyon çağrısı değil, yerinde açılan koddur.

macro_rules!Bildirimsel makro tanımlayan anahtar sözcük.
=>Matcher ile transcriber'ı ayırır.
!Çağrıdaki ünlem işareti: bu bir makro, fonksiyon değil.
NOT

Makro çağrısını kare!(...), kare![...] veya kare!{...} biçiminde yapabilirsin; üç ayraç da geçerlidir. Gelenek: ifade benzeri için (), koleksiyon için [] (örn. vec![]), blok benzeri için {}.

Bu bölümde

  • macro_rules! kuralları matcher => transcriber çiftleridir
  • :expr ile yakalanan argüman bölünmez bir AST düğümüdür (öncelik güvenli)
  • Makro derleme zamanında genişler; çalışma zamanı maliyeti yoktur
  • Çağrıda ( ), [ ], { } ayraçlarının üçü de geçerlidir

02 Fragment belirteçleri

Matcher içindeki $ad:tür ifadesinde tür, bir fragment specifier'dır (fragment belirteci). Yakaladığın token grubunun hangi gramer kuralına uyması gerektiğini belirtir. Bu, C makrolarında imkânsız olan tip-farkındalığın kalbidir.

Her belirteç farklı bir gramer parçasını eşler. Yanlış belirteç seçmek derleme hatası verir — bu iyidir, çünkü hata makro tanımında yakalanır, çağrının derinlerinde değil. En sık kullanılan belirteçler:

BelirteçEşlediğiÖrnek
:exprbir ifade2 + 1, foo()
:identtanımlayıcı / anahtar sözcükx, main
:tybir tipu32, Vec<T>
:tttek token ağacı (en esnek)+, (a b)
:blockküme parantezli blok{ let x = 1; x }
:patbir desen (pattern)Some(x), _
:literalliteral değer42, "sa"
:pathnitelikli yolstd::mem::swap
main.rs
// :ident ile değişken adı, :ty ile tip, :expr ile başlangıç üretiyoruz
macro_rules! degisken_yarat {
    ($ad:ident : $tip:ty = $ilk:expr) => {
        let $ad: $tip = $ilk;
    };
}

fn main() {
    degisken_yarat!(sayac : u32 = 0);   // let sayac: u32 = 0;
    degisken_yarat!(isim : &str = "x3");
    println!("{sayac} {isim}");
}
DİKKAT

:tt (token tree) en esnek belirteçtir ama en az denetimlidir — neredeyse her şeyi yakalar. Karmaşık makrolarda recursive desenler için gerekir, fakat mümkünse :expr, :ty gibi daha dar belirteçleri tercih et: daha iyi hata mesajı üretirler.

NOT

Belirteçlerin ardından gelen token'a kısıtlama vardır (follow-set kuralı): örneğin :expr'den sonra yalnızca =>, , veya ; gelebilir. Derleyici bunu zorlar; gelecekte makronun anlamı kaymasın diye konmuş bir güvenliktir.

Bu bölümde

  • $ad:belirteç yakalanan parçanın gramer kuralını sabitler
  • :expr :ident :ty :tt :block :pat :literal :path en sık kullanılanlar
  • :tt en esnek ama en denetimsizdir; mümkünse dar belirteç seç
  • Yanlış belirteç hatayı çağrıda değil, makro tanımında yakalar

03 Tekrar (repetition)

C'de değişken argümanlı makro (__VA_ARGS__) sefil bir deneyimdir. Rust'ta $(...) tekrar sözdizimi ile değişken sayıda argümanı temiz biçimde eşler ve aynı şekilde geri yayarsın (expand).

Tekrar üç parçadan oluşur: tekrarlanan desen $( ... ), isteğe bağlı bir ayraç (separator), ve bir tekrar operatörü. Operatörler C regex'ine benzer:

SözdizimiAnlamı
$(...),*virgülle ayrılmış, sıfır veya daha fazla
$(...)*ayraçsız, sıfır veya daha fazla
$(...)+bir veya daha fazla (en az bir)
$(...)?sıfır veya bir (opsiyonel)

Püf nokta: matcher'da $(...),* ile yakaladığın şeyi, transcriber'da yine $(...)* ile geri yayarsın. Derleyici her tekrar için gövdeyi bir kez üretir. Aşağıda vec!'e benzer kendi koleksiyon makromuzu yazıyoruz:

main.rs
// Değişken sayıda ifadeyi alıp bir Vec kuran makro
macro_rules! dizi {
    // $x:expr'i virgülle, sıfır+ kez yakala
    ( $( $x:expr ),* ) => {
        {
            let mut v = Vec::new();
            // Her yakalanan $x için push satırını bir kez üret
            $( v.push($x); )*
            v
        }
    };
}

fn main() {
    let v = dizi![10, 20, 30];   // vec![10,20,30] gibi
    println!("{v:?}");            // [10, 20, 30]
}

Çağrı dizi![10, 20, 30] için makro üç kez v.push(...) üretir. $( ... ),* sondaki virgülü de hoş görür (trailing comma), çünkü * sıfır veya daha fazla anlamına gelir ve son ayraç opsiyoneldir. Bu, gerçek vec!'in nasıl çalıştığının basitleştirilmiş halidir.

NOT

Birden fazla tekrar değişkenini birlikte yayabilirsin: $( $k:expr => $v:expr ),* deseni anahtar/değer çiftlerini eşler ve transcriber'da $( map.insert($k, $v); )* ile ikisini paralel yayarsın. İkisinin tekrar sayısı eşit olmalıdır.

Bu bölümde

  • $( ... )sep* / + / ? ile değişken sayıda token eşlenir
  • * sıfır+, + bir+, ? sıfır-veya-bir; ayraç (örn. virgül) opsiyoneldir
  • Matcher'da yakalanan tekrar, transcriber'da $(...)* ile geri yayılır
  • vec!-benzeri variadic makrolar bu mekanizmayla yazılır

04 Hijyen

Hijyen (hygiene), bir makronun içinde ürettiği isimlerin çağıran koddaki isimlerle asla çakışmamasıdır. C önişlemcisinde bu kavram yoktur; Rust'ta dilin garantisidir. Bu, C'den gelen biri için en çarpıcı farktır.

Önce C felaketini görelim. Klasik bir SWAP makrosu içinde geçici bir tmp kullanır; eğer çağıran kodun da tmp adlı bir değişkeni varsa, makro onu sessizce gölgeler ya da bozar:

kapsam_sizar.c
#define SWAP(a, b) { int tmp = a; a = b; b = tmp; }

int main(void) {
    int tmp = 7, y = 9;
    // Makronun 'tmp'si çağıranın 'tmp'siyle çakışır — bug
    SWAP(tmp, y);   // genişleyince iç içe iki tmp
    return tmp;
}

Rust'ta aynı durum imkânsızdır. Makro içinde tanımlanan değişken, makronun kendi "sözdizimsel bağlamına" (syntax context) aittir; çağıranın aynı adlı değişkeni tamamen ayrı sayılır. Derleyici bunları farklı renklerle işaretlenmiş gibi düşünür:

main.rs
macro_rules! takas {
    ($a:expr, $b:expr) => {{
        let tmp = $a;   // bu 'tmp' makroya ait, çağırana sızmaz
        $a = $b;
        $b = tmp;
    }};
}

fn main() {
    let mut tmp = 7;   // çağıranın 'tmp'si — DOKUNULMAZ
    let mut y = 9;
    takas!(tmp, y);
    // İki 'tmp' farklı bağlamda; çakışma yok
    println!("{tmp} {y}");   // 9 7
}

Burada makronun içindeki tmp ile main'deki tmp aynı yazılmış olmasına rağmen derleyici için farklı iki değişkendir. Hijyen sayesinde takas doğru çalışır ve çağıranın tmp'si bozulmaz. C'deki ad çakışması sınıfı baştan yok olur.

Hijyen yalnızca yerel değişkenleri değil, yolları (path) da kapsar. Makron Vec, Option gibi bir öğeye atıfta bulunuyorsa, çağıran modülde aynı adda bir tip tanımlıysa karışıklık olur. Çözüm $crate metadeğişkenidir: makronun tanımlandığı crate'in köküne sabit bir yol verir.

lib.rs
// $crate, makro hangi crate'te tanımlıysa onun köküne çözülür
// Çağıranın kendi 'Hata' tipi olsa bile doğru tip kullanılır
macro_rules! sonuc_olustur {
    ($v:expr) => { $crate::Sonuc::Tamam($v) };
}
NOT

Yol hijyeni tam değildir — Rust yerel değişken hijyenini güçlü garanti eder ama tipler/yollar için $crate kullanmak senin sorumluluğundur. Yayımlanan (public) bir makro yazıyorsan, içerideki her standart tip ve fonksiyon yoluna $crate:: ya da ::std:: tam yol ver.

Bu bölümde

  • Hijyen: makro içi isimler çağıranın isimleriyle çakışmaz
  • C'deki tmp/SWAP gölgeleme bug'ı Rust'ta tasarımdan dolayı yok
  • Aynı yazılan iki değişken farklı sözdizimsel bağlamlara ait sayılır
  • $crate ile yol hijyeni sağlanır; public makrolarda tam yol kullan

05 Yaygın yerleşik makrolar

Rust standart kütüphanesinin en sık kullanılan araçlarının çoğu aslında makrodur. Neden? Çünkü hepsi fonksiyonun yapamadığı bir şey ister: değişken sayıda argüman ya da derleme-zamanı denetim.

println!, format! ve vec! birer makrodur, fonksiyon değil — bu yüzden çağrıda ! görürsün. Bir fonksiyon değişken sayıda argüman ya da derleme zamanında doğrulanan bir format string'i kabul edemezdi; makrolar edebilir.

MakroİşNeden makro
println!biçimli yazdırmavariadic + format denetimi
format!String üretmevariadic + format denetimi
vec!Vec kurmavariadic kurucu
assert!çalışma-zamanı koşulbaşarısızlıkta ifade metnini basar
dbg!ifade + değer ayıklamadosya/satır + ifade metni
matches!desen eşleşme testidesen (pat) argümanı alır
todo! / unimplemented!panik yer tutucuasla-dönmez (!) tip
main.rs
fn main() {
    let ad = "x3";
    // format string DERLEME ZAMANINDA denetlenir:
    // eksik argüman veya yanlış {} derleme hatasıdır
    println!("merhaba {ad}");

    let v = vec![1, 2, 3];
    // dbg! ifadeyi VE değerini, dosya:satır ile basar, değeri döndürür
    let toplam = dbg!(v.iter().sum::<i32>());

    // matches! bir desen alır; if let'in kısa hâli
    let bos = matches!(v.first(), Some(&1));   // true
    assert!(toplam == 6, "toplam 6 olmalı");
    println!("{bos}");
}

En kritik kazanç format denetimidir: println!("{}", x) içinde argüman sayısı veya tipi yanlışsa, hata derleme zamanında çıkar — çalışma zamanında değil. C'deki printf format string'i bir runtime karakter dizisidir; tip uyuşmazlığı çoğu zaman fark edilmez ve UB'ye yol açar. Rust'ta bu sınıf hata baştan imkânsızdır çünkü format string'i makro tarafından derleme anında ayrıştırılır.

NOT

dbg! hem ifadenin metnini hem değerini, hem de dosya/satır numarasını stderr'e basar ve değeri geri döndürür — yani bir ifadenin ortasına dbg!(...) sarmalayıp akışı bozmadan ayıklama yapabilirsin. Bunu bir fonksiyon yapamaz; çünkü argümanın kaynak metnine erişmesi gerekir.

Bu bölümde

  • println!/format!/vec!/assert!/dbg!/matches!/todo! birer makrodur
  • Makro olmalarının sebebi: variadic argüman veya derleme-zamanı denetim
  • Format string derleme anında denetlenir; printf'in runtime UB'si yok
  • dbg! ifade metnine + dosya/satıra erişir; fonksiyon bunu yapamaz

06 cargo expand

Makro hatasını ayıklamanın en güçlü yolu, makronun ürettiği gerçek kodu görmektir. cargo expand tüm makroları açıp ortaya çıkan düz Rust kodunu basar — C'de gcc -E ile önişlemci çıktısını görmenin Rust karşılığıdır.

Bir makro beklenmedik davrandığında, kafa karıştırıcı hata mesajları yerine doğrudan üretilen koda bakmak istersin. cargo expand tam bunu yapar: kaynaktaki vec![...], #[derive(...)] ve kendi makrolarını çözüp altlarındaki gerçek kodu gösterir. Önce kurulur:

terminal
# Tek seferlik kurulum (cargo alt komutu olarak eklenir)
cargo install cargo-expand

# Tüm crate'i genişlet ve düz Rust kodunu bas
cargo expand

# Sadece belirli bir öğeyi genişlet (ör. main fonksiyonu)
cargo expand main

Örneğin let v = vec![1, 2, 3]; satırını içeren bir program cargo expand ile çalıştırıldığında, makronun açıldığı gerçek kodu görürsün — kabaca şuna benzer:

cargo expand çıktısı
// vec![1, 2, 3] bu hâle genişler (sadeleştirilmiş):
let v = {
    let mut tmp = ::alloc::vec::Vec::new();
    tmp.push(1);
    tmp.push(2);
    tmp.push(3);
    tmp
};
NOT

cargo expand nightly toolchain'in iç API'sini kullanır ama kararlı (stable) Rust üzerinde de çalışır; gerekirse cargo +nightly expand ile açıkça nightly seçebilirsin. Çıktının okunması için rustfmt kuruluysa otomatik biçimlendirilir.

DİKKAT

Genişletilmiş çıktı tam derlenebilir Rust değildir — bazı iç (internal) yollar ve öznitelikler içerir. Amacı okumak ve anlamaktır, kopyalayıp derlemek değil. Kendi macro_rules! makronu ayıklarken ilk başvuracağın araç budur.

Bu bölümde

  • cargo expand makroların ürettiği düz Rust kodunu gösterir
  • C'deki gcc -E (önişlemci çıktısı) görmenin Rust karşılığıdır
  • Kurulum: cargo install cargo-expand; kullanım: cargo expand [öğe]
  • Makro ayıklamanın ilk aracı; çıktı okumak içindir, derlemek için değil

07 Prosedürel makro türleri

macro_rules! desen-eşleme tabanlı bildirimsel bir araçtır. Prosedürel makrolar (procedural macros) ise farklı bir cins: girdiyi bir TokenStream olarak alıp, üzerinde gerçek Rust kodu çalıştırarak çıktı TokenStream üreten fonksiyonlardır. Esneklik tavanı çok daha yüksektir.

Aradaki temel fark: bildirimsel makro bir desene bakar ve şablon doldurur; prosedürel makro ise bir derleyici eklentisi gibidir — token'ları ayrıştırır, mantık yürütür, istediği kodu programatik olarak üretir. Üç türü vardır:

TürÇağrı biçimiKullanım
function-likebenimki!(...)macro_rules! gibi ama TokenStream işler
attribute#[benimki]öğeyi sarmalar/dönüştürür (örn. route)
derive#[derive(Benimki)]tip için trait impl üretir

Prosedürel makrolar ayrı bir cratete yaşamak zorundadır ve o crate'in Cargo.toml'unda proc-macro = true işaretlenmelidir. Sebep teknik: bu makrolar derleyicinin kendisi tarafından, hedef kodu derlemeden önce çalıştırılır; dolayısıyla derleyici-eklentisi gibi ayrı derlenmeleri gerekir.

Cargo.toml
# Prosedürel makro crate'i bu satırı ZORUNLU kılar
[lib]
proc-macro = true

[dependencies]
syn = "2"      # TokenStream -> AST ayrıştırma
quote = "1"    # AST -> TokenStream üretme
NOT

Prosedürel makro yazarken neredeyse her zaman iki crate kullanılır: syn gelen TokenStream'i yapısal bir AST'ye ayrıştırır, quote ise quote! { ... } şablonuyla kolayca TokenStream üretmeni sağlar. İkisi proc-macro ekosisteminin fiilî standardıdır.

DİKKAT

Prosedürel makro gücü pahalıya gelir: ayrı crate, derleme süresine ek yük, ve macro_rules!'a göre belirgin biçimde daha fazla karmaşıklık. Basit desen-değiştirme işleri için macro_rules! yeterliyse onu seç; proc-macro'yu gerçekten gerektiğinde kullan.

Bu bölümde

  • Prosedürel makro TokenStream alır, kod çalıştırır, TokenStream üretir
  • Üç tür: function-like, attribute, derive
  • Ayrı crate + Cargo.toml'da proc-macro = true zorunludur
  • Pratikte syn (ayrıştırma) + quote (üretme) ikilisi kullanılır

08 Türetme (derive) makroları

#[derive(Debug, Clone)] yazdığında aslında prosedürel derive makroları çağırırsın. Bunlar tipinin yapısına bakıp, ilgili trait'in impl bloğunu senin yerine otomatik üretir. Boilerplate'i sıfıra indirmenin yoludur.

Bir #[derive(Trait)] özniteliği, ilgili derive makrosuna tipin tüm tanımını TokenStream olarak verir. Makro bu tanımı ayrıştırır (alanları, generic'leri okur) ve trait'i o tip için gerçekleyen kodu üretir. Debug, Clone, PartialEq gibi standart trait'lerin hepsi bu mekanizmayla gelir.

main.rs
// Üç satır el yazısı impl yerine tek satır türetme
#[derive(Debug, Clone, PartialEq)]
struct Nokta { x: i32, y: i32 }

fn main() {
    let a = Nokta { x: 1, y: 2 };
    let b = a.clone();          // Clone türetildi
    println!("{a:?}");            // Debug türetildi -> Nokta { x: 1, y: 2 }
    assert!(a == b);            // PartialEq türetildi
}

Kendi özel derive makronu yazmanın iskeleti kavramsal olarak şöyledir: syn ile tip tanımını ayrıştır, ilgi duyduğun bilgiyi (tip adı, alanlar) çıkar, quote! ile impl kodunu üret. Aşağıdaki kısa örnek, bir tipin adını döndüren basit bir AdVer trait'ini türetir:

derive_crate/lib.rs
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};

// #[derive(AdVer)] bu fonksiyonu tetikler
#[proc_macro_derive(AdVer)]
pub fn ad_ver_turet(girdi: TokenStream) -> TokenStream {
    // 1) Token'ları yapısal AST'ye ayrıştır
    let ast = parse_macro_input!(girdi as DeriveInput);
    let ad = &ast.ident;          // tipin adı

    // 2) impl bloğunu quote! ile üret
    let uretilen = quote! {
        impl AdVer for #ad {
            fn ad(&self) -> &'static str {
                stringify!(#ad)
            }
        }
    };
    uretilen.into()              // 3) TokenStream olarak geri ver
}
parse_macro_input!TokenStream'i syn AST'sine (DeriveInput) çevirir.
#adquote! içinde değişken enjeksiyonu (interpolation).
stringify!Bir token'ı derleme anında &str'e çevirir.
NOT

Özel derive ne zaman değer? Aynı boilerplate impl'i onlarca tipe elle yazman gerektiğinde. Serde'nin #[derive(Serialize, Deserialize)]'ı bunun en bilinen örneğidir — yüzlerce satır serileştirme kodunu tek satıra indirir.

Bu bölümde

  • #[derive(...)] derive makrolarını çağırır; trait impl'i otomatik üretir
  • Debug/Clone/PartialEq gibi standart trait'ler bu yolla gelir
  • Özel derive: syn ile ayrıştır, quote! ile impl üret, TokenStream döndür
  • Çok sayıda tipe aynı boilerplate gerektiğinde değer kazanır (örn. serde)

09 Gerçek örnek

Teoriyi tek bir faydalı makroda toplayalım: anahtar/değer çiftlerinden HashMap kuran bir harita! makrosu. Tekrar, fragment belirteçleri ve hijyen burada bir arada çalışır — ve cargo expand ile ne ürettiğini görürüz.

Python'daki {k: v} sözdiziminin Rust karşılığı yoktur; standart yol HashMap::new() + tekrarlı insert'tür. Bunu tek satıra indiren bir makro hem variadic'tir hem de $k => $v çift desenini paralel yayar:

main.rs
use std::collections::HashMap;

// $k => $v çiftlerini virgülle, sıfır+ kez eşle
macro_rules! harita {
    ( $( $k:expr => $v:expr ),* $(,)? ) => {{
        let mut m = HashMap::new();
        // her çift için bir insert üret (paralel yayım)
        $( m.insert($k, $v); )*
        m
    }};
}

fn main() {
    let yaslar = harita!{
        "ali" => 30,
        "veli" => 25,   // sondaki virgül $(,)? ile serbest
    };
    println!("{:?}", yaslar.get("ali"));   // Some(30)
}

Burada üç teknik birleşir: $( ... ),* değişken sayıda çifti eşler, $(,)? sondaki opsiyonel virgülü hoş görür, ve makro içindeki m hijyen sayesinde çağıranın değişkenleriyle çakışmaz. cargo expand çalıştırıldığında makronun aslında ne ürettiği netleşir:

cargo expand çıktısı
// harita!{ "ali" => 30, "veli" => 25 } şuna açılır:
let yaslar = {
    let mut m = HashMap::new();
    m.insert("ali", 30);
    m.insert("veli", 25);
    m
};
harita!{...}  →  matcher eşleşir  →  $(...)* yayılır  →  insert satırları  →  HashMap

Peki bu makro gerçekten gerekli mi? İşte makrodan kaçınma kuralının özü: bu harita! meşrudur çünkü gerçekten variadic bir kurucu sağlıyor ve dilin sözdizimini anlamlı biçimde genişletiyor — bir fonksiyon bunu yapamaz. Ama tek bir değer alıp dönüştüren işler için makro yazmak yanlıştır; orada fonksiyon daha iyi hata mesajı ve IDE desteği verir.

DİKKAT

Makrodan ne zaman kaç: (1) Bir fonksiyon ya da generic aynı işi yapabiliyorsa. (2) Makro çağıranın sözdizimini bozacak kadar "sihirli" hâle geliyorsa. (3) Hata mesajları okunamaz hâle geldiyse. Makro güçlüdür ama her güçlü araç gibi az ve yerinde kullanılır.

Özetle: macro_rules! ile değişken argüman, kod üretimi ve sözdizimi genişletmeyi C önişlemcisinin tüm tuzaklarından arınmış biçimde yaparsın; prosedürel makrolar ve derive ise boilerplate'i programatik olarak yok eder. Aklında tek bir pusula tut: makro son çaredir, ama doğru zamanda eşi yoktur.

Bu bölümde

  • harita! makrosu tekrar + çift desen + hijyeni bir arada gösterir
  • $(,)? ile sondaki opsiyonel virgül zarif biçimde ele alınır
  • cargo expand üretilen düz insert kodunu doğrular
  • Makro son çaredir: fonksiyon/generic yetmediğinde, yerinde kullanılır