一边读 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 的类型

这些类型按值传递时会自动拷贝,所以原变量仍然有自己的所有权。

  1. 基本标量:
    类型 示例
    整数 i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize
    浮点数 f32, f64
    布尔 bool
    字符 char
    空单元 ()

isize 和 usize 的主要应用场景是用作某些集合的索引。比如,usize 是平台相关的,当32位系统则 usize = 32位无符号整数。

  1. 复合类型(复合类型的元素都实现 Copy 时):
    类型 示例
    数组 [i32; 5]
    元组 (i32, bool) —— 前提是内部类型都实现 Copy
    引用 &T, &mut T(本身是指针,拷贝指针)
  1. 函数指针 fn()

  2. 实现 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);  // 编译错误!
  1. 堆分配/拥有资源的类型:

    类型 拥有的资源
    String 堆上的字符串数据
    Vec<T> 堆上的动态数组
    Box<T> 堆上的单个值
    HashMap<K, V> 堆上的哈希表
    HashSet<T> 堆上的哈希集合
    BTreeMap, BTreeSet 堆上的树结构
    Rc<T>, Arc<T> 引用计数堆数据
    VecDeque<T> 双端队列
    LinkedList<T> 链表
    String 相关 OsString, CString
  2. I/O 与系统资源:

    类型 资源
    File 文件描述符
    TcpStream, UdpSocket 网络连接
    Mutex<T>, RwLock<T>
    Child(进程) 子进程句柄

可以总结为:堆分配字符串、所有集合类型、包含非 Copy 成员的元组/结构体、智能指针和可变引用(同一时刻只能有一个可变引用)。

容易发生所有权转移的操作

  1. 迭代器的 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 被移动!
  2. 闭包捕获(Capture)

    let s = String::from("hello");
    
    // 不可变借用
    let f = || println!("{}", s);  // 自动推断为 &s
    
    // 转移所有权(move 闭包)
    let f = move || println!("{}", s);  // s 被移动进闭包
  3. 函数参数与返回值

    fn take(s: String) { }  // s 被移动进函数
    
    let s = String::from("hi");
    take(s);        // s 被移动,之后不可用
    // println!("{}", s);  // ❌ 错误
  4. 集合的 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 仍可用 ✅
  1. 模式匹配中的转移
    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 的地方。编译器会分析变量的最后一次使用位置,允许生命周期提前结束,而不是死板地等到花括号。


文章作者: 易百分
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 易百分 !
 上一篇
GCC、G++ 与 Windows SDK:C/C++ 混合编译和头文件排查 GCC、G++ 与 Windows SDK:C/C++ 混合编译和头文件排查
从编译与链接流程出发,说明 gcc 和 g++ 的区别、C/C++ 混合工程的构建方式,以及 Windows SDK 头文件无法被 MinGW 找到或使用时的排查方法。
下一篇 
latex报错 latex报错
大部分情况下latex版本导致的问题都比较好解决,这个bug由于报错指向地方不那么明确,因此一直没发现。
  目录