rust所有权系统

2022-12-30 13:39:48 浏览数 (1)

所有权系统

在Rust中,核心的设计之一是所有权(ownership)系统。该系统以一种新的方式来管理程序在运行时使用内存的方式。一些语言中具有垃圾回收机制,在程序运行时有规律地寻找不再使用的内存;在另一些语言中,程序员必须亲自分配和释放内存。Rust 则选择了第三种方式:通过所有权系统管理内存,编译器在编译时会根据一系列的规则进行检查。如果违反了任何这些规则,程序都不能编译。在运行时,所有权系统的任何功能都不会减慢程序。

变量存储的位置

Rust的基本标量数据类型都存在栈中,栈中的所有数据都必须占用已知且固定的大小。而大小未知或者可能变化的数据,则存储在堆内存中。在栈中分配内存是非常快的,因为这个时候无需操作系统分配新的内存空间,只需要将数据入栈即可。而在堆上分配内存就需要更多的工作,首先需要分配一块内存空间,然后标记这块内存为已使用,并返回一个指针表示该位置,指针的大小是固定的,可以存储在栈中。当程序需要访问堆内存的时候,必须通过指针去访问,这就导致访问堆内存比访问栈的慢。栈的数据好管理,当你的代码调用一个函数时,传递给函数的值和函数的局部变量被压入栈中,调用结束后,这些数据出栈。所有权系统的主要目的是为了管理堆数据。跟踪那些数据在堆上,最大限度减少堆上重复数据的量以及清理堆上不再使用的数据,这些问题是所有权系统要处理的

所有权规则

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

作用域

rust的变量作用域和其它的编程语言没有什么区别,如下所示。

代码语言:javascript复制
fn main() {
    {
        let x = 123;
        println!("{x}");
    }   
    // println!("{x}");    // error,变量x作用域仅限于上面的{}之中,当离开作用域之后,无法访问。
}

转移所有权

前面说过rust中每一个值有且仅有一个所有者。因此当我们将一个值绑定给另一个值的时候,会发生所有权的转移。但是下面的例子可能在你的意料之外。

代码语言:javascript复制
fn main() {
    let x = 5;
	let y = x;

	println!("{x}");
	println!("{y}");
}

这段代码可以正常运行,不会报错。这是因为,整型是基本标量类型,它的大小是固定的,存储在栈中。在执行let y = x;时会自动进行值的拷贝,因此y会得到一个新的内存空间存储值5,而不会发生所有权的转移。实际上,Rust 基本标量类型在绑定时都是通过自动拷贝的方式。现在我们将上面代码中的x,y换成在堆上的数据类型String,来观察发生的变化。

代码语言:javascript复制
fn main() {
    // 堆上的数据
	let x = String::from("Hello");
	let y = x;

	println!("{x}");
	println!("{y}");
}

这段代码除了x,y的数据类型发生了变化,其余都和之前的一致,但是这段代码是无法通过编译的。这是因为发生了所有权转移,let y = x;这行代码将x的所有权转移到y上,因此x就失效了。这有点像C 的移动构造。堆上的数据Rust是不会进行自动拷贝的。因为这个拷贝的代价是昂贵的,Rust默认不进行深拷贝,而是进行浅拷贝。String 类型是一个复杂类型,由存储在栈中的堆指针、字符串长度、字符串容量共同组成。浅拷贝的时候只拷贝堆指针、字符串长度、字符串容量。现在假定一个值可以拥有两个所有者。当变量离开作用域后,Rust 会自动调用 drop 函数并清理变量的堆内存。不过由于两个 String 变量指向了同一位置。这就有了一个问题:当 x 和 y 离开作用域,它们都会尝试释放相同的内存。这是一个叫做 二次释放(double free) 的错误,也是之前提到过的内存安全性 BUG 之一。两次释放(相同)内存会导致内存污染,它可能会导致潜在的安全漏洞。因此当 x 赋予 y 后,Rust 认为 x 不再有效,因此也无需在 x 离开作用域后 drop 任何东西,这就是把所有权从 x 转移给了 y,x 在被赋予 y 后就马上失效了。由于Rust只拷贝堆指针、字符串长度、字符串容量,并且使得x失效,这个操作被称为移动

深拷贝

Rust 永远也不会自动创建数据的 “深拷贝”。因此,任何自动的复制可以被认为对运行时性能影响较小。如果我们确实需要深度复制 String 中堆上的数据,而不仅仅是栈上的数据,可以使用一个叫做 clone 的方法。例如:

代码语言:javascript复制
fn main() {
    let x = String::from("Hello");
	let y = x.clone();	// 深拷贝

	println!("{x}");
	println!("{y}");
}

这段代码的clone方法会深拷贝,能够正常运行。

浅拷贝

浅拷贝只发生在栈上,前面整型赋值(绑定)的例子就是发生在栈上的。实际上对于栈上的数据而言,没有深浅拷贝的区别,因此这里调用 clone 并不会与通常的浅拷贝有什么不同。例如:

代码语言:javascript复制
fn main() {
    let x = 123;
	let y = x.clone();      // 调用clone和不调用clone没有区别

	println!("{x}");
	println!("{y}");
}

这段代码调用了clone,和前文没有调用clone运行结果是一致的。

Rust 有一个叫做 Copy 的特征,可以用在类似整型这样在栈中存储的类型。如果一个类型拥有 Copy 特征,一个旧的变量在被赋值给其他变量后仍然可用。

那么什么类型是可 Copy 的呢?可以查看给定类型的文档来确认,不过作为一个通用的规则: 任何基本类型的组合可以 Copy ,不需要分配内存或某种形式资源的类型是可以 Copy 的。如下是一些 Copy 的类型:

代码语言:javascript复制
所有整数类型,比如 u32。
布尔类型,bool,它的值是 true 和 false。
所有浮点数类型,比如 f64。
字符类型,char。
元组,当且仅当其包含的类型也都是 Copy 的时候。比如,(i32, i32) 是 Copy 的,但 (i32, String) 就不是。
不可变引用 &T

关于引用和借用下一篇文章再讲。可以发现,所有权系统很强大,通过它我们合理的管理了堆内存,但是另外一个问题出现了“总是把一个值传来传去来使用它,会非常麻烦”。为了解决这个问题,Rust提供了引用和借用。

函数与所有权

代码语言:javascript复制
fn main() {
    let s = String::from("hello");  // s 进入作用域
    takes_ownership(s);             // s 移动到函数takes_ownership里,所以 s 在这之后不在有效

    let x = 5;                      // x 进入作用域
    makes_copy(x);                  // i32 是 Copy 的,makes_copy 中的是x的拷贝,后续x仍旧有效
    println!("{}", 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 移出作用域,由于它是i32类型,不会有特殊操作。

这个例子展示了Rust函数调用时,所有权的传递过程。同样,函数的返回值也有所有权,它也会发生所有权的传递。例如:

代码语言:javascript复制
fn main() {
    let s1 = gives_ownership();         // gives_ownership 将返回值所有权移给 s1
	println!("{s1}");

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

    let s3 = takes_and_gives_back(s2);  // s2 被移动到 takes_and_gives_back 中, 同时将返回值移给 s3
	println!("{s3}");
} // 这里, s3 移出作用域并调用drop;s2 也移出作用域,但由于s2已经被移走,所以什么也不会发生;s1 移出作用域并调用drop。

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 并移出给调用的函数
}

参考资料

Rust语言圣经

0 人点赞