《Rust 语言程序设计》笔记第三章-所有权

三、所有权

1、所有权概念

所有权规则
  1. Rust 中的每一个值都有一个被称为其 所有者owner)的变量。
  2. 值在任一时刻有且只有一个所有者。
  3. 当所有者(变量)离开作用域,这个值将被丢弃。
String 类型

这里使用 String 作为例子,并专注于 String 与所有权相关的部分。

String是分配在堆上的数据,可以存储在编译时位置大小的文本。

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

s.push_str(", world!"); // push_str() 在字符串后追加字面值

println!("{}", s); // 将打印 `hello, world!`
}
内存分配

在堆上进行内存分配,意味着:

  • 必须在运行时向内存分配器(memory allocator)请求内存。
  • 需要一个当我们处理完 String 时将内存返回给分配器的方法。

第二步在一些带 GC 的语言中,不需要担心。不带 GC 的语言则需要手动释放内存。

Rust采用了一种不同的策略:内存在拥有它的变量离开作用域后就被自动释放。

{
    let s = String::from("hello"); // 从此处起,s 是有效的

    // 使用 s
}                                  // 此作用域已结束,
                                   // s 不再有效

在变量离开作用域时,会调用drop函数,释放内存。

下面是复杂场景的一些探索:

变量与数据交互的方式(一):移动
let x = 5;
let y = x;

这里“将 5 绑定到 x;接着生成一个值 x拷贝并绑定到 y”。因为整数是有已知固定大小的简单值,所以这两个 5 被放入了栈中。

下面是 String 版本:

let s1 = String::from("hello");
let s2 = s1;

实际上是 s2 从栈上拷贝了 s1 的指针、长度和容量,并没有复制指针指向的堆上数据。

s1 and s2 pointing to the same value

这种模式与上面的整数直接拷贝数据不同,没有对堆内存进行拷贝,降低对运行时性能的影响。

这里就会出现一个问题:当 s2s1 离开作用域,他们都会尝试释放相同的内存。这是一个叫做 二次释放double free)的错误,也是之前提到过的内存安全性 bug 之一。

Rust的处理方式是:认为 s1 已经无效了,不需要清理。同时也就意味着无法使用 s1

let s1 = String::from("hello");
let s2 = s1;

println!("{}, world!", s1);

这段代码无法编译通过,因为 Rust 禁止使用无效的引用!

$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0382]: borrow of moved value: `s1`
 --> src/main.rs:5:28
  |
2 |     let s1 = String::from("hello");
  |         -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
3 |     let s2 = s1;
  |              -- value moved here
4 |
5 |     println!("{}, world!", s1);
  |                            ^^ value borrowed here after move

For more information about this error, try `rustc --explain E0382`.
error: could not compile `ownership` due to previous error

**!!!**这种操作被称为 移动move),而不是浅拷贝。

s1 moved to s2

另外,**这里还隐含了一个设计选择:Rust 永远也不会自动创建数据的 “深拷贝”。**因此,任何 自动 的复制可以被认为对运行时性能影响较小。

变量与数据交互的方式(二):克隆

相当于是深拷贝的方法

let s1 = String::from("hello");
let s2 = s1.clone();

println!("s1 = {}, s2 = {}", s1, s2);

Rust 有一个叫做 Copy trait 的特殊注解,可以用在类似整型这样的存储在栈上的类型上(第十章详细讲解 trait)。如果一个类型实现了 Copy trait,那么一个旧的变量在将其赋值给其他变量后仍然可用。Rust 不允许自身或其任何部分实现了 Drop trait 的类型使用 Copy trait。如果我们对其值离开作用域时需要特殊处理的类型使用 Copy 注解,将会出现一个编译时错误。

作为一个通用的规则,任何一组简单标量值的组合都可以实现 Copy,任何不需要分配内存或某种形式资源的类型都可以实现 Copy 。如下是一些 Copy 的类型:

  • 所有整数类型,比如 u32
  • 布尔类型,bool,它的值是 truefalse
  • 所有浮点数类型,比如 f64
  • 字符类型,char
  • 元组,当且仅当其包含的类型也都实现 Copy 的时候。比如,(i32, i32) 实现了 Copy,但 (i32, String) 就没有。

所以一些简单的栈上数据使用 = 时,默认是克隆而不是移动,因为这些克隆是快速的。

所有权与函数

将值传递给函数在语义上与给变量赋值相似。向函数传递值可能会移动或者复制,就像赋值语句一样。

fn main() {
    let s = String::from("hello");  // s 进入作用域

    takes_ownership(s);             // s 的值移动到函数里 ...
                                    // ... 所以到这里不再有效

    let x = 5;                      // x 进入作用域

    makes_copy(x);                  // x 应该移动函数里,
                                    // 但 i32 是 Copy 的,所以在后面可继续使用 x

} // 这里, x 先移出了作用域,然后是 s。但因为 s 的值已被移走,
  // 所以不会有特殊操作

fn takes_ownership(some_string: String) { // some_string 进入作用域
    println!("{}", some_string);
} // 这里,some_string 移出作用域并调用 `drop` 方法。占用的内存被释放

fn makes_copy(some_integer: i32) { // some_integer 进入作用域
    println!("{}", some_integer);
} // 这里,some_integer 移出作用域。不会有特殊操作

当尝试在调用 takes_ownership 后使用 s 时,Rust 会抛出一个编译时错误。

返回值与作用域

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

fn main() {
    let s1 = gives_ownership();         // gives_ownership 将返回值
                                        // 移给 s1

    let s2 = String::from("hello");     // s2 进入作用域

    let s3 = takes_and_gives_back(s2);  // s2 被移动到
                                        // takes_and_gives_back 中,
                                        // 它也将返回值移给 s3
} // 这里, s3 移出作用域并被丢弃。s2 也移出作用域,但已被移走,
  // 所以什么也不会发生。s1 移出作用域并被丢弃

fn gives_ownership() -> String {             // gives_ownership 将返回值移动给
                                             // 调用它的函数

    let some_string = String::from("hello"); // some_string 进入作用域.

    some_string                              // 返回 some_string 并移出给调用的函数
}

// takes_and_gives_back 将传入字符串并返回该值
fn takes_and_gives_back(a_string: String) -> String { // a_string 进入作用域

    a_string  // 返回 a_string 并移出给调用的函数
}

将值赋给另一个变量时移动它。当持有堆中数据值的变量离开作用域时,其值将通过 drop 被清理掉,除非数据被移动为另一个变量所有。

如果需要在传入参数之后,再次使用这个参数,可以使用元组来返回多个值,将参数返回,再次转移所有权:

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

    let (s2, len) = calculate_length(s1);

    println!("The length of '{}' is {}.", s2, len);
}

fn calculate_length(s: String) -> (String, usize) {
    let length = s.len(); // len() 返回字符串的长度

    (s, length)
}

输出为:The length of ‘hello’ is 5.

实现了 s1 作为参数之后,再次使用。

Rust 对此提供了一个功能,叫做 引用(references),可以避免这种繁琐的方式

2、引用与借用

引用
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()
}

这里传递的是 &s1, & 符号就是 引用,它们允许你使用值但不获取其所有权。

&String s pointing at String s1

与使用 & 引用相反的操作是 解引用dereferencing),它使用解引用运算符,*

&s1 语法让我们创建一个 指向s1 的引用,但是并不拥有它。因为并不拥有这个值,所以当引用停止使用时,它所指向的值也不会被丢弃。

fn calculate_length(s: &String) -> usize { // s 是对 String 的引用
    s.len()
} // 这里,s 离开了作用域。但因为它并不拥有引用值的所有权,
  // 所以什么也不会发生

我们将创建一个引用的行为称为 借用borrowing)。

由于借用并不拥有变量的所有权,所以无法对变量进行修改。

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

    change(&s);
}

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

这份代码无法编译成功。

可变引用

将变量声明为可变变量 mut,然后函数形参类型改为 &mut

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

    change(&mut s);
}

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;

println!("{}, {}", r1, r2);

这份代码会编译失败!

r1 最后使用之前,不能再次让 r2 引用 s。这样是为了避免数据竞争

数据竞争data race)类似于竞态条件,它可由这三个行为造成:

  • 两个或更多指针同时访问同一数据。
  • 至少有一个指针被用来写入数据。
  • 没有同步数据访问的机制。

类似的规则也存在于同时使用可变与不可变引用中。

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

let r1 = &s; // 没问题
let r2 = &s; // 没问题
let r3 = &mut s; // 大问题

println!("{}, {}, and {}", r1, r2, r3);

这份代码同样无法编译通过。

!!!一个引用的作用域从声明的地方开始一直持续到最后一次使用为止。

总结两个规则:

  • 同一个对象的可变引用和不可变引用不能有生命周期重叠。
  • 同一个对象的多个可变引用不能有生命周期重叠。
悬垂引用

在具有指针的语言中,很容易通过释放内存时保留指向它的指针而错误地生成一个 悬垂指针dangling pointer),所谓悬垂指针是其指向的内存可能已经被分配给其它持有者。

fn main() {
    let reference_to_nothing = dangle();
}

fn dangle() -> &String { // dangle 返回一个字符串的引用

    let s = String::from("hello"); // s 是一个新字符串

    &s // 返回字符串 s 的引用
} // 这里 s 离开作用域并被丢弃。其内存被释放。
  // 危险!

这份代码编译无法通过,因为 s 在结束时被释放,reference_to_nothing 成了悬垂指针。

解决方法是将 s 的所有权通过函数返回移动出去:

fn no_dangle() -> String {
    let s = String::from("hello");

    s
}

3、slice

字符串 slice

字符串 slicestring slice)是 String 中一部分值的引用,前闭后开

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

let hello = &s[0..5];
let world = &s[6..11];
world containing a pointer to the byte at index 6 of String s and a length 5

对于从索引 0 开始的 slice,可以省略两个点前面的值

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

let slice = &s[0..2];
let slice = &s[..2];

同理也可以舍弃尾部的数字:

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

let len = s.len();

let slice = &s[3..len];
let slice = &s[3..];

slice 相当于不可变引用,如果运行如下代码,就会编译失败:

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

    let word = first_word(&s);

    s.clear(); // 错误!

    println!("the first word is: {}", word);
}

因为 s.clear(); 需要获取一个可变引用,但是后面又要输出 word,这边是前一个不可变引用的生命周期还未结束,此时 s.clear() 会报错。这样就避免了,对字符串改动导致 slice 无效的问题。

字符串字面值就是 slice
let s = "Hello, world!";

这里 s 的类型是 &str:它是一个指向二进制程序特定位置的 slice。这也就是为什么字符串字面值是不可变的;&str 是一个不可变引用。

最后吐槽一下,csdn 居然连 Rust 的语法高亮都没做。。。。。。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值