所有权是Rust最独特的特征。使rust能在不需要垃圾收集器的情况下,保证内存安全。所有权的相关特征:借用、切片以及rust如何在内存中布局数据。
什么是所有权?
所有权是rust管理内存的一组规则。所有语言都必须在运行时管理计算机内存。java中使用的是垃圾回收机制,c++使用显式分配和释放内存,而rust是通过所有权定义一组规则,若违法任何规则,则程序无法变异。所有权的任何功能都不会在程序运行时减慢程序的速度。
堆栈:先进后出,分配更快,因为始终在堆栈的顶部分配。
堆:访问速度比堆栈慢,因为需根据指针。
所有权规则:
1. 每个值都有一个owner。 2. 同一时间只能有一个owner。 3. 当owner超出范围时,改值将被删除。
变量及其有效范围:
-
当s进入范围时,他是有效的。
-
在超出范围之前一直有效。
String 所有权
字符串文字(不带mut)是不可变的,但使用push_str,会改变字符串。
内存和分配
当string不带mut时,在编译时就知道内容,故而文本直接被硬编码到可执行文件中,速度高效。但大多情况下,String类型都是可变的、可增长的文本片段。为支持可变需做以下工作:
-
必须在运行时从内存分配器请求内存。
-
当字符串定义完成后,需使用一种方法将内存返回给分配器。
第一部分由我们完成:当调用String::from时,它的实现请求它需要的内存。
第二部分略有不同。在java中,gc会跟踪并清理不再使用的内存。在c++中,开发人员需确定何时不再使用内存,并调用代码显式释放内存(困难重重)。而在rust中,采用不同的方式:一个变量超出范围,内存就会返回。
{ let s = String::from("hello"); // s is valid from this point forward // do stuff with s } // this scope is now over, and s is no // longer valid
当调用String::from时,我们将String所需的内存返回给分配器。当字符串变量离开其作用域时,rust会自动调用drop函数。创建String类型的开发可以在drop函数中编写代码,将内存返还给分配器、Rust在关闭大括号时自动调用drop函数。
变量交互
简单类型交互
String类型交互
String类型的三部分:指向保存字符串堆的指针,长度和容量。
若不转移所有权,当一个变量超出范围时,rust在清理堆内存时,将同时释放s1和s2相同位置的内存。将会产生双重释放错误,会导致内存损坏,也会导致安全漏洞。
不同于浅拷贝,在rust中此操作会使第一个变量无效,故它被称为移动。此操作中我们会说s1被移动到了s2。
如此如此,就只有s2有效,当超出范围时只有它单独释放内存!
此现象也说明了一个设计选择: Rust永远不会自动创建数据的“深”副本。因此,可以认为任何自动复制在运行时性能方面都是廉价的。
clone
同时复制了堆数据,开销很大。
只在堆栈中的数据
在编译时具有已知大小的整数等类型完全存储在堆栈中,因此可以快速复制实际值。这意味着我们没有理由x
在创建变量后阻止其生效y
。换句话说,这里的深拷贝和浅拷贝没有区别,所以调用clone
和通常的浅拷贝没有什么不同,
let x = 5;
let y = x;
println!("x = {}, y = {}", x, y);
Rust 中有一种特殊的注释,叫做 Copy
trait,它可以放在存储在栈中的类型上。如果一个类型实现了 Copy
trait,使用它的变量不会移动,而是被简单复制,使它们在赋值给另一个变量后仍然有效。
如果类型或其任何部分实现了 Drop
trait,Rust 将不会让我们在类型上添加 Copy
注释。如果类型需要在值超出作用域时执行特殊操作,并且将 Copy
注释添加到该类型,则会出现编译时错误。
作为一般规则,任何简单标量值组都可以实现 Copy
,而任何需要分配或为某种资源的类型都不能实现 Copy
。以下是一些实现 Copy
trait 的类型:
-
所有整数类型,例如 u32。
-
布尔类型 bool,其值为 true 和 false。
-
所有浮点类型
-
所有字符类型 char。
-
元组,如果它们只包含也实现了
Copy
的类型。例如,(i32,i32)实现了Copy
,但(i32,String)不能。
这是实现 `Copy` trait 的一些类型的完整列表。注意,这些类型在赋值给另一个变量时不会移动,因为它们实现了 `Copy` trait,而不是因为它们存储在栈中。
例如,像数组这样的类型可以存储在堆上,但也可以实现 Copy
trait。这意味着在赋值给另一个变量时,该数组的所有内容将被简单复制,而不是移动。
同样,字符串 slice 也可以实现 Copy
trait,尽管它们存储在堆上。这意味着在赋值给另一个变量时,它们将被简单复制,而不是移动。
总的来说,如果希望类型能够被简单复制而不是移动,可以尝试将 Copy
trait 添加到该类型上。这在某些情况下可以使代码更简单,并且在运行时间方面也很容易。但是,要注意,如果类型需要在值超出作用域时执行特殊操作,则不能将 Copy
trait 添加到该类型。
函数所有权
rust中变了传递给函数的机制与将变量赋值给另一变量类似。
String类型不实现Copy
trait,故而函数结束后打印s(使用&s就没有此问题)会出错,但x由于是int类型,故函数结束后仍然存在。
Return Values and Scope
返回值也可以转移所有权。
变量的所有权每次都遵循相同的模式:将一个值赋给另一个变量会移动它。当包含堆上数据的变量超出范围时,drop除非数据的所有权已移至另一个变量,否则将清除该值。
但对于一个本应通用的概念来说,这是太多的仪式和大量的工作。对我们来说幸运的是,Rust 有一个使用值而不转移所有权的特性,称为引用
传递引用可以使其他变量拥有数据,而不获得值的所有权。引用类似于指针,根据地址来访问值,引用保证了在该引用的生命周期内指向特定类型的有效值。
引用图示: (与使用引用&
相反的是取消引用,这是通过取消引用运算符完成的*
)
fn calculate_length(s: &String) -> usize { // s is a reference to a String
s.len()
} // Here, s goes out of scope. But because it does not have ownership of what
// it refers to, it is not dropped.
变量s
有效的范围与任何函数参数的范围相同,但引用指向的值在s
停止使用时不会被删除,因为s
没有所有权。当函数将引用作为参数而不是实际值时,我们不需要返回值来归还所有权,因为我们从来没有所有权。 usize表示无符号整数、值的范围取决于系统架构(32位或64位)
正如默认情况下变量是不可变的一样,引用也是如此,未加mut就不可改变。
可变引用
一个值的可变引用同一时间只能有一个(必须等第一个借用者使用完才行,类似于单个可变引用)。
因为我们不能在同一时间多次可变借用 s。第一个可变借用在 r1 中,必须持续到在 println! 中使用它之后,但在创建那个可变引用并使用它之间,我们尝试创建另一个可变引用 r2,该引用借用了与 r1 相同的数据。
类似于单个可变引用:
限制防止同时对相同数据进行多次可变引用,允许在非常受控的方式下进行mut。这是新 Rustaceans 难以应对的,因为大多数语言都允许您在需要时进行mut。这种限制的好处在于,Rust 可以在编译时防止数据竞争。数据竞争类似于竞争条件,当出现以下三种行为时会发生:
-
两个或更多指针同时访问相同的数据。
-
至少有一个指针用于写入数据。
-
没有同步访问数据的机制。
数据竞争导致未定义行为,在运行时跟踪它们时很难诊断和修复;Rust 通过拒绝编译具有数据竞争的代码来防止这个问题!
多个可变引用(不同时)
新增范围,类似于上面的代码,但是更直观。
可变引用与不可变引用
可变引用允许在引用的数据上修改数据,而不可变引用则不允许。
在 Rust 中,如果要修改一个变量,则需要使用可变引用。而如果只是希望读取一个变量的值,而不希望修改它,则可以使用不可变引用。
类似与上面,在最后一次使用之后(范围不重叠),运行使用,建议加括号。编译器可以判断在范围结束之前的某个点不再使用该引用。
加上括号的版本(更直观)!
悬挂引用
在有指针的语言中,很容易通过在保留指向内存的指针的同时释放某些内存来错误地创建一个悬挂指针(即指向可能已给其他人的内存位置的指针)。相比之下,Rust 可以保证引用永远不会是悬挂引用:如果你对某些数据有一个引用,编译器会确保数据在引用数据之前不会离开作用域。
引用没有所有权,一下案例赶回的是借用,而真正的所有权仍在s上,而s在离开作用域后就已经被丢弃。因此以下代码是无效的!
正确的方法是直接移动所有权!
Slice Type
切片可以引用集合中连续的元素序列,而不是整个集合。切片是一种引用,因此他没有所有权。
不使用切片找单词
下面的方法可以获取到第一个单词结尾的下标,但此值是独立于String的,所以不能保证他将来有效。
将String转为字节数组。
let bytes = s.as_bytes();
使用迭代器遍历字节数组,enumerate包装了结果iter并将每个元素作为元组的一部分返回。第一个元素为索引,第二个元素为该元素的引用。
for (i, &item) in bytes.iter().enumerate(){}
获取结果后修改字符串
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()
}
fn main() {
let mut s = String::from("hello world");
let word = first_word(&s); // word will get the value 5
s.clear(); // this empties the String, making it equal to ""
// word still has the value 5 here, but there's no more string that
// we could meaningfully use the value 5 with. word is now totally invalid!
}
此种情况下,若想通过获取的下标值配合字符串获取第一个单词便会出错。
String Slices
字符串切片是对字符串一部分的引用。
一些切片的简便写法:
-
如果你想从索引 0 开始,你可以删除两个句点之前的值。
-
如果您的切片包含 的最后一个字节
String
,您可以删除尾随数字。 -
可以删除这两个值以获取整个字符串的一部分。
let s = String::from("hello");
let len = s.len();
// 以下相等
let slice = &s[0..2];
let slice = &s[..2];
// 以下相等
let slice = &s[3..len];
let slice = &s[3..];
// 以下相等
let slice = &s[0..len];
let slice = &s[..];
let slice = &s; // 当参数为&str时,二者相同;当参数为&String时,只能传&s
修改后的first_word
借用规则,如果我们有一个对某物的不可变引用,我们就不能同时使用一个可变引用。我认为就是被借用后,未使用完就不可以修改。
String Slices as Parameters
fn first_word(s: &String) -> &str {
fn first_word(s: &str) -> &str {
第一个函数的参数是一个 String 类型的引用,返回值是一个字符串 slice。第二个函数的参数是一个字符串 slice,返回值也是一个字符串 slice。
这两个函数在功能上是类似的,但它们处理的数据类型不同。第一个函数传入的是一个 String 类型的值,而第二个函数传入的是一个字符串 slice。
在需要字符串为参数时,更好的选择是使用字符串切片为参数,它允许我们对&String
值和&str
值使用相同的函数。
传切片
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
fn main() {
let my_string = String::from("hello world");
// `first_word` works on slices of `String`s, whether partial or whole
let word = first_word(&my_string[0..6]);
let word = first_word(&my_string[..]);
// `first_word` also works on references to `String`s, which are equivalent
// to whole slices of `String`s
let word = first_word(&my_string);
let my_string_literal = "hello world";
// `first_word` works on slices of string literals, whether partial or whole
let word = first_word(&my_string_literal[0..6]);
let word = first_word(&my_string_literal[..]);
// Because string literals *are* string slices already,
// this works too, without the slice syntax!
let word = first_word(my_string_literal);
}
在let s = "hello world"时,创建的是字符串字面量。使用 from
函数创建的字符串和字符串字面量是不同的东西。前者是堆上的字符串,后者是常量存储在程序二进制文件中的字符串。如果你将一个 &
引用传递给字符串字面量,它会被解释为字符串 slice。但是,如果你将 &
引用传递给通过 from
函数创建的字符串,它将会被解释为字符串类型的引用。
这是因为字符串字面量本身就是字符串 slice,所以也可以直接调用 first_word 函数,无需使用 slice 语法。
如果你传入了一个字符串字面量的引用,比如 &"hello world",那么它将会被转化为一个字符串 slice 类型,即 &str。所以,传入 &"hello world" 与传入 "hello world" 是等价的。
使用from创建的字符串与字符串字面量
使用 String::from
创建的字符串是在堆上的动态内存。使用 &
获取这个字符串的引用,返回的是一个字符串 slice,而不是字符串字面量。
字符串字面量是编译时就被确定的常量字符串。使用 let s = "str"
定义一个字符串字面量,这个字符串存储在程序的只读数据段中,可以直接使用。使用 &
取出来的依然是字符串字面量。字符串字面量是slice的一种特殊情况。其值是字符串字面量的地址(只读内存区域)。
Other Slices
字符串切片是特定与字符串的。但其他集合也可以使用切片。
{:?}
是格式化输出中的一种控制符,用于输出一个值的“调试形式”。 在本例中,使用 {:?}
会输出 slice
值的调试形式,这意味着在输出的序列中会包含所有元素的值,以及切片的边界。"{:#?}"是"Debug"格式化输出中的一个选项。它输出更好看的、更具可读性的调试输出。这种输出格式适用于较为复杂的数据结构。"{:?}"是"Debug"格式化输出中的另一个选项。它输出的内容与"{:#?}"的输出类似,但是在行与行之间没有缩进。由于"{:?}"没有缩进,输出的信息可能不是那么好看,但是也更加紧凑。所以,"{:#?}"和"{:?}"的主要区别在于输出的可读性和紧凑性之间的权衡。
控制符包括但不限于:
-
{}
:按照默认方式输出值。 -
{:b}
:以二进制输出值。 -
{:o}
:以八进制输出值。 -
{:x}
:以十六进制输出值。 -
{:e}
:以科学计数法输出值。
在 Rust 中,数组的数据可以存储在堆或者栈上,具体取决于数组的大小。如果数组的大小是固定的,那么数组的数据就会存储在栈上;如果数组的大小是可变的,那么数组的数据就会存储在堆上。
// 栈上
fn main() {
let a = [1, 2, 3, 4, 5];
let b: [i32; 5] = [1, 2, 3, 4, 5];
}
// 堆上
fn main() {
let c = vec![1, 2, 3, 4, 5];
}
文章评论