书接上回1. 我在外包学rust——所有权。
话说上一章节主要介绍了 rust 的所有权这一概念。
- 我们知道所有权是 rust 用来进行内存管理的,且主要是针对存储在堆(heap)上的数据进行的。
- 每个值有且只有一个变量作为它的所有者,当变量超出作用域(scope)时,rust 会调用 drop 函数,回收值所占用的堆内存空间。
- 将变量赋值给新的变量时,会发生所有权转移(move),所谓所有权转移,其实本质上发生了两件事,一个是浅拷贝,一个是旧的变量失效。
- 对于标量类型,rust 对其实现了 copy trait,将变量赋值给新的变量时,会对值进行拷贝。
以上差不多就是上一章的内容了。
接下来继续所有权的话题,首先来看看所有权在调用函数时会发生生么。
所有权与函数
函数传参
语义上,将值传递给函数和把值赋给变量是类似的。即将值传递给函数也会发生移动(move)或复制(copy)。
fn main(){
let s = String::from("hello");
print_string(s);
println!("{}",s); // 报错
}
fn print_string(some_string:String){
println!("{}",some_string);
}
上面的代码中,s 传递到print_string()函数中,发生了所有权转移,print_string()函数执行结束,s 离开函数作用域,被 drop。第 4 行再打印 s 就会报错。
fn main(){
let n = 11;
print_string( n);
println!("{}",n); // 11
}
fn print_string(some_num:i32){
println!("{}",some_num);
}
而对于i32这种标量类型,由于它是存储在栈上的,实现了 copy trait,因此不会报错。
返回值与作用域
函数在返回值的过程中同样也会发生所有权转移。
fn main(){
let mut s = String::from("hello");
s = print_string(s);
println!("{}",s); // "hello"
}
fn print_string(some_string:String)-> String{
println!("{}",some_string);
some_string
}
上面的代码中,print_string将some_string返回,因此函数结束后,就将some_string的所有权移交出去了。但是每次调用函数所有权转来转去,如果函数外部还希望拥有值的所有权,就不得不将变量返回,就很冗余,不够灵活,因此 rust 提供了引用与借用的方式使代码更加灵活。
一个变量的所有权总是遵循同样的模式:
- 把一个值赋给其他变量时就会发生转移;
- 当一个包含堆(heap)数据的变量离开作用域时,它的值就会被 drop 函数清理,除非数据的所有权移动到另一个变量上。
引用与借用
引用
引用,允许引用某些值,但不取得其所有权,用&来表示。
解引用,用*来表示。
fn main(){
let s = String:from("hello");
let l = get_string_len(&s);
println("The length of {} is {}",s,l);
}
fn get_string_len(s:&String) -> usize{
s.len()
}
借用
借用就是把引用作为函数参数的行为,这样函数内不过去该数据的所有权。如上面的代码将&s,即s的引用作为参数传给函数get_string_len的行为。
可变引用
使用mut关键字,将引用标记为可变,则可以更改原数据。
fn main(){
let mut s = String:from("hello");
let l = get_string_len(&mut s);
println("The length of {} is {}",s,l);
}
fn get_string_len(s:&mut String) -> usize{
s.push_str(",world");
s.len()
}
限制
-
在特定作用域内,对某一块数据,只能有一个可变引用。这样做的好处:在编译时防止数据竞争。
数据竞争,满足以下三个条件时会发生数据竞争:
- 两个以上指针访问同一个数据;
- 至少有一个指针用于写入数据;
- 没有使用任何机制来同步对数据的访问。
-
不可同时拥有一个可变引用和一个不可变引用,多个不可变引用是允许的。
-
但是可以通过创建新的作用域来允许非同时的创建多个可变引用。
fn main() { let mut s = String::from("hello world"); let a = &mut s; println!("{}",a); { let b = &mut s; println!("{}",b); } }上面的代码是没问题的。但是,下面的代码就会有问题,仅仅是将 a 的调用放到了 b 的作用域之后:
fn main() { let mut s = String::from("hello world"); let a = &mut s; { let b = &mut s; println!("{}",b); } println!("{}",a); }error[E0499]: cannot borrow `s` as mutable more than once at a time --> src/main.rs:6:17 | 4 | let a = &mut s; | ------ first mutable borrow occurs here 5 | { 6 | let b = &mut s; | ^^^^^^ second mutable borrow occurs here ... 9 | println!("{}",a); | - first borrow later used hererust 的编译器真的严格啊,在 4-7 行的作用域之后再调用 a,编译器就会教你做人了,因为 b 和 a 都是 s 的可变引用,那么 4-7 行里很有可能对 s 指向的数据做出了修改,那么之后获取到的 a 很可能就是错的,会导致程序错误的。所以 rust 就干脆不允许你这么干了。
悬空引用(Dangling References)
悬空指针:一个指针引用了内存的某个地址,而这块内存可能已经释放并分配给其他人使用了。 在 Rust 中,编译器可以保证引用永远都不是悬空引用。 如果你引用了某些数据,编译器将保证在引用离开作用域之前数据不会离开作用域。
fn main(){
let r = dangle();
}
fn dangle()→6 String{
let s = String::from("hello");
&s
}
上面这种会造成悬空指针的代码,rust 就直接报错了。
引用的规则
- 在任何情况下,只能满足下列条件之一:
- 一个可变的引用;
- 任意数量的不可变引用。
- 引用必须一直有效。
引用规则大汇总
最近又发现了新的学习资料Rust 语言从入门到实战;亲测不错,讲的深入浅出,九浅一深的。。。 下面就是这门课程里对引用借用规则的总结:
- 引用和借用是一体两面,你把东西借给别人用,也就是别人持有了对你这个东西的引用。
- 所有权型变量的作用域是从它定义时开始到所属那层花括号结束。
- 引用型变量的作用域是从它定义起到它最后一次使用时结束。
- 引用(不可变引用和可变引用)型变量的作用域不会长于所有权变量的作用域。这是肯定的,不然就会出现悬垂引用,这是典型的内存安全问题。
- 一个所有权型变量的不可变引用可以同时存在多个,可以复制多份。
- 一个所有权型变量的可变引用与不可变引用的作用域不能交叠,也可以说不能同时存在。
- 某个时刻对某个所有权型变量只能存在一个可变引用,不能有超过一个可变借用同时存在,也可以说,对同一个所有权型变量的可变借用之间的作用域不能交叠。
- 在有借用存在的情况下,不能通过原所有权型变量对值进行更新。当借用完成后(借用的作用域结束后),物归原主,又可以使用所有权型变量对值做更新操作了。
- 可变引用的再赋值,会执行移动操作。赋值后,原来的那个可变引用变量就不能用了。这有点类似于所有权的转移.因此一个所有权型变量的可变引用也具有所有权特征,它可以被理解为那个所有权变量的独家代理,具有排它性。
多级引用规则
- 对于多级可变引用,要利用可变引用去修改目标资源的值的时候,需要做正确的多级解引用操作,比如例子中的 **c,做了两级解引用。
- 只有全是多级可变引用的情况下,才能修改到目标资源的值。
- 对于多级引用(包含可变和不可变),打印语句中,可以自动为我们解引用正确的层数,直到访问到目标资源的值,这很符合人的直觉和业务的需求。
切片 slice
切片是 rust 中另一种不持有所有权的数据类型。
引子——一个例子
以一道题作为引子:
- 它接收字符串作为参数
- 返回它在这个字符串里找到的第一个单词
- 如果函数没找到任何空格,那么整个字符串就被返回
fn main(){
let mut s = String::from("Hello world");
let word_index = first_word(&s);
s.clear();
println!("{}",word_index);
}
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()
}
上面的代码是实现了功能的。不过,其实word_index的值是和 s 强关联的。如果在第 4 行调用s.clear(),word_index的值仍然是 5。也就是说,first_word函数的有效性脱离了字符串s的上下文,要保证s和word_index的同步性,就需要很繁琐的工作了。
而字符串切片就是为了解决这一类问题而设计的。
字符串切片就是指向字符串中一部分内容的引用。&字符串变量[开始索引..结束索引]左闭右开区间。
fn main(){
let s = String::from("Hello world");
let hello = &s[0..5];
let world = &s[6::11];
// 其他写法
// let hello = &[..5];
// let world = &[6..];
// let world = &s[6..s.len()]
// let hello_world = [0..s.len()];
// let hello_world = [..];
}
变量world包含了一个指向s索引 6 处的指针和一个值为 5 的 len。
注意:字符串切片的范围索引必须发生在有效的UTF-8字符边界内。 如果尝试从一个多字节的字符中创建字符串切片,程序会报错并退出。
用字符串切片重写例子
fn main(){
let mut s = String::from("Hello world");
let word_index = first_word(&s);
s.clear(); // 报错
println!("{}",word_index);
}
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[..]
}
这样第四行调用s.clear()就会报错:
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
--> src/main.rs:4:5
|
3 | let word_index = first_word(&s);
| -- immutable borrow occurs here
4 | s.clear(); // 报错
| ^^^^^^^^^ mutable borrow occurs here
5 | println!("{}",word_index);
| ---------- immutable borrow later used here
For more information about this error, try `rustc --explain E0502`.
字符串字面值是切片
字符串字面值直接被存储在二进制程序中。变量s的类型是&sr,它是一个指向二进制程序特定位置的切片。&s是不可变引用,所以字符串字面值也是不可变的
将字符串切片作为参数传递
之前的函数签名:fn first word(s:&String) -> &str是将字符串的引用作为参数传递的。
有经验的Rust开发者会采用&str作为参数类型,因为这样就可以同时接收String
和&str类型的参数了,即fn first word(s:&str) -> &str:
- 使用字符串切片,直接调用该函数
- 使用String,可以创建一个完整的String切片来调用该函数
定义函数时使用字符串切片来代替字符串引用会使我们的API更加通用,且不会损失任何功能。
fn main(){
let my_string = String:from("Hello world"); // String 类型
let word_index = first_world(&my_string[..]);
let my_string_literal = "hello world"; // &str 类型
let word_index = first_world(my_string_literal);
}
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[..]
}
其他类型的切片
如数组:
fn main(){
let a = [1,2,3,4,5];
let s = &a[1..3];
}
与字符串切片的含义一样,s包含一个指向a索引为 1 处的指针和一个值为 2 的len。
下图是根据“圣经”字符串与切片章节整理的图谱。字符串与切片
结尾
今天的摸鱼就到这吧。各位大帅,我继续去看视频,再探再报!