1、引用与借用
在之前我们将String 类型的值返回给调用函数,这样会导致这个String会被移动到函数中,这样在原来的作用域不可访问了,但是我们功能一个String值得引用,这样就不会导致这个String类型的值被移动,而传递的只是一个引用。引用更像一个指针,因为是一个地址,我们就可以基于这个地址找到改地址上存储的数据。 与指针不同,引用确保指向某个特定类型的有效值。
下面是一个引用传递的示例:
fn main() { let str = String::from("hello world!"); let len = _length(&str); println!("str is value: {}", str); println!("str length is: {}", len) } fn _length(s: &String) ->usize { s.len() }
运行结果所示所示:
根据以上代码可以看出_length方法中传递的参数为&str,所以这里传递的是str值的引用,用&符号代表引用
以下是一张对应的示意图:
根据上图也能看出s是s1的引用,引用的是s1在堆中对应类型的值。
注意:与使用
&
引用相反的操作是 解引用(dereferencing),它使用解引用运算符,*
。
变量 s
有效的作用域与函数参数的作用域一样,不过当 s
停止使用时并不丢弃引用指向的数据,因为 s
并没有所有权。当函数使用引用而不是实际值作为参数,无需返回值来交还所有权,因为就不曾拥有所有权。
我们将创建一个引用的行为称为 借用(borrowing),因为我们并没有拥有它的所有权,只是暂时借用以下。
我们可以尝试修改一下引用,把引用值改了,看下是否可以,这就类似于我借了别人的东西,然后把东西换了个样子,看看是不是可以呢?
fn main() { let str = String::from("hello world!"); let len = _length(&str); println!("str is value: {}", str); } fn _length(s: &String) { s.push_str("我把你给改了.........."); }
运行一下,看下结果:
根据提示可以s是一个引用,因此它引用的数据不能作为可变数据借用。
1.1 可变引用
允许我们修改一个借用的值,这就是 可变引用,把上面的示例改一下,如下所示:
fn main() { let mut str = String::from("hello world!"); _length(&mut str); println!("str is value: {}", str); } fn _length(s: &mut String) { s.push_str("我把你给改了.........."); }
运行代码,再看一下结果:
首先定义str必须时可变的,在方法中传递参数,也要指定引用为可变引用,因为引用指向的是被引用的地址,所以就会改变原有的值。
注意:可变引用有一个很大的限制:如果你有一个对该变量的可变引用,你就不能再创建对该变量的引用。
看以下示例:
fn main() { let mut str = String::from("hello world!"); let a = &mut str; let b = &mut str; println!("a {}, b{}", a, b) }
根据错误提示,可以知道同一时间不能多次借用str作为可变变量。Rust这样限制是因为可以在编译时就避免数据竞争。数据竞争(data race)类似于竞态条件,它可由这三个行为造成:
- 两个或更多指针同时访问同一数据。
- 至少有一个指针被用来写入数据。
- 没有同步数据访问的机制。
再看下以下示例:
fn main() { let mut str = String::from("hello world!"); let c = &mut str; let b = &str; println!("b{} {}", b, c) }
也会报错,借用和可变借用不能同时被使用。
再看下一个示例:
fn main() { let mut str = String::from("hello world!"); let c = &mut str; println!("{}", c); let b = &str; println!("{}", b) }
可以看到这次是可以打印处结果的,在第一次打印的时候,变量的作用域也就结束了,因而在下次进行赋值时可以的。
1.2 悬垂引用
在具有指针的语言中,很容易通过释放内存时保留指向它的指针而错误地生成一个 悬垂指针(dangling pointer),所谓悬垂指针是其指向的内存可能已经被分配给其它持有者。相比之下,在 Rust 中编译器确保引用永远也不会变成悬垂状态:当你拥有一些数据的引用,编译器确保数据不会在其引用之前离开作用域。
下面时一个悬垂应用的示例:
fn main() { dp(); } fn dp() -> &String { // 返回字符串的引用 let str = String::from("hello world!"); // 创建一个字符串 &str // 返回字符串的引用 } // str 的作用域结束 // 方法返回的时字符串的引用,而字符串离开作用与,被释放,然在此返回该字符串的引用, // 就会导致返回的结果不是预期的结果,在Rust中是不让这样操作的。
直接运行,会报如下错误:
根据报错可知,此函数的返回类型包含借用值,但没有可供借用的值,在返回类型引用处,提示: 错误的声明周期修饰符。
我们改以下返回字符串本身,看一下结果怎么样?
fn main() { println!("value is {}", dp()) } fn dp() -> String { let str = String::from("hello world!"); return str }
运行一下看看:
发现对应的值给打印出来,所有权交出去了,所以,可打印出对应的值。
1.3 引用的规则
根据之前的结果,我们可以总结出以下两点:
- 在任意给定时间,要么 只能有一个可变引用,要么 只能有多个不可变引用。
- 引用必须总是有效的。
2、slice 类型
slice 允许你引用集合中一段连续的元素序列,而不用引用整个集合。slice 是一类引用,所以它没有所有权。
以下有个slice示例:
fn main() { let s = String::from("hello world"); let hello = &s[0..5]; let world = &s[6..11]; println!("{}---{}", hello, world) }
运行以下,看下结果如何:
根据以上结果,可以知道hello变量从字符串(hello world)中进行截取的,开始的位置为0,长度为5,所以打印的结果为hello,而world变量是从开始索引6开始,11结束,11-5=6,那么它的长度也是5,所以打印的结果为world.
Slice 的主要结果包括2部分:
- 第一部分,是指针,指向数据开始的位置
- 第二部分,是长度,就是元素结束减去开始位置的值
以下是一个示意图,能够更加清楚知道slice与字符串的关系:
其他写法,例如取前5个字符:
fn main() { let s = String::from("hello world"); let hello = &s[0..5]; let hello1 = &s[..5]; // hello 和 hello1 是等价的 println!("{}---{}", hello, hello1) }
例如取最后5个字符:
fn main() { let s = String::from("hello world"); let world = &s[6..]; let world1 = &s[6..]; println!("{}---{}", world, world1) }
取整个长度的切片:
fn main() { let s = String::from("hello world"); let world = &s[..]; let world1 = &s[..]; println!("{}---{}", world, world1) }
注意:字符串 slice range 的索引必须位于有效的字符边界内,如果尝试从超过边界访问超出索引范围将导致panic错误。
2.1 字符串字面量其实就是一个slice
一个示例如下所示:
这里 s1
的类型是 &str
:world 的类型也是&str,所以s1它是一个指向二进制程序特定位置的 slice。这也就是为什么字符串字面值是不可变的;&str
是一个不可变引用。
2.2 总结
所有权、借用和 slice 这些概念让 Rust 程序在编译时确保内存安全。Rust 语言提供了跟其他系统编程语言相同的方式来控制你使用的内存,但拥有数据所有者在离开作用域后自动清除其数据的功能意味着你无须额外编写和调试相关的控制代码。