Loading

Rust所有权&引用

Rust使用纯手动和GC之外的第三种内存管理方式,即——所有权系统。

所有权规则

  1. Rust中的每个都有一个所有者(owner)
  2. 值在任意一刻有且只有一个所有者
  3. 当所有者(变量)离开作用域,这个值将会被丢弃

上面所说的是,值具有所有者,所谓所有者就是当前承载该值的变量,一定要搞清值和所有者的概念和区别。

变量作用域

{   // 进入作用域,目前s不可用
    let s = String::from("HHH");  // 创建s,目前s可用
    // s可用
} // 离开作用域,s不可用

这和其它语言的变量作用域没什么差别,不过在rust中,当变量s离开作用域,rust会为我们自动调用它的drop方法,String的实现者可以实现该方法来完成内存的释放。在rust中,内存在拥有它的变量离开作用域后就被自动释放

貌似不实现也会有drop方法

所有权移动

let str1 = String::from("hello");
let str2 = str1;
println!("{}", str1);

在大部分语言中,上面的代码都是将str1的引用赋值给str2,并不会造成堆上数据(字符串"hello")的拷贝。在rust中也是这样的,不过,rust的所有权规则有如下定义:

值在任意一刻有且只有一个所有者

所以,实际上,上面的代码实际上有一个错误,在第1行中,str1持有这个字符串值,在第2行中,这个字符串值的所有权转移到了str2上,所以,str1无效了。你尝试编译时会出现这样的错误,编译器告诉你值已经被转移了:

img

考虑如果rust允许str1str2同时持有该值,那么在作用域结束后,它会对这一个值drop两次。

rust的各种限制可以将所有(至少是大部分吧)内存错误在编译时被发现

下面的代码都不会出错,因为它们操作的都是字面量,和其它语言一样,字面量在赋值时是直接复制值的,而不是传递引用。这涉及到copy trait,稍后会说。

let str1 = "hello";
let str2 = str1;
println!("{}", str1);

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

上面所述的“引用”是其它语言中的引用的概念,比如Java中的引用。在Rust中,引用又是另一个东西,你可以通过&符号来获取一个变量存储的值的引用。

克隆和拷贝

当你确实想对某一个对象进行值赋值时,而非引用赋值,即类似其它语言中拷贝的概念,你可以使用通用方法clone来实现克隆:

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

如上面所说,那些只在栈上存储的数据,比如i32、字符串字面量,对它们的传递本身就是使用拷贝的,如果一个类实现了copy trait,它们在进行赋值给其它变量时就会使用拷贝,也不会发生所有权移动,因为此时它们已经是两个值了

rust中实现copy trait的类型:

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

所有权与函数

注意,在调用函数时实际上也是做了一次隐含的赋值操作,这里也涉及到所有权移动。

fn take_ownership(some_string: String) { // 值的所有权移动到`some_string`变量上
    println!("{}", some_string);
} // 超出作用域,销毁`some_string`的值,drop被调用

fn main() {
    let s = String::from("Hello");  
    take_ownership(s); // 变量s的值发生所有权转移,它不属于s了
    println!("{}", s); // move occurs because `s` has type `String`, which does not implement the `Copy` trait
} // s不具有任何值的所有权,所以什么都不会发生

相同的,对于实现了copy trait的类型,赋值时进行一次拷贝,所以不影响拷贝前的值

fn main() {
    let x = 15;
    makes_copy(x);  // 发生值拷贝
    println!("{}", x); // correct because x implement copy trait
} // x被移除作用域,同时它是栈上值,直接无了

fn makes_copy(some_integer: i32) { // 值被拷贝了一份新的给some_integer
    println!("{}", some_integer);
} // some_integer被移除作用域,同时它是栈上值,直接无了

返回值所有权

fn takes_and_gives_back(a: String) -> String {  // 值所有权转移到a
    a // 将a的值的所有权返回给调用者
}

fn main() {
    let s3 = String::from("ZZBBB");
    let s4 = takes_and_gives_back(s3); // 将s3的值的所有权转移给函数,同时用s4接受返回的值
    // s3不能使用了,因为它已经不拥有任何值的所有权
    println!("{}", s4);
}

引用与借用

前文说过,Rust中的引用是另一个独立的东西,现在所说的就是这个引用。

引用允许你使用值但不获取其所有权,可以像如下一样使用引用进行函数参数传递,这其中不会涉及所有权的转移:

fn reference_pass(some_string: &String) -> usize { // 传递引用
    some_string.len()  // 使用引用进行访问
} // some_string离开了作用域,但它是引用,不具有值的所有权,所以什么都不会发生

fn main() {
    let s2 = String::from("World");
    reference_pass(&s2); // 传递引用,不涉及到所有权转移
    println!("{}", s2); // s2依然可用
}

如下图,s是变量s1的一个引用,变量s1保存在栈上,它其中有一个指向其拥有的堆中值的指针。

img

使用引用在rust中称为借用,即你暂时使用一下这个值,但不拥有,出于此,无法使用普通引用去修改其中的值

fn reference_pass(some_string: &String) -> usize {
    some_string.push_str("test"); // cannot borrow `*some_string` as mutable, as it is behind a `&` reference `some_string` is a `&` reference, so the data it refers to cannot be borrowed as mutable
    some_string.len()
}

可变引用

你可以使用可变引用来修改引用中的值。

fn reference_pass(some_string: &mut String) -> usize {
    some_string.push_str("test");
    some_string.len()
}

fn main() {
    let mut s2 = String::from("World"); // 变量必须是mut的,否则变量本身就改不了
    reference_pass(&mut s2); // 引用必须创建mut的
    println!("{}", s2);
}

可变引用和非可变引用的关系就像数据库系统中排他锁和共享锁的区别,一旦一个变量有了一个可变引用,你再也不能创建任何类型的该变量的引用,因为这会造成同时读写的竞态条件。但你可以同时创建多个非可变引用。

let mut mutvar = String::from("MUTVAR");
let ref_1 = &mut mutvar;
let ref_2 = &mut mutvar; //  cannot borrow `mutvar` as mutable more than once at a time
let ref_3 = &mutvar; // cannot borrow `mutvar` as immutable because it is also borrowed as mutable immutable borrow occurs here
println!("{}", ref_1);
println!("{}", ref_2);

而且,一旦你创建了一个可变引用,直到它离开作用域之前,原变量都不能被修改

let mut mutvar = String::from("MUTVAR");
let ref_1 = &mut mutvar;
mutvar.push_str("asdf"); // cannot borrow `mutvar` as mutable more than once at a time second mutable borrow occurs here

但一旦这个可变引用脱离了作用域,你就可以修改原变量

let mut mutvar = String::from("MUTVAR");
{
    let ref_1 = &mut mutvar;
}
mutvar.push_str("asdf"); // 可以通过编译

可变引用在它最后一次被使用后失效,意思就是:

let mut mutvar = String::from("MUTVAR");
let ref_1 = &mut mutvar;
ref_1.push_str("AB"); // 这里是`ref_1`在所有代码中最后被使用的位置,从这里之后,它失效了
let ref_2 = &mut mutvar; // 正常使用
let mut mutvar = String::from("MUTVAR");
let ref_1 = &mut mutvar; // 这里是`ref_1`在所有代码中最后被使用的位置,从这里之后,它失效了
let ref_2 = &mut mutvar; // 正常使用
let mut mutvar = String::from("MUTVAR");
let ref_1 = &mut mutvar;
let ref_2 = &mut mutvar;  // 你不能在可变引用ref_1后面还要被使用的情况下创建新的引用
println!("{}", ref_1); // 最后一次使用ref_1

也就是说在任意一段代码中,不可能存在一个可变引用还没结束被使用的情况下,再创建其它引用的可能,从根本上杜绝了竞态条件

垂悬引用

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

fn dangle() -> &String {
    let s = String::from("hello");
    &s // dangle结束,s被销毁,无法返回引用
}

slice引用

很多语言里都有切片的概念,rust也有,rust可以对字符串进行切片。

fn main() {
    let mut my_string = String::from("Hello, World!");
    let slice = &my_string[7..];
    println!("{}", slice);
}

rust中的字符串切片类型是&str,它是一个不可变引用,所以像如下这样写就会出错:

fn main() {
    let mut my_string = String::from("Hello, World!");
    let slice = &my_string[7..]; // &x[start_index..len] len可以省略
    my_string.push_str("zindex"); // 这一行错了,已经创建了不可变引用,在它的使用范围内,不可改变`my_string`
    println!("{}", slice);
}

下图中,s是一个字符串,world是一个切片引用,它指向String值中的一个位置,并且具有一个长度信息,通过这两个信息就可以确定切片的范围
img

字符串字面量就是一个&str切片,所以它是不可变的。

posted @ 2022-11-14 11:47  yudoge  阅读(154)  评论(0编辑  收藏  举报