云计算百科
云计算领域专业知识百科平台

rust入门教程 (2)

Rust 入门教程 2:所有权、借用与引用

在上一教程中,我们学习了 Rust 的基础语法、变量绑定和数据类型。今天,我们将深入 Rust 最核心的概念:所有权(Ownership)、借用(Borrowing) 和 引用(References)。这些概念是 Rust 内存安全保证的基石,也是初学者最需要花时间理解的部分。

1. 所有权(Ownership)

所有权是 Rust 独特的内存管理系统。它既没有垃圾回收器(GC),也不依赖程序员手动分配和释放内存,而是通过一套规则在编译时进行检查。

1.1 所有权规则

  • 每个值在 Rust 中都有一个变量作为它的所有者(Owner)。
  • 一个值在同一时刻只能有一个所有者。
  • 当所有者离开作用域时,这个值将被丢弃(内存被释放)。
  • 1.2 作用域

    与很多编程语言类似,Rust 使用大括号 {} 来定义作用域。变量在作用域内有效。

    fn main() {
    // s 在这里无效,因为它尚未声明
    let s = \\"hello\\"; // s 从这里开始有效
    println!(\\"{}\\", s);
    } // 作用域结束,s 不再有效

    1.3 移动(Move)

    让我们来看一个稍复杂的例子。这里涉及到 String 类型,它存储在堆上。

    fn main() {
    let s1 = String::from(\\"hello\\");
    let s2 = s1;

    // println!(\\"{}\\", s1); // 这行会导致编译错误!
    println!(\\"{}\\", s2); // 正常运行
    }

    为什么 s1 不能再使用了?这是因为 let s2 = s1 发生了一次 移动(Move)。

    • String 由三部分组成:指向堆内存的指针、长度、容量。这些数据存储在栈上。
    • 真正的字符串内容 \\"hello\\" 存储在堆上。
    • 当我们将 s1 赋值给 s2 时,Rust 浅拷贝了栈上的指针、长度和容量。
    • 为了避免 s1 和 s2 同时指向同一块堆内存(导致“双倍释放”错误),Rust 立即将 s1 置为无效。
    • 因此,s1 的所有权被 移动 到了 s2。

    1.4 克隆(Clone)

    如果我们确实需要深度复制堆上的数据,可以使用 clone 方法。

    fn main() {
    let s1 = String::from(\\"hello\\");
    let s2 = s1.clone();

    println!(\\"s1 = {}, s2 = {}\\", s1, s2); // 现在都可以使用了
    }

    1.5 栈数据:复制(Copy)

    像整数、布尔、浮点数这些在编译时大小已知的类型,完全存储在栈上,因此复制它们非常快。对于这类实现了 Copy trait 的类型,赋值操作会进行完整的复制,不会发生移动。

    fn main() {
    let x = 5;
    let y = x; // x 没有被移动,而是被复制了

    println!(\\"x = {}, y = {}\\", x, y); // x 仍然有效
    }

    1.6 函数与所有权

    将变量传递给函数也会发生移动或复制。

    fn main() {
    let s = String::from(\\"hello\\");
    takes_ownership(s); // s 的值移动到函数里
    // println!(\\"{}\\", s); // 这里 s 已经无效,编译错误

    let x = 5;
    makes_copy(x); // x 是 Copy 类型,仍然有效
    println!(\\"{}\\", x); // 正常运行
    }

    fn takes_ownership(some_string: String) {
    println!(\\"{}\\", some_string);
    } // some_string 离开作用域,内存被释放

    fn makes_copy(some_integer: i32) {
    println!(\\"{}\\", some_integer);
    }

    1.7 返回值与所有权

    函数返回值也可以转移所有权。

    fn main() {
    let s1 = gives_ownership(); // 返回值移动到 s1
    let s2 = String::from(\\"hello\\");
    let s3 = takes_and_gives_back(s2); // s2 移动到函数,函数再将返回值移动到 s3
    // println!(\\"{}\\", s2); // s2 已经无效
    println!(\\"s1 = {}, s3 = {}\\", s1, s3);
    }

    fn gives_ownership() -> String {
    let some_string = String::from(\\"hello\\");
    some_string // 返回,所有权转移给调用者
    }

    fn takes_and_gives_back(a_string: String) -> String {
    a_string // 返回,所有权转移给调用者
    }

    2. 引用与借用

    每次都要把所有权传来传去很麻烦。如果我们只是想使用一个值而不获取其所有权,可以使用 引用(References)。

    2.1 引用

    引用允许你引用某个值而不获取其所有权。

    fn main() {
    let s1 = String::from(\\"hello\\");
    let len = calculate_length(&s1); // 传递 s1 的引用
    println!(\\"The length of '{}' is {}.\\", s1, len); // s1 仍然有效
    }

    fn calculate_length(s: &String) -> usize { // s 是对 String 的引用
    s.len()
    } // s 离开作用域,但它不拥有所有权,所以不会发生 drop

    这里 &s1 语法创建了一个指向 s1 值的引用,但没有获取它的所有权。因为不拥有它,所以当引用离开作用域时,它指向的值不会被丢弃。这种通过引用传递参数的行为称为 借用(Borrowing)。

    2.2 可变引用

    默认情况下,引用是不可变的。我们可以使用 &mut 来创建可变引用,从而修改借用的值。

    fn main() {
    let mut s = String::from(\\"hello\\");
    change(&mut s); // 传递可变引用
    println!(\\"{}\\", s); // 输出 \\"hello, world\\"
    }

    fn change(some_string: &mut String) {
    some_string.push_str(\\", world\\");
    }

    可变引用有一个重要限制:在特定作用域内,对于某一块数据,只能有一个活跃的可变引用。

    let mut s = String::from(\\"hello\\");

    let r1 = &mut s;
    let r2 = &mut s; // 编译错误:不能多次借用 s 作为可变引用

    这个限制防止了数据竞争。

    2.3 引用的规则

    总结一下引用的规则:

    • 在任意给定时间,要么 只能有一个可变引用,要么 只能有多个不可变引用。
    • 引用必须总是有效的(不能是悬垂引用)。

    3. 切片(Slice)

    切片是另一种不持有所有权的数据类型。它允许你引用集合中的一段连续元素序列,而不是整个集合。

    3.1 字符串切片

    fn main() {
    let s = String::from(\\"hello world\\");

    let hello = &s[0..5]; // 包含 0,不包含 5
    let world = &s[6..11]; // 包含 6,不包含 11

    println!(\\"{}, {}\\", hello, world); // 输出 \\"hello, world\\"
    }

    .. 是范围语法,有多种写法:

    let s = String::from(\\"hello\\");

    let slice1 = &s[0..2]; // \\"he\\"
    let slice2 = &s[..2]; // 从开头到索引 2,等同于 &s[0..2]
    let slice3 = &s[2..5]; // \\"llo\\"
    let slice4 = &s[2..]; // 从索引 2 到结尾
    let slice5 = &s[..]; // 整个字符串

    3.2 字符串字面量就是切片

    还记得我们之前说的 let s = \\"hello\\"; 吗?这里的 s 实际上是一个 &str 类型,它是一个指向二进制程序特定位置的切片。

    let s = \\"hello world\\"; // s: &str

    3.3 其他类型的切片

    数组也可以有切片:

    let a = [1, 2, 3, 4, 5];
    let slice = &a[1..3]; // 类型是 &[i32],包含 [2, 3]

    4. 总结

    本章我们学习了 Rust 的核心概念:

    • 所有权:每个值都有唯一的所有者,所有者离开作用域时值被释放
    • 移动:赋值或传参时,所有权可能被转移
    • 借用:通过引用访问值而不获取所有权
    • 引用规则:要么一个可变引用,要么多个不可变引用
    • 切片:对集合的部分引用

    这些概念是理解 Rust 内存安全的基础。虽然一开始可能有些抽象,但随着练习会逐渐熟悉。下一章我们将学习 Rust 的结构体和方法!

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » rust入门教程 (2)
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!