Rustマクロ入門:マクロの宣言とメタプログラミング
マクロはRustの「コード生成マシン」です。マクロを一度記述するだけで、同じコードが無数に自動的に生成されるため、プログラマーはコピー&ペーストという面倒な作業から解放されます。
関数が「繰り返し呼び出しのためのロジックをカプセル化したもの」であるならば、マクロは「繰り返し展開のためのコード生成ルールをカプセル化したもの」と言えます。関数は実行時の値に対して動作するのに対し、マクロはコンパイル時にコードそのものに対して動作します。つまり、関数は工場の組立ライン(原材料を入力し、製品を出力する)であり、マクロは工場の設計図(設計図を入力し、組立ライン全体を出力する)のようなものです。
1. 学習内容
macro_rules!宣言マクロの基本構文とマッチング規則- リピートパターン
$()*および$()+の使い方 - なぜ
vec!とprintln!は関数ではなくマクロなのか――関数とマクロの根本的な違い 一般的な組み込みマクロ:vec!、println!、format!、todo!、unimplemented! - マクロの衛生管理とプロセスマクロの概念入門
2. 紙幣印刷機の物語
(1) 苦悩:重複コードという泥沼
トムは、ある物流会社でRustエンジニアとして働いています。彼には、一見単純そうなタスクが割り当てられました。それは、5つの輸送手段(トラック、船、飛行機、列車、ドローン)それぞれについて、送料と配送予定時間を計算する関数を記述することでした。
- 各関数のロジックはほぼ同じです。基本運賃を読み取り、距離係数を計算し、燃料サーチャージを加算します。
- 唯一の違いは、配送方法ごとに料金体系と配達時間が異なるという点です。
- トムはそれを5回コピー&ペーストして、数字を少し調整した――どうやら10分で終わらせたようだ。
それで、一体何が起きたの?
- 3日目に、燃料費の計算ルールが変更された。トムは5つの関数を修正する必要があったが、1つ見落としてしまった。
- 第5週に「コールドチェーン輸送」という選択肢が追加され、トムはそれを6回目となるコピー&ペーストを行った。
- 2か月目には、コードは300行にまで膨れ上がり、そのうち200行は重複していた。
「ルールを一度記述するだけで、類似する関数をすべて自動的に生成できる方法があれば素晴らしいのですが……」
(2) Rustのマクロによるアプローチ
// Define using a macro"Shipping Cost Calculator"Template
macro_rules! create_shipping_calculator {
// Matching Patterns: Name of Mode of Transportation + Rate per kilometer + Speed
($name:ident, $rate_per_km:expr, $speed:expr) => {
fn $name(distance: f64) -> (f64, f64) {
let base_cost = distance * $rate_per_km;
let fuel_surcharge = base_cost * 0.1;
let total = base_cost + fuel_surcharge;
let time_hours = distance / $speed;
(total, time_hours)
}
};
}
// Single-line macro call = Generate a complete function
create_shipping_calculator!(truck, 1.5, 60.0);
create_shipping_calculator!(ship, 0.8, 30.0);
create_shipping_calculator!(plane, 5.0, 800.0);
create_shipping_calculator!(train, 1.2, 80.0);
create_shipping_calculator!(drone, 2.0, 50.0);
fn main() {
let distance = 500.0;
// Every function exists by default.,Just like handwriting
println!("Truck : cost=${:.2}, time={:.1}h", truck(distance).0, truck(distance).1);
println!("Ship : cost=${:.2}, time={:.1}h", ship(distance).0, ship(distance).1);
println!("Plane : cost=${:.2}, time={:.1}h", plane(distance).0, plane(distance).1);
println!("Train : cost=${:.2}, time={:.1}h", train(distance).0, train(distance).1);
println!("Drone : cost=${:.2}, time={:.1}h", drone(distance).0, drone(distance).1);
}
出力:
Truck : cost=$825.00, time=8.3h
Ship : cost=$440.00, time=16.7h
Plane : cost=$2750.00, time=0.6h
Train : cost=$660.00, time=6.2h
Drone : cost=$1100.00, time=10.0h
マクロは「コードプリンター」のようなものです。テンプレート(印刷版)を設計しておけば、マクロを呼び出すたびに、完全なコードが「印刷」されます。変更を加える際は、テンプレートの1か所だけを修正すれば、生成されるすべてのコードが同時に更新されます。各関数を1つずつ手作業で修正する必要はもうありません。
3. 基本概念
(1) マクロ展開プロセス
graph TB
A[macro_rules! Declaration Macro] --> B[Matching Arm 1: Pattern => Template]
A --> C[Matching Arm 2: Pattern => Template]
A --> D[Matching Arm N: Pattern => Template]
B --> E[Compiler Matching input token]
C --> E
D --> E
E --> F{Match Successful?}
F -->|Yes| G[Replace the template and expand the code]
F -->|No| H[Compilation Error: Mismatch]
G --> I[Generate AST Node]
I --> J[Continue compiling]
style A fill:#4a90d9,color:#fff
style E fill:#e6a23c,color:#fff
style F fill:#f56c6c,color:#fff
style G fill:#67c23a,color:#fff
(2) 関数とマクロの比較
| 次元 | 関数 (fn) | マクロ (macro_rules!) |
|---|---|---|
| 実行タイミング | 実行時呼び出し | コンパイル時の展開 |
| パラメータ数 | 固定 | 可変(繰り返しパターンを通じて) |
| パラメータ型 | 固定型 | 任意のトークンストリーム |
| コード生成 | コードを生成しない | 新しいコードASTを生成する |
| 戻り値 | 戻り値の型がある | 任意のコードスニペットを生成できる |
| 用法 | foo(args) |
感嘆符付きの foo!(args) |
| 再帰の制限 | スタックの深さ | マクロの再帰深さ(デフォルト:128レベル) |
| 衛生 | 自然隔離の範囲 | 宣言マクロによる部分的な衛生 |
(3) 一般的な組み込みマクロ
| マクロ | 関数 | 例 |
|---|---|---|
println! |
stdoutに出力し、改行を追加する | println!("Hello, {}!", name) |
print! |
改行なしで標準出力に出力する | print!("count: {}", i) |
format! |
フォーマットされた文字列はStringを返す | let s = format!("{}:{}", h, m) |
vec! |
ベクトルを素早く作成 | let v = vec![1, 2, 3] |
todo! |
プレースホルダー。実行時にパニックを引き起こす | fn foo() { todo!() } |
unimplemented! |
未実装のフラグ、実行時パニック | fn bar() { unimplemented!() } |
eprintln! |
stderr に出力 | eprintln!("Error: {}", msg) |
write! |
fmt::Write を実装する型 | write!(&mut s, "{}", val) |
concat! |
コンパイル時の文字列連結 | concat!("a", "b", "c") → "abc" |
stringify! |
式を文字列リテラルに変換する | stringify!(1+2) → "1 + 2" |
(4) 宣言型マクロと手続き型マクロの比較
| 次元 | 宣言マクロ macro_rules! |
プロシージャマクロ (Proc Macro) |
|---|---|---|
| 定義方法 | macro_rules! name { ... } |
スタンドアロン・クレート + #[proc_macro_*] |
| 展開のタイミング | コンパイル時のパターンマッチングと置換 | コンパイル時の手続き型コード生成 |
| 機能 | パターンマッチング + トークン置換 | あらゆるASTの読み取り・生成が可能 |
| 複雑度 | 低(宣言型) | 高(ASTを処理するためにRustコードの記述が必要) |
| サブカテゴリ | なし | 派生マクロ #[derive] / 属性マクロ #[attr] / 機能マクロ name!() |
| 代表的な用途 | vec![], println! |
serde::Serialize, tokio::main |
| デバッグの難易度 | 中 | 高 |
4. マクロの例
(1) ▶ サンプル:最初のマクロの宣言――ゲッター関数の自動生成(難易度 ⭐)
// ============================================
// Usage macro_rules! Define a Macro
// Scene: Automatically Generate getter Methods for Structure Fields
// ============================================
// Macro Definitions: Generate getter Function based on field name and type
// A macro name followed by ! Indicates that this is a macro
macro_rules! create_getter {
// Matching Patterns: $name is an identifier (ident), $ty is a type (ty)
// => The code block below is a template expansion.
($name:ident, $ty:ty) => {
pub fn $name(&self) -> $ty {
self.$name.clone()
}
};
}
// Structures That Use Macros
#[derive(Debug)]
struct Student {
name: String,
age: u32,
grade: String,
}
impl Student {
// Written by hand getter —— You have to write one for each field.
// pub fn name(&self) -> String { self.name.clone() }
// pub fn age(&self) -> u32 { self.age }
// pub fn grade(&self) -> String { self.grade.clone() }
// Automatically Generated Using a Macro —— One per line getter
create_getter!(name, String);
create_getter!(age, u32);
create_getter!(grade, String);
}
fn main() {
let s = Student {
name: "Alice".to_string(),
age: 20,
grade: "A".to_string(),
};
// Generated by calling a macro getter Methods
println!("Name : {}", s.name());
println!("Age : {}", s.age());
println!("Grade : {}", s.grade());
}
出力:
Name : Alice
Age : 20
Grade : A
マクロ
create_getter!はフィールド名と型を受け取り、完全なpub fn定義に展開されます。展開されると、create_getter!を 3 回呼び出すことは、3 つのゲッター関数を手作業で記述することと同等になります。これが「コードプリンター」のプロトタイプです。テンプレートがコード生成ルールを定義し、呼び出しごとにコードの一部が出力されます。
(2) ▶ サンプル:繰り返しパターン $()* および $()+ — 可変パラメータマクロ (難易度 ⭐⭐)
// ============================================
// Repetition Patterns in Presentation Macros:$()* Zero or more times、$()+ Once or multiple times
// Scene: A Mini Test Framework, Supports multiple assertions
// ============================================
// Macro: Run Multiple Test Cases, each case includes a name + Expression + Expected Value
// $()* Indicates that the pattern inside the parentheses can be repeated zero or more times
macro_rules! run_tests {
// Each test consists of (Name, Expression, Expected Value) Composition of Trios
// $test_name It is an identifier,$expr It is an expression,$expected It is an expression
($( $test_name:ident, $expr:expr, $expected:expr );* $(;)?) => {
$(
println!("[Test] {} ...", stringify!($test_name));
let result = $expr;
let expected: i32 = $expected;
if result == expected {
println!(" ✅ PASS: {} == {}", result, expected);
} else {
println!(" ❌ FAIL: {} != {} (expected {})", stringify!($expr), result, expected);
}
)*
};
}
// Macro: Calculate the sum of any number of values
// $()+ Indicates that the pattern inside the parentheses must be repeated at least once
macro_rules! sum_of {
// Usage $()+ At least one argument is required.
($($x:expr),+ $(,)?) => {
// 0 + $x Cumulative total: 0 + a + b + c ...
{
let mut sum = 0i64;
$(
sum += $x as i64;
)+
sum
}
};
}
fn main() {
println!("=== Mini Test Framework ===");
// Call run_tests! macro - Pass multiple test cases
run_tests! {
add_one, 1 + 1, 2;
multiply, 3 * 4, 12;
subtract, 10 - 3, 7;
power, 2 * 2 * 2, 8
}
println!("\n=== Sum Calculator ===");
// Call sum_of! macro - Pass any number of arguments
let s1 = sum_of!(1, 2, 3, 4, 5);
println!("sum_of!(1..5) = {}", s1);
let s2 = sum_of!(10, 20, 30);
println!("sum_of!(10,20,30) = {}", s2);
// A single parameter is also acceptable.
let s3 = sum_of!(42);
println!("sum_of!(42) = {}", s3);
println!("\n=== Done ===");
}
出力:
=== Mini Test Framework ===
[Test] add_one ...
✅ PASS: 2 == 2
[Test] multiply ...
✅ PASS: 12 == 12
[Test] subtract ...
✅ PASS: 7 == 7
[Test] power ...
✅ PASS: 8 == 8
=== Sum Calculator ===
sum_of!(1..5) = 15
sum_of!(10,20,30) = 60
sum_of!(42) = 42
=== Done ===
$()*および$()+は、マクロで「可変引数」を実装する上で重要な役割を果たします。$()*は「0回以上の繰り返し」(例:Vecは空でもよい)を示し、$()+は「1回以上の繰り返し」(少なくとも1つの引数)を示します。繰り返しパターン内では、区切り文字 (,、;など) を使用して、パラメータのグループ化方法を制御することもできます。
(3) ▶ サンプル:vec! マクロの簡略化された実装 — 組み込みマクロの仕組みを理解する (難易度 ⭐⭐)
// ============================================
// Implement a simplified version of the vec! macro
// Understanding vec! The Underlying Principles of Macro Expansion
// ============================================
// Simplified vec! macro - Does not support vec![x; n] syntax
macro_rules! my_vec {
// Empty vector
() => {
Vec::new()
};
// A single element
($elem:expr) => {
{
let mut v = Vec::new();
v.push($elem);
v
}
};
// Multiple elements,Separated by commas
($($x:expr),+ $(,)?) => {
{
let mut v = Vec::new();
$(
v.push($x);
)+
v
}
};
}
fn main() {
// Use the built-in vec! macro
let builtin_empty: Vec<i32> = vec![];
let builtin_one = vec![42];
let builtin_multi = vec![1, 2, 3, 4, 5];
println!("=== Built-in vec! ===");
println!("empty : {:?}", builtin_empty);
println!("one : {:?}", builtin_one);
println!("multi : {:?}", builtin_multi);
// Use Custom my_vec! macro
let my_empty: Vec<i32> = my_vec![];
let my_one = my_vec![42];
let my_multi = my_vec![10, 20, 30, 40, 50];
println!("\n=== Custom my_vec! ===");
println!("my_empty : {:?}", my_empty);
println!("my_one : {:?}", my_one);
println!("my_multi : {:?}", my_multi);
// Verify Functional Consistency
assert_eq!(builtin_multi.len(), 5);
assert_eq!(my_multi.len(), 5);
assert_eq!(builtin_multi, vec![1, 2, 3, 4, 5]);
assert_eq!(my_multi, vec![10, 20, 30, 40, 50]);
println!("\n=== All assertions passed! ===");
}
出力:
=== Built-in vec! ===
empty : []
one : [42]
multi : [1, 2, 3, 4, 5]
=== Custom my_vec! ===
my_empty : []
my_one : [42]
my_multi : [10, 20, 30, 40, 50]
=== All assertions passed! ===
表示されている
vec!マクロは、実はmacro_rules!という宣言マクロなのです!これは$($x:expr),+を使用してカンマ区切りの式リストに一致させ、その後、v.push($x)コードの繰り返しブロックに展開されます。これは「関数ではできないこと」です—vec![1, 2, 3]もし関数として記述した場合、コンパイル時に要素数を特定し、対応するpushコードを生成することは不可能です。
(4) ▶ サンプル:組み込みマクロ todo! および unimplemented! の使用(難易度:⭐)
// ============================================
// Demonstrate Built-in Macros: todo! / unimplemented! / format! / eprintln!
// Scene: An inventory management system currently under development
// ============================================
// Simulated Inventory Items
#[derive(Debug)]
struct InventoryItem {
id: u32,
name: String,
quantity: u32,
}
// Inventory Manager
struct InventoryManager {
items: Vec<InventoryItem>,
}
impl InventoryManager {
fn new() -> InventoryManager {
InventoryManager {
items: Vec::new(),
}
}
// Implemented: Add Item
fn add_item(&mut self, id: u32, name: &str, quantity: u32) {
self.items.push(InventoryItem {
id,
name: name.to_string(),
quantity,
});
// Usage format! Macro-Formatted Log Messages
let log_msg = format!("[INFO] Added item: {} (id={}, qty={})", name, id, quantity);
println!("{}", log_msg);
}
// Implemented: Search for Products
fn find_item(&self, id: u32) -> Option<&InventoryItem> {
self.items.iter().find(|item| item.id == id)
}
// Not implemented: Update Inventory
fn update_quantity(&mut self, _id: u32, _new_qty: u32) {
// TODO: Implement the inventory update logic
// todo!() will panic with a "Not implemented" message
todo!("update_quantity: id={} quantity={}", _id, _new_qty);
}
// Not implemented: Generate an Inventory Report
fn generate_report(&self) -> String {
// unimplemented!() Indicates that this feature has not been implemented yet
unimplemented!("generate_report() is not yet implemented");
}
// Implemented: Print All Items
fn list_items(&self) {
if self.items.is_empty() {
println!(" (no items in inventory)");
return;
}
for item in &self.items {
println!(" #{} {} (qty: {})", item.id, item.name, item.quantity);
}
}
}
fn main() {
let mut manager = InventoryManager::new();
println!("=== Inventory Manager ===");
// Add Item
manager.add_item(101, "Laptop", 10);
manager.add_item(102, "Mouse", 50);
manager.add_item(103, "Keyboard", 30);
// List Products
println!("\nCurrent inventory:");
manager.list_items();
// Search for Products
if let Some(item) = manager.find_item(102) {
println!("\nFound: {:?}", item);
}
// Uncommenting the following code will panic (But it won't result in a compilation error):
// manager.update_quantity(101, 8); // panics: "not yet implemented"
// let report = manager.generate_report(); // panics: "not yet implemented"
println!("\n=== Demo completed ===");
println!("Note: Try uncommenting update_quantity() or generate_report() to see todo!/unimplemented! in action.");
}
出力:
=== Inventory Manager ===
[INFO] Added item: Laptop (id=101, qty=10)
[INFO] Added item: Mouse (id=102, qty=50)
[INFO] Added item: Keyboard (id=103, qty=30)
Current inventory:
#101 Laptop (qty: 10)
#102 Mouse (qty: 50)
#103 Keyboard (qty: 30)
Found: InventoryItem { id: 102, name: "Mouse", quantity: 50 }
=== Demo completed ===
Note: Try uncommenting update_quantity() or generate_report() to see todo!/unimplemented! in action.
todo!()とunimplemented!()は、Rust 開発者にとって「強力なプレースホルダーツール」です。todo!()には説明文 (todo!("msg: {}", val)) を追加できるため、開発中の未完成な機能をマークするのに最適です。unimplemented!()は、インターフェース定義内でまだ実装されていないメソッドをマークするのに適しています。どちらも実行時にパニックを引き起こしますが、コンパイルエラーにはなりません。これにより、まずコードを書き、段階的に実装していくことが可能になります。
(5) ▶ サンプル:マクロの衛生管理 — 内部のマクロ変数が外部環境を汚染しない(難易度 ⭐⭐⭐)
// ============================================
// Demonstrating Macro Hygiene
// Variables created within a macro do not conflict with those in the outer scope.
// ============================================
// Note: Rust declaration macros have "partial hygiene"
// For internal use by Hong $ Captured variable names will not conflict with external ones
// However, identifiers written directly within the macro are in Rust 2018+ There are special rules in this case
// Macro: Create a local variable tmp and swap values
// Note: tmp written directly within the macro is's hygienic.——It will not affect the outside world. tmp
macro_rules! swap_with_tmp {
($a:expr, $b:expr) => {
{
let tmp = $a;
$a = $b;
$b = tmp;
}
};
}
// Macro: Demonstrate Hygiene - Even if there is a tmp variable outside
macro_rules! demonstrate_hygiene {
($x:expr) => {
{
// Defined internally by the macro tmp It's hygienic.
let tmp = $x * 2;
println!(" Inside macro: tmp = {}", tmp);
tmp
}
};
}
// Counterexample of Unhygienic Conditions (Demonstrated differently)
// Note: Rust declarative macros do not allow creating variables that cause cross-scope conflicts.
// So here we use concat Simulation"Non-health-related"Potential Issues
fn main() {
println!("=== Macro Hygiene Demonstration ===\n");
// Scenario 1: Internal variables in a macro do not affect external variables
println!("1. Macro internal variable vs external variable:");
let mut x = 10;
let mut y = 20;
println!(" Before swap: x={}, y={}", x, y);
// The macro uses the following internally: tmp,But the external variable names tmp Not affected
swap_with_tmp!(x, y);
println!(" After swap: x={}, y={}", x, y);
// External tmp The variable does not exist. —— Inside the macro tmp It's hygienic.
// If you uncomment the following line, you'll get a compilation error.:
// println!("tmp from macro = {}", tmp); // ❌ Compilation Error: tmp not found
// Scenario 2: Internal macro variables do not conflict with external variables of the same name
println!("\n2. Hygiene with same name:");
let tmp = 100; // External tmp
println!(" Outside macro: tmp = {}", tmp);
let result = demonstrate_hygiene!(5);
println!(" Return value: {}", result);
println!(" Outside macro again: tmp = {}", tmp); // It's still 100,Not affected by macros
// Scenario 3: Why Is Hygiene Important?
println!("\n3. Why hygiene matters:");
println!(" Without hygiene, macros could accidentally:");
println!(" - Overwrite variables in the caller's scope");
println!(" - Create hard-to-find bugs");
println!(" - Break encapsulation of the calling code");
println!(" Rust's hygiene prevents these issues at compile time.");
println!("\n=== Demo completed ===");
}
出力:
=== Macro Hygiene Demonstration ===
1. Macro internal variable vs external variable:
Before swap: x=10, y=20
After swap: x=20, y=10
2. Hygiene with same name:
Outside macro: tmp = 100
Inside macro: tmp = 10
Return value: 10
Outside macro again: tmp = 100
3. Why hygiene matters:
Without hygiene, macros could accidentally:
- Overwrite variables in the caller's scope
- Create hard-to-find bugs
- Break encapsulation of the calling code
Rust's hygiene prevents these issues at compile time.
マクロの衛生性は、Rustのマクロにおける重要な特徴の一つです。マクロ内で作成された変数名は、呼び出し元のスコープに「漏れ出」ることはありません。上記の例では、マクロ内に
tmpがあり、マクロの外側にtmpがありますが、これらは互いに干渉しません。これにより、Cのマクロでよく見られる「名前衝突」の問題を回避できます。Cでは、マクロがtmpという名前の変数を使用していて、呼び出し元にたまたまtmpという名前の変数があると、デバッグが困難なバグにつながる可能性があります。
(6) ▶ サンプル:総合的な例――マクロを使ったミニテストフレームワークの構築(難易度 ⭐⭐⭐)
// ============================================
// Comprehensive Example: Building a Mini Unit Testing Framework Using Macros
// Use in combination:Matching Patterns、Repetition Pattern、Built-in Macros
// ============================================
// Test Results Summary
struct TestStats {
total: u32,
passed: u32,
failed: u32,
}
impl TestStats {
fn new() -> TestStats {
TestStats { total: 0, passed: 0, failed: 0 }
}
fn print_summary(&self) {
println!("\n==============================");
println!("Test Summary:");
println!(" Total : {}", self.total);
println!(" Passed: {}", self.passed);
println!(" Failed: {}", self.failed);
if self.failed == 0 {
println!(" ✅ All tests passed!");
} else {
println!(" ❌ {} test(s) failed", self.failed);
}
println!("==============================");
}
}
// Macro: Define a set of test cases
// Each test is identified by its name、Assertion Expressions、Expected Results Breakdown
macro_rules! test_suite {
// Matches zero or more tests
($( $name:ident: $left:expr, $op:tt, $right:expr );* $(;)?) => {{
let mut stats = TestStats::new();
$(
stats.total += 1;
print!("[Test] {}: {} {} {} ... ", stringify!($name),
stringify!($left), stringify!($op), stringify!($right));
let passed = match $op {
== => { $left == $right }
!= => { $left != $right }
< => { $left < $right }
<= => { $left <= $right }
> => { $left > $right }
>= => { $left >= $right }
_ => { panic!("Unsupported operator: {}", stringify!($op)); }
};
if passed {
stats.passed += 1;
println!("✅ PASS");
} else {
stats.failed += 1;
println!("❌ FAIL (got {:?}, expected {:?})", $left, $right);
}
)*
stats
}};
}
fn main() {
println!("=== Mini Test Framework ===");
println!("Using macro-generated test suite\n");
// Test Suites Defined Using Macros
let stats = test_suite! {
test_add: 2 + 2, ==, 4;
test_sub: 10 - 3, ==, 7;
test_mul: 3 * 4, ==, 12;
test_div: 10 / 2, ==, 5;
test_gt: 100, >, 50;
test_lt: 3, <, 10;
test_eq: "hello", ==, "hello";
test_neq: 42, !=, 0
};
// Print Statistics
stats.print_summary();
// Verify that all tests have passed
assert_eq!(stats.total, 8);
assert_eq!(stats.passed, 8);
assert_eq!(stats.failed, 0);
println!("\n=== Demo completed ===");
}
出力:
=== Mini Test Framework ===
Using macro-generated test suite
[Test] test_add: 2 + 2 == 4 ... ✅ PASS
[Test] test_sub: 10 - 3 == 7 ... ✅ PASS
[Test] test_mul: 3 * 4 == 12 ... ✅ PASS
[Test] test_div: 10 / 2 == 5 ... ✅ PASS
[Test] test_gt: 100 > 50 ... ✅ PASS
[Test] test_lt: 3 < 10 ... ✅ PASS
[Test] test_eq: "hello" == "hello" ... ✅ PASS
[Test] test_neq: 42 != 0 ... ✅ PASS
==============================
Test Summary:
Total : 8
Passed: 8
Failed: 0
✅ All tests passed!
==============================
=== Demo completed ===
この包括的な例は、マクロの真の威力を示しています。
test_suite!マクロは、一連のテスト定義(名前、式、比較演算子、期待値)を受け取り、それらを完全なテスト実行コードに自動的に展開します。このマクロを使用して記述された8つのテストケースを呼び出すのに必要なコードはわずか8行ですが、同等のコードを手作業で記述すると、少なくとも60行は必要になります。さらに重要なのは、「テストのタイムアウト」や「テストのグループ化」といった機能を追加する必要がある場合でも、マクロテンプレートの1箇所を変更するだけで、すべてのテストが自動的に更新されるという点です。
❓ よくある質問
vec![1, 2, 3]など)の処理、コンパイル時のコード生成、コードスニペットを引数として受け取るなどです。しかし、マクロはデバッグが難しく、コードの可読性が低下し、わかりにくいコンパイル時のエラーメッセージが発生する可能性があります。基本的には関数の使用を優先し、関数ではできない処理が必要な場合にのみマクロを使用してください。macro_rules! における $()* と $()+ の違いは何ですか?$()* は 0 回以上の繰り返し(空文字列を含む)に一致するのに対し、$()+ は 1 回以上の繰り返し(少なくとも 1 回)に一致します。 たとえば、vecでは $()* を使用しますが、vec![1, 2] では $()+ または $()* のいずれかを使用できます。マクロに少なくとも1つの引数を受け入れさせたい場合は $()+ を、引数を0個でも許可する場合は $()* を使用してください。tmpを使用しているのに、呼び出し元にもたまたまtmpという名前の変数がある場合などです。Rustの宣言マクロは「部分的に衛生的な」仕様となっており、マクロ内で$によってキャプチャされた変数名は外部に漏れないため、このようなバグを防ぐことができます。todo!() と unimplemented!() の違いは何ですか?todo!() は「この機能は計画されているが、まだ実装されていない」ことを示し、進捗状況を指定するためのパラメータを含めることができます(例:todo!("implement pagination"))。unimplemented!()は「このインターフェース/メソッドは現在、実装予定がない」ことを示します。todo!()は開発中にToDoリストの項目をマークするために、unimplemented!()はトレイトのデフォルト実装で、それぞれより一般的に使用されます。#[derive(...)] 派生マクロ(#[derive(Debug)] など)、属性マクロ(#[test] など)、関数マクロ(#[async] など。これは async fn の基盤となっている)の3種類があります。手続き型マクロは別の proc-macro クレートで定義する必要がありますが、macro_rules! 宣言型マクロはどこでも定義できます。このレッスンでは概念のみを扱います。手続き型マクロの詳細な使用法は上級者向けの内容です。📖 まとめ
macro_rules!は Rust の宣言型マクロシステムであり、パターンマッチングを用いて、入力トークンのストリームを、コンパイル時に展開されるコードテンプレートに置き換えます。- 繰り返しパターン
$()*(0回以上)および$()+(1回以上)は、可変引数マクロを実装するための中核となる仕組みです。 - 一般的な組み込みマクロ
vec!、println!、format!、todo!、およびunimplemented!は、本質的にはマクロ宣言です。一方、vec!はVec::new()に展開され、その後にpushが繰り返し出現します。 - 関数とマクロの比較:関数は実行時の値に対して動作するのに対し、マクロはコンパイル時のコードに対して動作します。関数の引数は固定されていますが、マクロの引数は可変です。関数は型チェックを行いますが、マクロは展開後にのみ型チェックが行われます。
- マクロの衛生管理により、内部のマクロ変数が外側のスコープに漏れ出すことを防ぎ、C言語のマクロとの名前衝突を回避します。
- プロセスマクロは、派生マクロ、属性マクロ、関数マクロの3種類を含む、より高度なマクロシステムです。これらは、別の
proc-macroクレート内で定義する必要があります。
📝 練習問題
- 難易度 ⭐: 2つの式を引数として受け取り、タプル
(expr1, expr2)を返すmake_pair!マクロを作成してください。例えば、make_pair!(42, "hello")は(42, "hello")に展開されます。main内でこのマクロを呼び出し、結果を出力してください。 - 難易度 ⭐⭐: 2つの式を引数として受け取る
assert_equal!マクロを作成してください。それらが等しい場合は✅ PASSを出力し、そうでない場合は❌ FAIL: left != rightを出力してください。元の式を出力するにはstringify!マクロを使用してください。少なくとも3つのテストケース(式が等しい場合と等しくない場合を含む)を作成し、実行してください。 - 難易度 ⭐⭐⭐: 列挙型の名前と一連のバリアント名を入力として受け取り、
create_enum_with_display!マクロがDisplayトレイトの列挙型定義と実装を自動的に生成するようにします(各バリアントは対応する文字列として表示されます)。たとえば、create_enum_with_display!(Color, Red, Green, Blue)はColor列挙型に展開され、そこでRedは"Red"として表示されます。ヒント:$()*反復パターンとstringify!を使用してください。



