0 前言
是我的一点学习笔记
所有权,是Rust里一个比较特别比较有意思的概念,理解起来很容易,但对于Rust的使用有很广泛的影响,所以需要多用才能更好地掌握
原文链接:Rust程序设计语言
1 什么是所有权
1.1 引言
所有权,说到底,就是对于内存的使用方式,举个例子,简单地将,a变量他拥有一块内存的所有权,然后这个所有权可能会被移交给b变量,那么a变量就有可能会失去所有权
一个例子,在js中,a=‘abc’,b=a,那么a=1并且b=1,但是在rust中,a会失去所有权,再使用a会报错
那么为什么要所有权呢?其实所有权,是一种管理内存的方式,并且不需要开发者来亲自分配和释放内存,也不需要复杂的垃圾回收机制,只需要通过所有权进行内存管理,在编译时就基于规则做好检查,在运行时不会因为内存分配而导致问题或者减慢速度,是不是很神奇?
1.2 基本规则
- Rust中的每一个值,都有一个所有者,所有者是变量。
- 值,任何时候,有且只会有一个所有者。
- 当所有者离开作用域,值就会被丢弃。
1.3 作用域
与其他语言基本一致,所以没啥好说的。
1.4 内存分配
简单来说,有栈、堆,两种内存分配方式
栈中的所有数据都必须占用已知且固定的大小,堆则是你请求一定空间时内存分配器给你找一块足够的空位分配给你。
整型放在栈上,字符串放在堆上。
1.5 所有权与移动
直接看代码:
let x = 5;
let y = x;
println!("{}",x); // 没有问题
let s1 = String::from("hello");
let s2 = s1;
println!("{}",r1); // 编译报错
对于整型而言,具有固定大小的简单之,所以是在栈上对5进行了拷贝。
对于字符串而言,则是进行了“浅拷贝”,然后把s1的释放了,避免了不确定大小的空间的内存重新分配,提升了运行效率
所以,这种赋值方式,我们形象地叫他“移动”
1.6 所有权与克隆
但是我们有时确实需要深度拷贝一份数据进行赋值,这是我们需要使用clone
let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1 = {}, s2 = {}", s1, s2);
1.7 所有权与函数
函数涉及到入参、返回值,那不是会导致数据的转移或者拷贝么?并且还跨越了作用域,有什么办法可以方便地实现呢?
这就引出了下文的“引用与借用”。
2 引用与借用
2.1 引用
如果你了解c/cpp,那看到下面的代码你应该也不会陌生
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("The length of '{}' is {}.", s1, len);
}
fn calculate_length(s: &String) -> usize {
s.len()
}
1、注意这里的&String
,就是一个引用,使用值但不获取所有权
2、而与&
相反的,是*
,也被称为“解引用”,是不是很熟悉?
3、再看代码中的&s1
,其实就是创建了一个指向s1的引用
2.2 可变引用
创建一个引用的行为,被称为借用,“借用”这个词很形象,这意味着你从别人地方借了一本书过来,而还回去的时候也应当是这本书,不可以改变他的值,不能借来是《红楼梦》,然后阅读几页之后就变成《西游记》!
那怎么办呢,还记得之前的不可变的变量么?同样的道理!加上mut
就行。
fn main() {
let mut s = String::from("hello");
change(&mut s);
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
}
但是你可能会发现一个问题,如果有s1
与s2
,都对s
使用了可变引用,是不是他们都能改变s
的值了呢?No,这是不允许的,有且只能有一个对某一特定数据的可变引用,创建两个编译器会直接报错。
不过需要注意的是,这里的“有且只有一个”,是指在同一个作用域里的,所以你可以创建一个作用域再进行可变引用,因为这样r1
与r2
不会同时地去争抢可变引用,而是先后地获得可变引用。
fn main() {
let mut s = String::from("hello");
{
let r1 = &mut s;
} // r1 在这里离开了作用域,所以我们完全可以创建一个新的引用
let r2 = &mut s;
}
哦,当然,除了不能同时有两个可变引用之外,也不能同时有可变引用与不可变引用,不过多个不可变引用是可以的。
2.3 悬垂指针
最后,如果你了解有指针的语言,你可能知道一个叫“悬垂指针”的东西,不过不用担心,rust在编译时会发现的,这里不展开说啦。
3 切片slice
3.1 为什么要切片slice
还有一个没有所有权的数据类型slice
。
他为什么叫切片?刚才我们已经了解了“引用”,而切片则允许你引用集合中的一段连续的元素序列,而不用引用整个集合。
举个例子,用一个函数获取字符串中的第一个单词,如果字符串中没有出现空格,则认为整个字符串就是一个单词。
// 部分方法可能暂时看不懂,会在后面的章节展开
fn first_word(s: &String) -> usize { //.. 返回一个独立的usize
let bytes = s.as_bytes(); // 将字符串转化为字节数组
for (i, &item) in bytes.iter().enumerate() {
// 创建了一个iter迭代器,enumerate返回元组
if item == b' ' {
return i;
}
}
s.len() // 返回单词结尾的索引
}
fn main() {
let mut s = String::from("hello world");
let word = first_word(&s); // word 的值为 5
s.clear(); // 这清空了字符串,使其等于 ""
// word 在此处的值仍然是 5,
}
这种情况下存在一个问题,就是实际的s
与获得的word
是分离的,也就是s
改变了之后,word
依然是原来的值,这可能导致事情的管理更加复杂、代码的运行容易出错。
如果再写一个second_word
函数,会更容易出错fn second_word(s: &String) -> (usize, usize) {
,因为你需要同时跟踪一个开始索引、一个结尾索引,并且都与输入的字符串有关,也就是说有三个分离的变量需要同步维护!
因此,需要使用字符串slice。
3.2 字符串slice
他看起来是这样的,而在具体数据结构上,他其实存储了开始位置与长度,比如 let world = &s[6..11];
,world
将是一个包含指向s
索引 6 的指针和长度值 5的slice。
fn main() {
let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6..11];
// 可以省略头/尾
let slice = &s[0..2];
let slice = &s[..2];
let slice = &s[3..len];
let slice = &s[3..];
let slice = &s[0..len];
let slice = &s[..];
}
基于slice,改造一下上面的函数
fn first_word(s: &String) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
fn main() {
let mut s = String::from("hello world");
let word = first_word(&s);
s.clear(); // error!
println!("the first word is: {}", word);
}
这里rust就会正确地报错了,但为什么会报错呢?
还记得上文说的借用吗?
1、s.clear()
尝试清除一个不可变引用,这意味着clear
在尝试获取一个s
的可变引用然后再改变s
2、word
使用了&s
也就是不可变引用
3、word
的作用域一直延伸到了最后一行
这意味着在s.clear()
的时候,同时出现了s
的可变引用、不可变引用!所以编译器就报错了。
另外,补充一下,现在我们的签名是这样的:
fn first_word(s: &String) -> &str {
而既然能够获取字面量和String
,那我们也可以把签名写成这样,这样会更好:
fn first_word(s: &str) -> &str {
当然,在调用的时候,也需要传入一个slice切片。
3.3 其他slice
上文的字符串slice是针对字符串的,但也有通用的slice。
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3];
你可以对数组进行切片引用,也可以对其他集合使用这类slice,在之后降到vector的章节时会继续展开。
4 最后
这一篇学习笔记拖得有点久了,最近实在是有点忙
后面会一边继续看教程,一边直接开始用rust动手写点小项目小代码了
2024大家一起加油💪🏻