Rust 中有所有权的概念,常见的赋值、函数传参、函数返回值等场景,都可能会发生所有权的转移,而某些场景下,只是想使用一下,所以就有了引用。
简介
引用类似于指针,可以用来传递地址,进而访问保存在该地址里的变量,不过指针可能是空指针,但引用可确保这个值是有效的。
当涉及到所有权时,就引入了借用的概念,将创建一个引用的行为称为借用,如名所示,只是借用,并没有这个变量的所有权,使用完之后需要再还回去。
如果要想修改引用所指向的变量,那么就有了可变引用。
fn length(s: &String) -> usize {
s.len()
}
fn append(s: &mut String) {
s.push_str(" World!!!")
}
fn main() {
let hi = String::from("Hello");
println!("Length of '{hi}' is {}", length(&hi));
let mut hi = String::from("Hello");
append(&mut hi);
println!("{}", hi);
}
混搭
这样就涉及到了类似于 C/CPP 中与指针类似的问题,也就是 const *
* const
const *const
的区别,其对应到 Rust 的引用就是 &mut
mut &
mut &mut
的区别是什么。
&mut
可变变量的引用,变量可以修改,引用无法修改,当确定引用那个变量之后,就不能再引用其它变量了。mut &
引用可变,可以引用A
也可以引用B
,但是A
B
本身不可变。mut &mut
上面两者的结合,引用和变量都可以修改。
示例如下。
fn main() {
let data = String::from("Data0");
let info = String::from("Hello");
let refr = &data;
println!("{}", refr); // Data0
// _refr = &info; error: cannot assign twice to immutable variable
let mut refw = &data;
println!("{}", refw); // Data0
refw = &info;
println!("{}", refw); // Hello
//refw.push_str(" World!!!"); error: the data it refers to cannot be borrowed
let mut data = String::from("Hey");
let mut info = String::from("Hello");
let refw = &mut info;
refw.push_str(" World!!!");
println!("{}", refw); // Hello World!!!
let mut refrw = &mut data;
refrw.push_str(" World!!!");
println!("{}", refrw); // Hey World!!!
refrw = &mut info;
refrw.push_str(" World!!!");
println!("{}", refrw); // Hello World!!!
}
ref 关键字
除了上述的 &
操作之外,还支持 ref
来定义指针,区别在于声明变量时两者的位置不同,例如上述的定义 let refw = &mut info
等价于 let ref mut refw = info
,两者只在定义时有区别,使用时并无差异。
fn foobar1(x: &mut i32) {
*x = 91;
}
fn foobar2(ref mut x: i32) {
*x = 92;
}
fn foobar3(ref mut x: &mut i32) {
**x = 93;
}
fn main() {
let mut a: i32 = 0;
foobar1(&mut a); // 可以修改变量
println!("result = {}", a); // 91
foobar2(a); // 无法修改变量
println!("result = {}", a); // 91
foobar3(&mut a); // 可以修改,但使用比较复杂
println!("result = {}", a); // 93
}
其中的 foobar2()
函数语义是将变量复制到栈空间,而 x
变量是栈所在数据的可变引用,所以,即使能够修改,修改的也是栈上的数据,对源变量也没有影响。
另外,还有一种场景的区别。
fn main() {
let s = Some(String::from("hello"));
// 如下这种方式意味着发生了所有权转移,后面的打印会报错
// match s {
// Some(v) => println!("got value {}", v),
// None => {}
// }
// 意味着 match 只是借用了 s 变量,没有发生所有权转移
match &s {
Some(v) => println!("got value {}", v),
None => {}
}
// 通过 ref 声明 v 是个指针,同样可以避免所有权转移
match s {
Some(ref v) => println!("got value {}", v),
None => {}
}
println!("value is {}", s.unwrap());
}
解引用
与其它语言类似,通过 &
表示引用/借用,而 *
解引用,常规的引用是一个指针类型,包含了目标数据的内存地址。而且读取变量的时候,即使有多层引用,Rust 会自动进行解引用,智能指针也支持。
fn main() {
let v = 5;
let x = &v;
let y = &&v;
let z = &&&v;
println!("v={} x={} y={} z={}", v, *x, **y, ***z);
println!("v={} x={} y={} z={}", v, x, y, z);
}
最终都会输出 v=5 x=5 y=5 z=5
数据,其中的 y 就是一个常规引用,包含的是 5 所在的地址,那么通过解引用 *y
获取到了对应的值 5
,如果执行 assert_eq!(5, y)
就会报错,因为引用和数值不是一个类型。
Deref 特征
上述的解引用根很多成熟语言比较类似,不过 Rust 有自己的扩展,如果结构他实现了 Deref
特征,那么也可以通过 *
方式解引用,这也是很多智能指针实现的基础,例如 Box<T>
类型。
fn main() {
let x = Box::new(1);
println!("{}", *x + 1);
}
对应的特征在 std::ops::Deref
中定义,内容如下。
pub trait Deref {
type Target: ?Sized;
fn deref(&self) -> &Self::Target;
}
对智能指针解引用时,实际调用了 *(x.deref())
代码,如上,通过 deref()
返回变量的常规引用,然后再通过 *
对其进行解引用,最终获取到目标值。
隐式转换
用于将某种类型的引用转换为另一种类型,例如标准库中的 String
实现了如下的 Deref
特征。
impl ops::Deref for String {
type Target = str;
fn deref(&self) -> &str {
unsafe { str::from_utf8_unchecked(&self.vec) }
}
}
也就是可以将 &String
转换为 &str
类型,那么在调用的时候,会根据函数签名、是否实现目标类型的解引用接口,来判断是否进行隐式转换。
fn hello(name: &str) {
println!("Hello {}!!!", name)
}
fn main() {
let name = String::from("World");
hello(&name);
}
也就是说:
String
实现了Deref
特征,可以在需要的时候自动转换为&str
类型。- 上述的
&name
是&String
类型,传给hello()
时会自动转换为&str
类型。 - 必须使用
&name
方式来触发自动解引用,注意,仅引用类型实参才会触发自动。
而且,还可以实现连续的自动解引用,将如上代码修改为。
fn hello(name: &str) {
println!("Hello {}!!!", name)
}
fn main() {
let name = Box::new(String::from("World"));
hello(&name);
hello(&(*name)[..]);
let simple: &str = &name;
println!("{simple}");
}
这种隐式转换的好处就是代码比较简洁,但是因为不确定某个类型是否实现了 Deref
特征,导致阅读起来比较复杂。
其它
Rust 仅对 &v
的引用进行解引用操作,常见的智能指针 (例如 Box
Rc
Arc
Cow
等) 会脱壳为内部引用;而多重 &&&v
则会归一为 &v
调用。第一种比较好容易理解,而第二种对应如下代码。
impl<T: ?Sized> Deref for &T {
type Target = T;
fn deref(&self) -> &T {
*self
}
}
注意,上述的实现有点难理解,其中 &self
是 self: &Self
的简写,而这里的 Self
又是 &T
类型,所以会自动将上述的 &&&v
会依次转换为 &&v
&v
类型。
从解引用的角度看,相比 impl Deref for &T
来说 impl Deref for T
是完全没用的,因为解引用针对的是引用,而不是对象,当调用 &T
的解引用时,Rust 会看 &T
有没有实现 Deref
而不是 T
。