【Rust自学】4.4. 引用与借用
4.4.0 写在正文之前
这一节的内容其实就相当于C++的智能指针移动语义在编译器层面做了一些约束。Rust中引用的写法通过编译器的约束写成了C++中最理想、最规范的指针写法。所以学过C++的人对这一章肯定会非常熟悉。
喜欢的话别忘了点赞、收藏加关注哦(加关注即可阅读全文),对接下来的教程有兴趣的可以关注专栏。谢谢喵!(=・ω・=)
4.4.1. 引用
引用让函数使用某个值而不获得其所有权,声明时在类型前加上&
即代表引用。例如String
的引用就是&String
。如果学过C++的话,C++中的解引用符号是*
,Rust中也是一样的。
学了引用之后,就可以把上一篇文章最后的示例代码给简化
这是先前的代码:
fn main(){let s1 = String::from("hello");let (s2, len) = calculate_length(s1);println!("The length of '{}' is {}", s2, len);
}fn calculate_length(s:String) -> (String, uszie) {let length = s.len();(s, length)
}
这是修改后的代码:
fn main(){let s1 = String::from("hello");let length = calculate_length(&s1);println!("The length of '{}' is {}", s1, length);
}fn calculate_length(s:&String) -> usize {s.len()
}
对比两者,后者中数据的指针被传入函数calculate_length
供其操作,而数据所有权依然在变量s1
上。不需要返回元组,也不需要再声明一个变量s2
,更加简洁。
函数calculate_length
的参数s
实际上是一个指针,指向s
所在栈内存位置(不会直接指向堆内存中的数据)。这个指针在走出作用域时,Rust并不会消除其指向的数据(因为s
没有所有权),只会弹出栈上所存储的指针信息,也就是释放下图中的最左侧的部分所占的内存。
这种以引用作为函数的参数叫做借用
4.4.2. 借用的特性
借用的内容是不能被修改的,除非是可变引用
以房产为例:你把自己有房产权的房子租给别人就是借用,租户只能住不能乱装修,这就是借用的内容不能被修改的特性;如果你允许租客装修,这就是可变引用。
以这个代码为例:
fn main(){let s1 = String::from("hello");let length = calculate_length(&s1);println!("The length of '{}' is {}", s1, length);
}fn calculate_length(s:&String) -> usize {s.push_str(", world");s.len()
}
在编译时这个代码会报错:
error[E0596]: cannot borrow `*s` as mutable, as it is behind a `&` reference
报错的原因在于s.push_str(", world");
这一行:引用默认是不可变的,但这一行修改了其数据内容。
引用跟普通的变量声明一样,默认不可变,但加上mut
关键字后就可变了:
fn main(){let mut s1 = String::from("hello");let length = calculate_length(&mut s1);println!("The length of '{}' is {}", s1, length);
}fn calculate_length(s:&mut String) -> usize {s.push_str(", world");s.len()
}
这样写就不会报错了(但记得在声明s1
时把s1
声明为可变变量)
这种可以修改数据内容的引用就叫做可变引用
4.4.3. 可变引用的限制
可变引用有两个非常重要的限制,其一是:在特定作用域内,对某一块数据,只能有一个可变的引用。
以这个代码为例:
fn main() {let mut s = String::from("hello");let s1 = &mut s;let s2 = &mut s;
}
因为s1
和s2
都是指向s
的可变引用,且在同一个作用域内,所以在编译时会报错:
error[E0499]: cannot borrow `s` as mutable more than once at a time
这么做的目的是防止数据竞争,以下三种条件同时满足时会发生数据竞争:
- 两个或多个指针同时访问同一个数据
- 至少有一个指针用于写入数据
- 没有使用任何机制来同步对数据的访问
在报错信息中提及了at a time
,意思为同时(也就是在同一个作用域内)。所以说,只要不同时,也就是两个可变引用在不同的作用域指向同一块数据是可以的。下面的代码就体现了这一点:
fn main() {let mut s = String::from("hello");{let s1 = &mut s;}let s2 = &mut s;
}
s1
和s2
作用域不相同,所以指向同一块数据是允许的。
可变引用的第二个重要限制是:不可以同时拥有一个可变引用和一个不变的引用。 因为可变引用存在的目的是修改数据内容,不变的引用存在的作用就是为了保持数据内容不变,如果两者同时存在,可变引用修改值之后,不可变引用的作用就失效了。
fn main() {let mut s = String::from("hello");let s1 = &mut s;let s2 = &s;
}
因为s1
是可变引用,s2
是不可变引用,两者出现在同一个作用域指向同一块数据,所以编译器会报错:
error[E0502]: cannot borrow `s` as mutable because it also borrowed as immutable
当然,多个不可变的引用是可以同时出现的。
总结:多个读(不可变引用)是可以同时存在的,多个写(可变引用)可以存在但不能同时,多个写和同时读写是不允许的。
4.4.4. 悬空引用(Dangling References)
在使用指针时非常容易引起叫做悬空指针(Dangling Pointer) 的错误,其定义为:一个指针引用了内存中的某个地址,而这块内存可能已经释放并分配给其他人使用了。
如果你引用了某些数据,Rust编译器保证在引用离开作用域前数据不会离开作用域。 这是Rust保证悬空引用永远不会出现的做法。
以这个代码为例:
fn main() {let r = dangle();
}fn dangle() -> &String {let s = String::from("hello");&s
}
- 创建了一个局部变量 s:
变量s
是一个String
,它被分配在栈上,但其底层数据存储在堆上。 - 返回对
s
的引用:
函数最后通过&s
返回了s
的引用。 - s 的作用域结束:
在函数dangle
返回后,变量s
离开了作用域,根据Rust所有权规则,s
的内存被自动释放,&s
所指向的内存数据已不再存储s
的数据,返回的引用指向的是已经被释放的内存地址,变成了悬空引用(Dangling Pointer)。
Rust的编译器会检查到这一点,在编译时会报错。
4.4.5. 引用的规则
- 在任何给定的时刻,只能满足下列条件之一:
- 一个可变的引用
- 任意数量不可变的引用
- 引用必须一直有效