WHAT - 高性能和内存安全的 Rust(二)

了解 Rust 的所有权(ownership)、借用(borrowing)、可变性(mutability)以及作用域(scope)是掌握 Rust 的关键。

下面通过具体的代码示例来解释这些概念。

1. 所有权(Ownership)

所有权和生命周期是 Rust 最大的特性之一,理解了也就掌握了 80% Rust 的精髓。

  • Rust 中的每一个值都有一个所有者,某个特定的变量
  • Rust 中任何一个值在任何时刻有且只有一个所有者
  • 当所有者离开作用域,值即被丢弃

以下示例展示了基本的所有权规则:

fn main() {
    let s1 = String::from("hello");
    let s2 = s1; // s1的所有权被移动到s2
    // println!("{}", s1); // 这行代码会报错,因为s1不再拥有这个值的所有权
    println!("{}", s2); // s2现在拥有这个值
}

在这个例子中,s1将其所有权移动到s2,因此s1不再有效。可以尝试在 Rust 在线工具 中运行。

首先要明确值的概念,上述例子中,字符串在堆上被分配的空间以及内容才是真正的值,而 s1s2 是指向这个值的指针,也就是所有者。由于任何一个值在任何时刻有且只有一个所有者,当执行 let s2 = s1; 后,s1 不再拥有值的所有权,因此编译会报错。

如此编译错误也可以避免生产环境的 Use-after-free 漏洞。

再来看另一个例子:

fn main() {
    let s1 = String::from("hello");
    let length = calc_length(s1);
    println!("length of {} is {}.", s1, length);
}

fn calc_length(s: String) -> usize {
	s.len()
}

上述代码会编译错误。可以尝试在 Rust 在线工具 中运行。

问题出来 calc_length,执行该函数时,值的所有权已经有 s1 移交给 s,而因为当所有者离开作用域后对应的值会被丢弃,s 所指向的值就被丢弃了。

想要避免这个编译错误,我们需要把所有权移交出去:

fn main() {
    let s1 = String::from("hello");
    let (s2, length) = calc_length(s1);
    println!("length of {} is {}.", s2, length);
}

fn calc_length(s: String) -> (String, usize) {
	(s, s.len())
}

很不幸,上述代码会有新的编译错误。问题出在 (s, s.len());,因为任何一个值在任何时刻有且只有一个所有者,所有执行完前面一半代码后,(s, s 的所有权已经移交出去了,再执行后一半,s.len()),就会报错,因为 s 在这一刻就不存在了。

继续优化代码:

fn main() {
    let s1 = String::from("hello");
    let (s2, length) = calc_length(s1);
    println!("length of {} is {}.", s2, length);
}

fn calc_length(s: String) -> (String, usize) {
	let length = s.len();
	(s, length)
}

2. 借用(Borrowing)

上述第二个例子的代码出现一个问题:只想要获取字符串长度,却需要将所有权转移两次。因此,Rust 提供了另外一个重要的概念:借用。

借用是指通过引用传递数据,而不是通过值传递。Rust 区分可变借用和不可变借用。

不可变借用

fn main() {
    let s1 = String::from("hello");
    let len = calculate_length(&s1);
    println!("The length of '{}' is {}.", s1, len);
}

fn calculate_length(s: &String) -> usize {
    s.len() // 借用s,不会改变所有权
}

在这个例子中,calculate_length函数借用了s1的不可变引用,因此calculate_length函数不能修改s1的值。

可变借用

fn main() {
    let mut s = String::from("hello");
    change(&mut s);
    println!("{}", s);
}

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

在这个例子中,change函数借用了s的可变引用,因此可以修改s的值。

由于 Rust 对所有权以及引用有严格的控制,因此 Rust 在任何时间可以准确知晓引用的数量,因此 Rust 可以对引用使用类似读锁(共享锁)以及写锁(排他锁)的控制。换句话说,Rust 中的任何值:

  1. 在任意时间,要么只有一个可变引用(排他锁),要么只能有多个不可变引用(共享锁)
  2. 引用必须总是有效的

上述特性在多线程编程尤为重要,可以很大程度保证代码的线程安全,并且由于这些分析发生在编译时,Rust 可以避免为了保证线程安全带来的额外性能开销。

3. 可变性(Mutability)

Rust 中变量默认是不可变的,需要显式地使用 mut 关键字声明可变变量。

fn main() {
    let mut x = 5;
    println!("The value of x is: {}", x);
    x = 6;
    println!("The value of x is: {}", x);
}

在这个例子中,x被声明为可变的,因此可以修改其值。

4. 作用域(Scope)

作用域决定了变量在程序中的生命周期。变量一旦离开作用域,其值会被自动销毁。

Rust 和大部分编程语言类似,值的作用域是离它的声明最近的一对花括号中间。

fn main() {
    {
        let s = String::from("hello"); // s在这个作用域内有效
        println!("{}", s);
    } // 这里s离开作用域并被销毁
    // println!("{}", s); // 这行代码会报错,因为s已经超出了作用域
}

在这个例子中,s的作用域仅限于花括号内部,一旦超出作用域,s会被销毁,无法再访问。

综合示例

下面的示例综合展示了所有权、借用、可变性和作用域:

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

    {
        let r1 = &s; // 不可变借用
        let r2 = &s; // 不可变借用
        println!("r1: {}, r2: {}", r1, r2);
    } // r1和r2在这里离开作用域

    {
        let r3 = &mut s; // 可变借用
        r3.push_str(", world");
        println!("r3: {}", r3);
    } // r3在这里离开作用域

    println!("s: {}", s);
}

这个例子展示了如何在不同作用域内进行不可变和可变借用,以及如何在作用域结束时自动释放借用。

通过这些示例,可以更好地理解Rust中的所有权、借用、可变性和作用域的基本概念。

  • 25
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值