Alex

Rust 里的 vec!:为什么必须用宏(以及 C++ 的对比)

  • Rust
  • Macro

在 Rust 里,vec![1, 2, 3] 的感叹号 ! 不是装饰,它明确表示:这是宏调用(macro invocation),不是函数调用

很多初学者会追问:既然只是创建一个 Vec,为什么 Rust 不像 C++ 那样直接支持 {1, 2, 3}?为什么这里“非得”用宏?

这篇文章用最短路径回答三个问题:

  • vec! 为什么适合用宏实现?
  • 它到底展开成了什么?
  • C++ 的 std::vector v = {1,2,3}; 为什么不需要宏?

0) 先给结论

vec! 之所以是宏,核心原因是:它需要在编译期基于“代码形状”生成更合适的初始化代码,包括但不限于:

  1. 接收不定长输入(vec![a] / vec![a, b, c]
  2. 提供接近“字面量”的简洁语法(少写样板 push
  3. 生成更高效、更贴合所有权与容量的初始化方式(尽量减少中间步骤与扩容)

函数处理“值”,宏处理“代码结构”。vec! 属于典型的“代码结构层面的语法糖”。


1) 变长参数:函数签名做不到这么自然

Rust 的函数参数个数在类型系统层面是固定的。你当然可以用切片参数让调用端把东西先装起来:

fn make_vec(xs: &[i32]) -> Vec<i32> {
    xs.to_vec()
}

但这会让调用端失去 vec![1, 2, 3] 这种“把元素直接写在调用点”的体验。

声明式宏(macro_rules!)可以直接匹配“逗号分隔的重复项”:

  • $( $x:expr ),*:0 个或多个表达式
  • $(,)?:允许末尾多一个逗号(更符合 Rust 常见写法)

这类模式匹配是宏擅长、而函数无法直接表达的。


2) 初始化语法糖:把 push 样板藏起来

如果没有 vec!,你经常会写出这种样板代码:

let mut v = Vec::new();
v.push(1);
v.push(2);
v.push(3);

vec! 的价值之一,就是让这段重复代码在编译期自动生成。你写起来更像在写“数组字面量”,但运行起来仍是显式的 Vec 初始化逻辑。


3) 性能与所有权:宏能生成更贴合目标的代码

很多语言会把 {1, 2, 3}[1, 2, 3] 先当作一个临时数组,再把它转换成 Vec。在某些情况下,这意味着“先构造中间容器,再拷贝/移动到最终容器”。

而宏展开可以直接生成更接近你“真实意图”的代码,比如(示意):

{
    let mut v = Vec::with_capacity(3);
    v.push(1);
    v.push(2);
    v.push(3);
    v
}

关键点不在于“Rust 一定比别的语言快”,而在于:宏可以把初始化策略显式地体现在展开后的代码结构里,避免你为了获得简洁调用而引入不必要的中间步骤。


4) 为什么 C++ 不需要宏:initializer_list 是“语言级特权”

在 C++11 之后,你可以这样写:

std::vector<int> v = {1, 2, 3};

这并不是 std::vector “自己发明了语法”,而是 C++ 标准给了 {...} 一整套语言级规则,并配套了 std::initializer_list<T> 这种机制:

  1. 编译器识别 {1,2,3} 这种列表初始化语法
  2. 把它包装成 initializer_list
  3. 匹配到 vector 对应的构造函数

Rust 的选择不同:它尽量保持核心语法更精简,不为 Vec 或某类容器硬编码一套专用初始化语法;相反,它提供通用的宏系统,让 vec!format! 以及社区里的 hashmap! 这类需求用同一条路解决。


5) Rust 宏并不是 C 宏:它做的是 token 匹配,不是文本替换

vec! 属于声明式宏(macro_rules!),其工作方式更接近“对 token 流做模式匹配并生成新 token 流”,而不是 C 宏那种字符串级别的粗暴替换。

另外 Rust 宏有一个非常关键的安全特性:卫生性(hygiene)。宏内部引入的临时变量名一般不会意外污染外部作用域,也不容易被外部变量“撞名干扰”。


6) 你也能写一个:简化版 my_vec!

下面是一个简化版 my_vec!,支持 my_vec![a, b, c] 这种形式:

macro_rules! my_vec {
    ( $( $x:expr ),* $(,)? ) => {{
        let mut v = Vec::new();
        $(
            v.push($x);
        )*
        v
    }};
}
 
fn main() {
    let v = my_vec![10, 20, 30];
    println!("{:?}", v);
}

如果你见过 vec![1; 10] 这种“重复元素”写法,那是因为 vec! 宏内部定义了多条匹配规则(类似 match 的不同分支),在不同输入形状下生成不同代码。


7) 过程宏(提一嘴)

你常见的 #[derive(Serialize)] 这类属于过程宏:本质上是“编译期运行的一段 Rust 代码”来处理输入的语法结构并生成新代码。它能力更强,但像 vec! 这种轻量的语法糖场景,用 macro_rules! 更直接、更清晰。


8) 小结

vec! 之所以是宏,不是因为 Rust “缺少函数能力”,而是因为:

  1. 它要吃进不定长的代码片段(逗号分隔的表达式列表)
  2. 它要把重复样板压缩成一个看起来像字面量的调用点
  3. 它要在编译期生成更贴合初始化策略的代码结构

当你把宏理解成“编译期的代码生成器”,vec![] 这件事就会变得非常自然。