网站建设收费标准效果,wordpress数据表格,友链交易,选择做华为网站的目的和意义文章目录 什么是所有权Stack vs Heap所有权规则变量作用域String类型内存与分配所有权与函数 引用与借用可变引用悬垂引用引用的规则 切片字符串切片其他类型的切片 什么是所有权 什么是所有权 所有程序在运行时都必须管理其使用计算机内存的方式#xff1a;
一些语言中具有垃… 文章目录 什么是所有权Stack vs Heap所有权规则变量作用域String类型内存与分配所有权与函数 引用与借用可变引用悬垂引用引用的规则 切片字符串切片其他类型的切片 什么是所有权 什么是所有权 所有程序在运行时都必须管理其使用计算机内存的方式
一些语言中具有垃圾回收机制在程序运行时有规律地寻找不再使用的内存比如C#和Java。在另一些语言中程序员必须自行分配和释放内存比如C/C。
而Rust则是通过所有权系统管理内存
所有权是Rust最独特的特性它让Rust无需GC就可以保证内存安全这也是Rust的核心特性。通过所有权系统管理内存编译器在编译时会根据一系列的规则进行检查如果违反了所有权规定则程序不能通过编译。在程序运行时所有权系统不会减慢程序的运行速度因为所有权规则的检查是在编译时进行的。
Stack vs Heap
在很多语言中程序员不需要经常考虑到堆和栈但在Rust这样的系统编程语言中一个值存储在heap上还是stack上会很大程度上影响语言的行为所以这里先对堆和栈进行简单介绍。 分配内存 栈stack
stack按值的接收顺序来存储按相反的顺序将它们移除即后进先出LIFO。所有存储在stack上的数据必须拥有已知的固定大小添加数据叫做入栈移除数据叫做出栈。将数据存放在stack时不需要寻找用来存储数据的空间因为那个位置永远在stack的顶端。
堆heap
在编译时大小未知的数据或运行时大小可能发生变化的数据必须存放在heap上。当把数据放入heap时需要先在heap上分配对应大小的空间即操作系统在heap中找到一块足够大的空间把它标记为在用并将该空间的地址返回后续访问heap上的数据时需要通过指针来定位。 访问数据 访问heap中的数据比访问stack中的数据慢因为需要通过指针才能找到heap中的数据属于间接访问。对于现代的处理器来说由于缓存的缘故如果指令在内存中的跳转的次数越少那么速度就越快。stack中数据存放的距离比较近而heap中数据存放的距离比较远因此访问heap中的数据比访问stack中的数据慢。 所有权存在的原因 所有权存在的原因就是为了管理存放在heap上的数据
跟踪代码的哪些部分正在使用heap上的哪些数据。最小化heap上的重复数据量。清理heap上未使用的数据以避免内存泄露。
所有权规则 所有权规则 所有权的规则如下
Rust中的每一个值都有一个对应的变量作为它的所有者。在同一时间内每个值有且只有一个所有者。当所有者离开自己的作用域时它持有的值就会被释放掉。
变量作用域 变量作用域 作用域scope指的是程序中一个项在程序中的有效范围。
在下面的代码中变量s从第三行声明开始变得可用在第五行代码块结束时离开作用域变得不可用。如下
fn main() {//s不可用let s hello; //s可用//可以对s进行相关操作
} //s作用域到此结束s不再可用String类型 String类型 为了后续讲解Rust的所有权我们需要借助一个管理的数据存储在heap上的类型这里选择String类型。
Rust中基础的标量类型的数据是存储在stack上的而String类型比这些类型更加复杂它管理的数据是存储在heap上。String类型管理的数据存储在heap上因此String类型能够存储在编译时未知大小的文本即String类型是可变的。Rust中有两种字符串类型一种是字符串字面值它是不可变的另一种就是String类型其管理的字符串是可变的。
String类型由三部分组成
ptr指向存放字符串内容的指针。len表示字符串的长度。capacity表示字符串的容量。
String类型的这三部分数据存储在stack上而String管理的字符串则存储在heap上。如下 创建String字符串 创建String字符串可以使用from函数该函数可以基于字符串字面值来创建String字符串。如下
fn main() {let mut s String::from(Hello);s.push_str( String);println!({}, s); //Hello String
}说明一下
代码中的::表示from是String类型的命名空间下的函数。String类型的push_str方法可以将指定的字符串插入到String字符串的后面。
内存与分配 内存与分配 对于字符串字面值来说在编译时就知道它的内容了其文本内容会直接被硬编码到最终的可执行文件中因此访问字符串字面值快速且高效。而String类型为了支持可变性需要在heap上分配内存来保存编译时未知的文本内容其必须在运行时向内存分配器请求内存当我们处理完String时再将内存返回给分配器。
注在Rust中当某个值离开作用域时会自动调用drop函数释放内存。变量与数据交互的方式移动Move 在Rust中多个变量可以采取不同的方式与同一数据进行交互。如下
fn main() {let x 10;let y x;println!(x {}, x); //x 10println!(y {}, y); //y 10
}说明一下
代码中先将整数字面值10绑定到了变量x接着生成了变量x的拷贝并将其绑定到变量y。因为整数是已知固定大小的简单值因此x和y都被放入到了栈中在赋值后两个变量都有效。
如果将代码中的整数换成String那么运行程序将会产生报错。如下
fn main() {let x String::from(hello);let y x;println!(x {}, x); //errorprintln!(y {}, y);
}报错的原因就是我们借用了已经被移动的值x。如下 现在我们来分析一下代码刚开始声明变量x的时候整体布局如下 当把变量x赋值给变量y时String的数据被拷贝了一份但拷贝的仅仅是stack上的String元数据而并没有拷贝指针所指向的heap上的数据。如下 当变量离开作用域时Rust会自动调用drop函数释放内存为了避免这种情况下heap上的数据被二次释放因此Rust会让赋值后的变量x失效此时当x离开作用域时就不会释放内存。如下 这就是为什么在赋值后访问变量x就会产生报错的原因因为此时变量x已经失效了。
说明一下
stack上的拷贝可以视为浅拷贝heap上的拷贝可以视为深拷贝。由于深拷贝的成本比较高因此Rust不会自动进行数据的深拷贝。 变量与数据交互的方式克隆Clone 如果确实需要对String的heap上的数据进行拷贝那么可以使用String的clone方法。如下
fn main() {let x String::from(hello);let y x.clone(); //深拷贝println!(x {}, x); //x helloprintln!(y {}, y); //y hello
}拷贝后变量x和变量y都是有效的因为String的clone方法会将stack和heap上的数据都进行拷贝。如下 stack上的数据拷贝Copy Copy trait可以用在类似整型这样存储在栈上的类型如果一个类型实现了Copy trait那么旧的变量在赋值给其他变量后仍然可用。任何简单标量的组合类型都可以实现Copy trait任何不需要分配内存或某种形式资源的类型也都可以实现Copy trait。所有整数类型、浮点类型、布尔类型、字符类型都实现了Copy此外如果元组中所有字段都实现了Copy那么这个元组也是可Copy的比如(i32, i32)是可Copy的而(i32, String)是不可Copy的。
说明一下
如果一个类型或该类型的一部分实现了Drop trait那么Rust不允许它再实现Copy trait。如果一个类型要实现Copy trait那么该类型也必须实现Clone trait。String赋值后变量会失效就是因为String没有实现Copy trait在赋值时会发生移动。
所有权与函数 所有权与函数 将值传递给函数和给变量赋值的原理类似
对于没有实现Copy trait类型的变量来说将值传递给函数时会发生移动调用函数后变量失效。对于实现了Copy trait类型的变量来说将值传递给函数时会发生拷贝调用函数后变量仍然有效。
例如下面代码中变量s传入函数时将发生移动后续不再有效而变量x传入函数时将发生拷贝后续仍然有效。如下
fn main() {let s String::from(hello world);take_ownership(s); //发生移动//println!(s {}, s); //errorlet x 10;makes_copy(x); //发生拷贝println!(x {}, x); //x 10;
}fn take_ownership(some_string: String) {println!({}, some_string); //hello world
}fn makes_copy(some_number: i32) {println!({}, some_number); //10
}返回值与作用域 函数在返回值的过程中同样会发生所有权的转移。如下
fn main() {let s1 gives_ownership();let s2 String::from(hello);let s3 takes_and_gives_back(s2);
}fn gives_ownership() - String {let some_string String::from(hello);some_string
}fn takes_and_gives_back(a_string: String) - String {a_string
}代码说明
gives_ownership函数的作用是创建了一个String并将其所有权返回。takes_and_gives_back函数的所用是取得了一个String的所有权然后再将其所有权返回。
引用与借用 引用与借用 对于String类型来说String就是String类型的引用我们将创建一个引用的行为称为借用。一个类型的引用不会取得该类型变量的所有权因此当引用离开作用域时不会释放对应的空间。
例如下面代码中的calculate_length函数的参数类型是String该函数返回传入String的长度但不获取其所有权函数调用后传入的String变量仍然有效。如下
fn main() {let s1 String::from(hello world);let len calculate_length(s1);println!({}的长度是{}, s1, len); //hello world的长度是11
}fn calculate_length(s: String) - usize {s.len()
}实际calculate_length函数的参数s就是一个指针它指向了传入的实参s1。如下 说明一下
与引用相反的操作是解引用解引用运算符是*。
可变引用 可变引用 引用和变量一样默认也是不可变的要让引用变得可变同样需要使用mut关键字。如下
fn main() {let mut s1 String::from(hello world);let len calculate_length(mut s1);println!({}的长度是{}, s1, len); //hello world!!!的长度是14
}fn calculate_length(s: mut String) - usize {s.push_str(!!!); //修改了引用的变量s.len()
}但可变引用有一个重要的限制就是在特定作用域内一个变量只能有一个可变引用否则会产生报错。如下
fn main() {let mut s String::from(hello world);let s1 mut s;let s2 mut s; //errorprintln!(s1 {}, s2 {}, s1, s2);
}Rust这样做可以在编译时就防止数据竞争但可以通过创建新的作用域来允许非同时的创建多个可变引用因为只要保证同一个作用域下一个变量只有一个可变引用即可。如下
fn main() {let mut s String::from(hello world);{let s1 mut s;}let s2 mut s;
}可变引用的其他限制 Rust中不允许一个变量同时拥有可变引用和不可变引用否则会产生报错。如下
fn main() {let mut s String::from(hello world);let r1 s;let r2 s;let s1 mut s; //errorprintln!({} {} {}, r1, r2, s1);
}原因 不可变引用的要求其引用的值不能发生改变而可变引用却可以改变其引用的值因此一个变量同时拥有可变引用和不可变引用就是的不可变引用的作用失效了但一个变量同时拥有多个不可变引用是可以的。
悬垂引用 悬垂引用Dangling References 悬垂引用指的是一个指针引用了内存中的某个地址但这块内存可能已经释放了。如下
fn main() {let r dangle();
}fn dangle() - String {let s String::from(hello world);s //悬垂引用
}在Rust中编译器确保引用永远不会变成悬垂状态因为编译器会确保数据不会在其引用之前离开作用域因此上述代码会编译报错。如下 说明一下 报错内容说缺少一个生命周期说明符生命周期相关的内容会在后续博客中讲解。
引用的规则 引用的规则 引用的规则如下
在任何时刻一个变量只能有一个可变引用或任意数量的不可变引用。引用必须一直有效。
切片
字符串切片 字符串切片 除了引用之外Rust还有另一种不持有所有权的数据类型叫做切片slice。字符串切片就是指向字符串中一部分值的引用切片形式为字符串变量名[开始索引..结束索引]。字符串切片中的开始索引指的是切片起始位置的索引值结束索引指的是切片终止位置的下一个索引值。如果切片的起始位置是0那么开始索引可以省略如果切片的终止位置是字符串的长度那么结束索引可以省略。字符串切片的类型是str字符串切片是不可变的。
例如下面分别创建了字符串hello world的hello的切片和world的切片。如下
fn main() {let s String::from(hello world);let hello s[0..5];let world s[6..11];println!(hello {}, hello); //hello helloprintln!(world {}, world); //world world
}切片中包含一个指针和一个长度比如上述的world切片其指针指向字符串索引为6的位置其长度就是5。如下 切片在Rust中是非常有用的比如获取字符串中的第一个单词那么借助字符串切片可以编写出如下代码
fn main() {let s String::from(hello world);let word first_word(s);//s.clear(); //error: s已经存在一个不可变引用println!(word {}, word); //word hello
}fn first_word(s: String) - str {let bytes s.as_bytes();for (i, item) in bytes.iter().enumerate() {if item b {return s[..i];}}s[..]
}说明一下
如果不使用字符串切片也可以通过返回字符串中第一个空格的索引来间接表示字符串中第一个单词的位置但此时这个索引是独立于这个字符串存在的当字符串中的内容被清除后这个索引就没有意义了。而切片是不可变的因此如果一个字符串存在一个切片那么在这个切片没有离开作用域之前这个字符串中的内容是无法被修改的因为Rust不允许一个变量同时拥有可变引用和不可变引用否则会产生报错。as_bytes方法的作用是将String转化为字节数组以方便遍历String的每一个字节来与空格进行比较。iter方法的作用是在字节数组上创建一个迭代器它将会返回字节数组中的每一个元素而enumerate方法的作用是对iter的结果进行包装将这些元素作为元组的一部分来返回。enumerate返回的元组中第一个元素是索引第二个元素是集合中元素的引用。在for循环中通过模式对enumerate返回的元组进行解构由于元组中第二个元素是集合中元素的引用因此item需要使用。
注意
字符串切片的范围索引必须发生在有效的UTF-8字符边界内。如果尝试从一个多字节的字符中创建字符串切片程序会报错并退出。 字符串字面值就是切片 字符串字面值的类型实际上就是字符串切片str这就是为什么字符串字面值不可变的原因因为字符串切片str就是不可变的。如下
fn main() {let s hello world; //s的类型是str
}将字符串切片作为参数 如果要将字符串切片作为函数的参数那么最好将函数的参数类型定义为str而不是String这样就能同时接收String和str的参数了能够使我们的API更加通用且不会损失任何功能。如下
fn main() {let my_string String::from(hello world);let word first_word(my_string); //接收Stringlet my_string_literal hello world;let word first_word(my_string_literal); //接收str
}fn first_word(s: str) - str {let bytes s.as_bytes();for (i, item) in bytes.iter().enumerate() {if item b {return s[..i];}}s[..]
}说明一下
String等价于整个String的切片因此可以用str接收而字符串字面值的类型本来就是str。
其他类型的切片 其他类型的切片 与字符串切片类似其他类型也可以有切片比如对于下面代码中的数组来说其切片类型就是[i32]。如下
fn main() {let a [1, 2, 3, 4, 5];let slice a[1..3]; //[i32]类型
}