kuikuitage

  博客园  ::  :: 新随笔  :: 联系 ::  :: 管理

所有权规则

  1. Rust 中的每一个值都有一个被称为其 所有者(owner)的变量。
  2. 值有且只有一个所有者。
  3. 当所有者(变量)离开作用域,这个值将被丢弃。

变量作用域

{ // s 在这里无效, 它尚未声明
    let s = "hello"; // 从此处起,s 是有效的
    // 使用 s
} // 此作用域已结束,s 不再有效

变量是否有效与作用域的关系跟其他编程语言是类似的。

String 与所有权

let mut s = String::from("hello"); //"hello"构造一个String对象
s.push_str(", world!"); // push_str() 在字符串后追加字面值

内存与分配

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

{
    let s = String::from("hello"); // 从此处起,s 是有效的
    // 使用 s
} // 此作用域已结束,
// s 不再有效

这是一个将 String 需要的内存返回给操作系统的很自然的位置:当 s 离开作用域的时候。当变量离开作用域,Rust 为我们调用一个特殊的函数。这个函数叫做 drop ,在这里String 的作者可以放置释放内存的代码。Rust 在结尾的 } 处自动调用 drop 。这一点理解起来和C++的析构函数应该是一样的。

变量与数据交互的方式(一):移动

let s1 = String::from("hello");
let s2 = s1;
println!("{}, world!", s1); //s1已经成为无效的引用,这里会报错

rust通过move的操作,规避浅拷贝没有数据复制,深拷贝又会出现性能开销的影响,通过move操作将s2的地址指针指向s1并使s1无效。

这里还隐含了一个设计选择:Rust 永远也不会自动创建数据的 “深拷贝”。

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

确实 需要深度复制 String 中堆上的数据,而不仅仅是栈上的数据,可以使用一个
叫做 clone 的通用函数。

let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1 = {}, s2 = {}", s1, s2);

只在栈上的数据:拷贝

let x = 5;
let y = x;
println!("x = {}, y = {}", x, y);

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

大概意思就是说基本类型的数据或者不需要构造函数额外处理的对象,直接基于字节复制,应该就是C++的trivial和notirvial的区别,一旦有资源释放需要调用Drop的默认不提供Copy,等同于说如果需要析构函数做处理,则不提供深拷贝,结合C++的指针类型成员,或者虚表指针好理解。

这里 copy和move的语义,其实就是基本类型直接浅拷贝copy的,复杂对象类型默认是move的,除非字节添加copy语义,即深拷贝。

所有权与函数

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 移出作用域。不会有特殊操作

可以看到s在被作为参数传递过程中,所有权变更了,并且在被调用的函数takes_ownership结束时,被析构drop了。

当尝试在调用 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 被清理掉,除非数据被移动为另一个变量所有。99

如果我们想要函数使用一个值但不获取所有权该怎么办呢?如果我们还要接着使用它的话,每次都传进去再返回来就有点烦人了,除此之外,我们也可能想返回函数体中产生的一些数据。

解决办法之一是引用传参,解决办法二是使用元组返回多个值,实现所有权的转入再转出。

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)
}

但是这未免有些形式主义,而且这种场景应该很常见。幸运的是,Rust 对此提供了一个功
能,叫做 引用(references)。

引用与借用

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

& 符号就是 引用,它们允许你使用值但不获取其所有权

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

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

变量 s 有效的作用域与函数参数的作用域一样,不过当引用离开作用域后并不丢弃它指向的
数据,因为我们没有所有权。当函数使用引用而不是实际值作为参数,无需返回值来交还所
有权,因为就不曾拥有所有权。

我们将获取引用作为函数参数称为 借用(borrowing)

fn main() {
    let s = String::from("hello");
    change(&s);
}
fn change(some_string: &String) {
    some_string.push_str(", world");//错误,默认借用形式为不可变应用,不允许修改
}

正如变量默认是不可变的,引用也一样。(默认)不允许修改引用的值。

可变引用

fn main() {
    let mut s = String::from("hello");
    change(&mut s);
}
fn change(some_string: &mut String) {
    some_string.push_str(", world");
}

创建一个可变引用 &mut s 和接受一个可变引用some_string: &mut String 。

不过可变引用有一个很大的限制:在特定作用域中的特定数据有且只有一个可变引用

let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s;//错误:^^^^^^ second mutable borrow occurs here
println!("{}, {}", r1, r2);

即允许可变性,不过是以一种受限制的方式允许。

这个限制的好处是 Rust 可以在编译时就避免数据竞争。数据竞争(data race)类似于竞态条
件,它可由这三个行为造成:

  • 两个或更多指针同时访问同一数据。
  • 至少有一个指针被用来写入数据。
  • 没有同步数据访问的机制。
let mut s = String::from("hello");
{
    let r1 = &mut s;
} // r1 在这里离开了作用域,所以我们完全可以创建一个新的引用
let r2 = &mut s;
let mut s = String::from("hello");
let r1 = &s; // 没问题
let r2 = &s; // 没问题
let r3 = &mut s; // 大问题
println!("{}, {}, and {}", r1, r2, r3);

我们 也 不能在拥有不可变引用的同时拥有可变引用,多个不可变引用是可以的,因为没有哪个只能读取数据的人有能力影响其他人读取到的数据。

注意一个引用的作用域从声明的地方开始一直持续到最后一次使用为止。例如,因为最后一次使用不可变引用在声明可变引用之前,所以如下代码是可以编译的:

let mut s = String::from("hello");
let r1 = &s; // 没问题
let r2 = &s; // 没问题
println!("{} and {}", r1, r2);
// 此位置之后 r1 和 r2 不再使用
let r3 = &mut s; // 没问题
println!("{}", r3);

它们的作用域没有重叠,所以代码是可以编译的。

Rust 编译器在提前指出一个潜在的 bug(在编译时而不是在运行时)并精准显示问题所在。

悬垂引用(Dangling References)

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

fn main() {
    let reference_to_nothing = dangle();
}
fn dangle() -> &String {// dangle 返回一个字符串的引用
    let s = String::from("hello");// s 是一个新字符串
    &s // 返回字符串 s 的引用
}// 这里 s 离开作用域并被丢弃。其内存被释放。
// 危险!

这里返回局部变量的引用,s 是在 dangle 函数内创建的,当 dangle 的代码执行完毕后, s 将被释放。不过我们尝试返回它的引用。这意味着这个引用会指向一个无效的 String ,这可不对!Rust 不会允许我们这么做。

这里的解决方法是直接返回 String :

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

这样就没有任何错误了。所有权被移动出去,所以没有值被释放。

引用的规则

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

Slice 类型

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

fn first_word(s: &String) -> usize {
    let bytes = s.as_bytes();
    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
        return i;
        }
    }
    s.len()
}

first_word 函数返回 String 参数的一个字节索引值。

let bytes = s.as_bytes();//将 String 转化为字节数组。

iter 方法返回集合中的每一个元素, 而 enumerate 包装了 iter 的结果,将这些元素作为元组的一部分来返回。

所以在 for 循环中,我们指定了一个模式,其中元组中的 i 是索引而元组中的 &item 是单个字节。因为我们从 .iter().enumerate() 中获取了集合元素的引用,所以模式中使用了 & 。

fn main() {
    let mut s = String::from("hello world");
    let word = first_word(&s); // word 的值为 5
    s.clear(); // 这清空了字符串,使其等于 ""
    // word 在此处的值仍然是 5,
    // 但是没有更多的字符串让我们可以有效地应用数值 5。word 的值现在完全无效!
}

这个程序编译时没有任何错误,而且在调用 s.clear() 之后使用 word 也不会出错。但因为s已经被清空,所以结果可能非预期。s的ptr指向的地址可能已经被写入其他数据。

Rust如何解决这个问题:字符串 slice

字符串 slice

字符串 slice(string slice)是 String 中一部分值的引用,它看起来像这样:

let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6..11];

如果尝试从一个多字节字符的中间位置创建字符串 slice,则程序将会因错误而退出,本部分假设只使用 ASCII 字符集,即一个索引对应一个字符。多字节字符一个字符可能占据多个索引位置。

fn first_word(s: &String) -> &str {
    let bytes = s.as_bytes();
    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }
    &s[..]
}
fn main() {
    let mut s = String::from("hello world");
    let word = first_word(&s);
    s.clear(); // 错误!
    println!("the first word is: {}", word);
}

错误原因就是编译器已经发现,word是借用(引用)的s,但是在word还未销毁前,清除了s,如果删除掉最后一行打印,也会报错:

the immutable borrow prevents subsequent moves or mutable borrows of s until the borrow ends。

大意是说,因为借用是不可变引用,只有等该借用结束才能,使用可变应用s。

如果把let s = String::from("hello world");即把s定义为可变的,s.clear(); // 错误!还是会报错error: cannot borrow immutable local variable s as mutable,猜测可能只能在mut时才能用clear,先不管。

回忆一下借用规则,当拥有某值的不可变引用时,就不能再获取一个可变引用

字符串字面值就是 slice

let s = "Hello, world!";

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

字符串 slice 作为参数

fn first_word(s: &str) -> &str {

定义一个获取字符串 slice 而不是 String 引用的函数使得我们的 API 更加通用并且不会丢失任何功能

// first_word 中传入 `String` 的 slice
let word = first_word(&my_string[..]);

于是我们的调用函数也需要改进一下。

let my_string_literal = "hello world";
// 因为字符串字面值 **就是** 字符串 slice,
// 这样写也可以,即不使用 slice 语法!
let word = first_word(my_string_literal);

其他类型的 slice

字符串 slice,正如你想象的那样,是针对字符串的。不过也有更通用的 slice 类型。考虑一下
这个数组:

let a = [1, 2, 3, 4, 5];

我们也会想要引用数组的一部分。我们可以这样做:

let a = [1, 2, 3, 4, 5];
let slice = &a[1..3];

这个 slice 的类型是 &[i32]

总结

所有权、借用和 slice 这些概念让 Rust 程序在编译时确保内存安全。Rust 语言提供了跟其他
系统编程语言相同的方式来控制你使用的内存,但拥有数据所有者在离开作用域后自动清除
其数据的功能意味着你无须额外编写和调试相关的控制代码。

总之,就是Rust通过在语法规则在静态编译阶段直接报错禁止来规避内存使用错误,但对开发者的编程素养要求更高。

posted on 2020-08-02 17:29  kuikuitage  阅读(182)  评论(0编辑  收藏  举报