Rust 基本语法特性

本文梳理了 Rust 中一些常用但容易忽略的语法特性,涵盖格式化宏、编译期函数执行、闭包捕获语义、复合数据类型、泛型分发机制、引用借用规则以及常用字符串方法。

一、println! 格式化宏

println! 宏支持丰富的格式化占位符,下表列出了常用的格式化形式:

占位符 含义 示例 输出说明
{} / nothing Display trait println!("{}", 2) 常规输出
{?} Debug trait println!("{:?}", 2) 调试格式
{o} 八进制 println!("{:o}", 2) → 2
{x} 十六进制(小写) println!("{:x}", 255) → ff
{X} 十六进制(大写) println!("{:X}", 255) → FF
{p} 指针地址 println!("{:p}", &val) 内存地址
{b} 二进制 println!("{:b}", 2) → 10
{e} 指数(小写) println!("{:e}", 10000.0) → 1e4
{E} 指数(大写) println!("{:E}", 10000.0) → 1E4

二、const_fn:编译期函数执行

CTFE(Compile-Time Function Execution,编译时函数执行)机制允许在编译阶段完成函数计算,将运行时开销提前消除。

  • Rust 2018 版本起已无需添加 #![feature(const_fn)]
  • 使用 const fn 定义的函数必须能在编译期确定返回值,不能有歧义
  • 与普通 fn 的区别:const fn 强制编译器在编译期执行;而 const 一般用于定义全局常量
1
2
3
4
5
6
7
8
#![feature(const_fn)]
const fn init_len() -> i32 {
return 5;
}

fn main() {
let arr = [0; init_len()];
}

三、Never 类型与闭包

3.1 Never 类型

never 类型表示永远不会返回值的计算类型(例如线程退出、panic! 后的分支)。当前属于实验特性,需要在 Nightly 版本下通过 #![feature(never_type)] 显式使用。never 类型可以强制转换为其他任何类型。

3.2 闭包(Closure)

Rust 的闭包是一种匿名函数,具有三个核心能力:

  • 像函数一样被调用
  • 捕获上下文环境中的自由变量
  • 自动推断输入和返回类型

闭包本质上由匿名结构体 + trait 组合实现,同样可以作为返回值使用。

⚠️ 注意:闭包默认按引用捕获变量。如果将闭包作为返回值,函数内局部变量的引用会跟着返回——函数调用结束后局部变量销毁,引用变成悬垂指针(dangling pointer)。解决方式是使用 move 关键字将所捕获变量的所有权转移给闭包,使闭包可以安全返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
fn main(){
let out = 42;
let closure_annotated = |i: i32, j: i32| -> i32 { i + j + out };
let closure_inferred = |i, j| i + j + out;

// 闭包作为参数
let a = 2;
let b = 3;
let res = math(|| a + b);

// 闭包作为返回值
let result = two_times_impl();
}

// 使用 impl Fn(i32) -> i32 作为函数的返回值
fn two_times_impl() -> impl Fn(i32) -> i32 {
let i = 2;
move |j| j * i
}

// 参数是一个泛型 F,受 Fn() -> i32 trait 约束
fn math<F: Fn() -> i32>(op: F) -> i32 {
op()
}

3.3 闭包的 Trait 自动实现规则

编译器会根据闭包对环境变量的捕获方式,自动为其实现对应的 trait(FnFnMutFnOnce):

捕获情况 是否需要修改 是否用 move 自动实现的 trait
未捕获任何环境变量 Fn
捕获了复制语义类型 ❌ 不需要 有/无 move Fn
捕获了复制语义类型 ✅ 需要 FnMut
捕获了移动语义类型 ❌ 不需要 ❌ 无 move FnOnce
捕获了移动语义类型 ❌ 不需要 ✅ 有 move FnOnce
捕获了移动语义类型 ✅ 需要 FnMut

对于使用了 move 关键字的 FnMut 闭包:如果捕获的是复制语义类型的变量,则该闭包会自动实现 Copy / Clone;否则不会实现。

四、复合数据类型

Rust 提供 4 种复合数据类型:

类型总览

类型 说明 子分类
元组(Tuple) 固定长度,元素可不同类型
结构体(Struct) 具名字段集合 具名结构体、元组结构体、单元结构体
枚举体(Enum) 同一类型多种变体
联合体(Union) 共享内存,同一段数据多种解释

4.1 元组

元组中元素可以是不同类型,长度固定,通过索引访问:

1
2
3
4
5
let tuple: (&'static str, i32, char) = ("hello", 5, 'c');

assert_eq!(tuple.0, "hello");
assert_eq!(tuple.1, 5);
assert_eq!(tuple.2, 'c');

4.2 结构体

结构体遵循驼峰命名法,字段默认不可变,字段类型可以是任意类型(包括自身):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#[derive(Debug, PartialEq)]
// derive 让结构体自动实现 Debug 和 PartialEq trait,
// 分别支持实例打印和相等性比较
struct People {
name: &'static str,
gender: u32,
}

impl People {
fn new(name: &'static str, gender: u32) -> Self {
return People { name, gender };
}

fn set_name(&mut self, name: &'static str) {
self.name = name;
}

fn get_name(&self) -> String {
return self.name.to_string();
}
}

fn main() {
let mut people = People::new("aa", 2);
println!("{}", people.get_name());
people.set_name("bb");
println!("{}", people.get_name());
}

4.3 单元结构体

单元结构体不携带任何数据。在 Release 编译模式下,多个单元结构体实例可能被优化为同一对象;Debug 模式下则不会进行此优化:

1
2
3
4
5
6
7
8
9
10
11
struct Empty;

fn main() {
let x = Empty;
println!("{:p}", &x);
let y = x;
println!("{:p}", &y);
let z = Empty;
println!("{:p}", &z);
assert_eq!((..), std::ops::RangeFull);
}

4.4 集合容器

标准库提供了丰富的集合容器类型:

分类 容器 说明
线性序列 Vec、VecDeque、LinkedList 向量、双端队列、链表
Key-Value 映射 HashMap、BTreeMap 无序哈希表、有序红黑树映射
集合 HashSet、BTreeSet 无序集合、有序集合
优先队列 BinaryHeap 二叉堆

双端队列 VecDeque 同时实现了 push_front(栈行为)和 push_back(队列行为)。实际开发中优先使用 VecVecDeque,它们比链表更快速、内存访问效率更高,且能更好地利用 CPU 缓存。

五、泛型与 Trait

5.1 Option 匹配

1
2
3
4
5
6
7
8
9
10
11
12
13
fn match_option<T: Debug>(op: Option<T>) {
match op {
Some(o) => println!("{:?}", o),
None => println!("没有值"),
}
}

fn main() {
let a = Some(1);
match_option(a);
let b: Option<u8> = None;
match_option(b);
}

5.2 静态分发 vs 动态分发

这是 Rust 泛型的两种核心调度方式:

特性 静态分发(Static Dispatch) 动态分发(Dynamic Dispatch)
实现方式 泛型 <T: Trait> trait 对象 &dyn Trait
调度时机 编译期展开具体类型代码 运行时通过 vtable 查找
运行开销 零开销抽象 少量虚函数调用开销
二进制体积 每种类型生成一份副本 仅生成一份

静态分发示例fly_static::<Pig>(pig)fly_static::<Duck>(duck) 在编译期各自生成特化代码,抽象在编译阶段就被消除了。

动态分发示例fly_dyn(&Duck) 通过 trait 对象在运行时查找方法实现,带来少量开销(通常可忽略不计)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
trait Fly {
fn is_fly(&self) -> bool;
}

struct Duck;
struct Pig;

impl Fly for Duck {
fn is_fly(&self) -> bool {
println!("鸭子会飞");
return true;
}
}

impl Fly for Pig {
fn is_fly(&self) -> bool {
println!("猪不会飞");
return false;
}
}

// 静态分发:编译期单态化
fn fly_static<T: Fly>(f: T) -> String {
if f.is_fly() {
"飞起来了".to_string()
} else {
"飞不动".to_string()
}
}

// 动态分发:运行时查找
fn fly_dyn(f: &Fly) -> String {
if f.is_fly() {
"飞起来了".to_string()
} else {
"飞不动".to_string()
}
}

fn main() {
let res = fly_static::<Pig>(Pig);
println!("{}", res);
let res = fly_static::<Duck>(Duck);
println!("{}", res);
let res = fly_dyn(&Pig);
println!("{}", res);
}

5.3 重要标签 Trait(Marker Traits)

Rust 在 std::marker 模块中定义了 5 个重要的标签 trait:

Trait 作用
Sized 标识编译期可确定大小的类型。编译器依赖它来识别哪些类型可以在栈上分配
Unsize (实验性)标识动态大小类型 DST,如 [T]dyn Trait
Copy 标识可以按位安全复制的类型(所有字段均需实现 Copy)
Send 标识可以跨线程安全传递所有权的类型
Sync 标识可以在线程间安全共享引用的类型(&T 是 Send 的当且仅当 T: Sync)

Sized 是最核心的标签 trait——几乎所有泛型约束都隐含 T: Sized。若要支持动态大小类型,需显式使用 T: ?Sized

#[fundamental] 属性表示该 trait 不受孤儿规则的限制。通过 #[derive(Debug, Copy, Clone)] 可以让结构体自动实现 Copy,但前提是所有成员都是可复制的——否则按位复制可能引发内存安全问题。

六、引用与借用

6.1 基本概念

引用(Reference)是 Rust 提供的一种指针语义。它与裸指针的区别在于:指针保存的是内存地址,而引用是某块内存的别名(Alias),必须满足编译器的安全检查规则。

  • 不可变引用&T —— 只读借用
  • 可变引用&mut T —— 可写借用

在所有权体系中,引用也称为借用(Borrowing)。借用不会转移所有权,但会对原所有者施加限制。

6.2 借用三大规则

为保证内存安全,Rust 的借用检查器强制执行以下三条规则:

规则 内容 防止的问题
规则一 借用的生命周期不得长于出借方(owner)的生命周期 悬垂指针(dangling pointer)
规则二 可变引用不允许有别名(独占性) 数据竞争(data race)
规则三 不可变借用在存在期间不能再出借为可变借用 违反不变量保证

规则二和三的核心原则概括为八个字:共享不可变,可变不共享

从读写锁的角度理解:同一时刻,要么持有一个写锁(可变引用),要么持有多个读锁(不可变引用),两者互斥。

6.3 内部可变性(Interior Mutability)

当需要在拥有不可变引用的前提下修改数据时,Rust 提供了以下几种内部可变性容器:

类型 适用场景 机制
Cell<T> T: Copy 类型 提供 get() / set() 方法
RefCell<T> 非 Copy 类型 提供 borrow() / borrow_mut() 方法,运行时检查借用规则,违规时触发 panic
Cow<T> (Clone on Write) 写时复制 Borrowed 包裹引用,Owned 包裹所有权

Cow 的关键方法:

  • to_mut():获取可变借用,首次调用时克隆,后续调用复用第一次的克隆结果。如果 T 本身就是 Owned,则不会发生额外克隆
  • into_owned():获取一个拥有所有权的对象。如果是 Borrowed 则克隆转为 Owned;如果是 Owned 则直接转移所有权

七、字符串常用方法

char 类型提供了丰富的判断和转换方法,均基于 Unicode 字符集:

方法 功能说明 示例
is_digit(radix) 判断字符是否为指定进制的数字字符 '5'.is_digit(10)true
to_digit(radix) 将字符转换为目标进制的数值 'f'.to_digit(16)Some(15)
is_lowercase 判断是否为小写字母(Unicode Lowercase) 'a'.is_lowercasetrue
is_uppercase 判断是否为大写字母(Unicode Uppercase) 'A'.is_uppercasetrue
to_lowercase 转为小写(完整 Unicode 映射) 'A'.to_lowercase()"a"
to_uppercase 转为大写(完整 Unicode 映射) 'a'.to_uppercase()"A"
is_whitespace 判断是否为空白字符(空格、制表符等) ' '.is_whitespacetrue
is_alphabetic 判断是否为字母(汉字也算字母) '字'.is_alphabetictrue
is_alphanumeric 判断是否为字母或数字 'a'.is_alphanumerictrue
is_control 判断是否为控制字符 '\n'.is_controltrue
is_numeric 判断是否为数字(含 Unicode 数值字符) '٣'.is_numerictrue(阿拉伯数字)
escape_default 转义 \t\r\n、引号等特殊符号 返回转义后的字符串迭代器

以上方法均定义在 char 原始类型上,返回值为 boolOption<T> 等。其中涉及大小写转换的方法会处理完整的 Unicode 映射关系(如德语 ßSS),而非简单的 ASCII 替换。


以上就是 Rust 中常见的基础语法特性。掌握了这些,日常编码的大部分场景就都能应对了。如果想进一步深入,建议继续探索生命周期标注、模式匹配的高级用法、异步编程(async/await)以及宏编程等主题。