一边读 Rust 程序设计语言这本电子书,一边做 Rustlings 练习。Rustlings 的设计理念是”通过修复错误来学习”。
一、引用与借用
let a = [1, 2, 3, 4, 5];
// ❌ 拷贝(会复制数据)
let copied = a[1..4].to_vec(); // 新建 Vec,复制 3 个元素
// ✅ 借用(零拷贝,只是引用。属于借用的概念,不拥有)
let borrowed = &a[1..4]; // 胖指针,引用原数组
| 术语 | 含义 | 语法 |
|---|---|---|
| 引用 (reference) | 指向数据的指针,不拥有所有权 | & 或 &mut |
| 不可变借用 (immutable borrow) | 通过引用读取数据,不能修改 | &T |
| 可变借用 (mutable borrow) | 通过引用读取和修改数据 | &mut T |
引用是语法形式,借用是语义行为。不可变借用使用不可变引用,可变借用使用可变引用。两者都是“引用”。
可变引用需要解引用才能修改指向的值。可变引用的使用场景之一:给函数传递一些非copy trait参数,不希望这个参数在函数结束后退出作用域时被注销,同时在函数中这个参数会被修改(本质是在不转移所有权的前提下,对数据进行修改)。
二、所有权和参数类型
哪些类型在编程时不需要在意所有权,哪些类型(包括集合类型)需要在意所有权?这些在意所有权的在什么情况下有强制转换,编写时可以忽略掉所有权,但开发者需要清楚某些值已经被移动了。
实现 Copy trait 的类型
这些类型按值传递时会自动拷贝,所以原变量仍然有自己的所有权。
- 基本标量:
类型 示例 整数 i8,i16,i32,i64,i128,isize,u8,u16,u32,u64,u128,usize浮点数 f32,f64布尔 bool字符 char空单元 ()
isize 和 usize 的主要应用场景是用作某些集合的索引。比如,usize 是平台相关的,当32位系统则 usize = 32位无符号整数。
- 复合类型(复合类型的元素都实现 Copy 时):
类型 示例 数组 [i32; 5]元组 (i32, bool)—— 前提是内部类型都实现 Copy引用 &T,&mut T(本身是指针,拷贝指针)
函数指针 fn()
实现 Copy 的自定义 struct/enum。是指struct/enum中的类型都实现了Copy trait,或创建Copy trait时人为实现了Copy trait.
未实现 Copy 的类型
这些类型按值传递时会移动(move),原变量失效不能再被读写。
let s = String::from("hello");
let t = s; // s 被移动,s 失效 ❌
// println!("{}", s); // 编译错误!
堆分配/拥有资源的类型:
类型 拥有的资源 String堆上的字符串数据 Vec<T>堆上的动态数组 Box<T>堆上的单个值 HashMap<K, V>堆上的哈希表 HashSet<T>堆上的哈希集合 BTreeMap,BTreeSet堆上的树结构 Rc<T>,Arc<T>引用计数堆数据 VecDeque<T>双端队列 LinkedList<T>链表 String相关OsString,CStringI/O 与系统资源:
类型 资源 File文件描述符 TcpStream,UdpSocket网络连接 Mutex<T>,RwLock<T>锁 Child(进程)子进程句柄
可以总结为:堆分配字符串、所有集合类型、包含非 Copy 成员的元组/结构体、智能指针和可变引用(同一时刻只能有一个可变引用)。
容易发生所有权转移的操作
迭代器的 into_iter() 和 iter()
let v = vec![1, 2, 3]; // iter() - 借用,不转移 for x in v.iter() { /* x 是 &i32 */ } // into_iter() - 转移所有权(消耗 v) for x in v.into_iter() { /* x 是 i32,v 失效 */ } // for x in v {} 默认就是 into_iter(),v 被移动!闭包捕获(Capture)
let s = String::from("hello"); // 不可变借用 let f = || println!("{}", s); // 自动推断为 &s // 转移所有权(move 闭包) let f = move || println!("{}", s); // s 被移动进闭包函数参数与返回值
fn take(s: String) { } // s 被移动进函数 let s = String::from("hi"); take(s); // s 被移动,之后不可用 // println!("{}", s); // ❌ 错误集合的 insert/get
let mut map = HashMap::new(); let key = String::from("a"); map.insert(key, 1); // key 被移动进 map // println!("{}", key); // ❌ key 已失效 let k = String::from("a"); map.get(&k); // 借用查询,k 仍可用 ✅
- 模式匹配中的转移
fn plus_one(x: Option<i32>) -> Option<i32> { match x { None => None, // x 是 None,直接返回 None Some(i) => Some(i + 1), // x 是 Some,取出里面的值 +1 } } let five = Some(5); // five: Option<i32> let six = plus_one(five); // ❌ five 被移动了! // println!("{:?}", five); // 编译错误!five 已失效 let opt = Some(String::from("hello")); // 1. 转移所有权(默认) match opt { Some(s) => println!("{}", s), // String 被 move 到 s None => {}, } // opt 已失效 ❌ // 2. 借用(不转移) match &opt { Some(s) => println!("{}", s), // s 是 &String,借用 None => {}, } // opt 仍可用 ✅ // 3. 可变借用 match &mut opt { Some(s) => s.push_str("!"), // s 是 &mut String None => {}, } // 4. ref 关键字(显式绑定引用) match opt { Some(ref s) => println!("{}", s), // s 是 &String None => {}, } // opt 仍可用 ✅
三、字符串和字面量
String 可以借用为 &str,但无法变成静态字面量(如果 String 内容恰好是编译时常量,可以硬编码,否则不可能获得 ‘static 引用)。像"word"这样一个静态字面量在程序二进制中(静态只读)。
// String → &str 但是不能算是静态的了
let s = String::from("hello");
let slice: &str = &s; // 自动转换:&String → &str
let slice = s.as_str(); // 显式方法
// &str → String
let slice = "hello";
let s = slice.to_string(); // 分配新内存
let s = String::from(slice); // 分配新内存
四、常量和不可变变量
| 特性 | 常量 (const) |
不可变变量 (let) |
|---|---|---|
| 关键字 | const |
let |
| 是否必须显式标注类型 | 是,必须写类型 | 通常可省略,由编译器推导 |
| 初始化时机 | 编译时,必须用常量表达式求值 | 运行时求值即可(当然也能用常量) |
| 作用域 | 全局或模块作用域,或函数内的块作用域 | 通常局部作用域(函数/代码块内) |
| 内存/存储 | 没有固定的内存地址,可能被内联到使用处 | 有确定的内存位置(栈上) |
是否支持 mut 变可变 |
不支持,永远不可变 | 支持,加 mut 就变成可变变量 |
能否在 match 或 const 泛型中使用 |
能,因为编译期确定 | 不能(除非用 const 环境) |
将整个程序中用到的硬编码(hardcode)值命名为常量,对于将该值的含义传达给代码的未来维护者很有用。如果将来需要更改硬编码的值,则只需要在代码中改动一处就可以了。
五、作用域
在 Rust 中,所有变量的作用域最终都是由花括号 {} 来界定的。无论变量是通过 if let、for 循环、match 分支还是函数参数引入的,它们的生命周期结束点(即 drop 被调用的位置)都在某个花括号的 } 处。
fn main() {
let x = 5;
let x = x + 1;
{
let x = x * 2; // 嵌套规则:内层可以访问外层变量
println!("The value of x in the inner scope is: {}", x); // 12
}
println!("The value of x is: {}", x); // 2
}
但是,上面那句话不完全正确。如下例子:
let mut s = String::from("hello");
let r1 = &s; // 没问题
let r2 = &s; // 没问题
println!("{} and {}", r1, r2);
// 此位置之后 r1 和 r2 不再使用
let r3 = &mut s; // 没问题
println!("{}", r3);
不可变引用 r1 和 r2 的作用域在 println! 最后一次使用之后结束,这也是创建可变引用 r3 的地方。编译器会分析变量的最后一次使用位置,允许生命周期提前结束,而不是死板地等到花括号。