前端同学的rust入门(四)--所有权和借用

所有权

什么是所有权?字面意思就是,这个这个数据是属于谁的。 比如 let a = 1213 那么 数值 1213 的所有权就属于变量a 。 在rust中,所有权有几个特点:

  1. 一个值只能有一个所有者,或者从内存的角度来讲,每一块内存只能拥有一个所有者。
  2. 当所有者离开作用时,这个值会被销毁,这片内存会被释放。

作用域

rust的作用域概念并不是JS中的作用域,而是与ES6的块作用域相似,有着非常明确的作用域界限,而不是像JS一样会声明提前。

css 复制代码
  // 这是一个作用域a
    let a = 3;
    {
     //这是一个新的作用域b
        let b = 3;    
    }             //b的作用域结束,此时b不能再使用,同时数值3也被销毁
   

前面的章节,介绍了基础类型的变量,基础类型的变量与借用的关系不大。复合类型则是所有权和借用的核心。 我们以一个简单的复合类型来举例String类型。 需要注意,在rust中,String类型并不是字符串

rust 复制代码
let a ='hello world' //这是一个字符串,它在rust中表示的类型是&str
let b  = String::from("hello"); //这是一个String类型的值,它是一个复合对象

这点有点像TS中的string和String的区别。

转移所有权

我们先用一个基础类型来解释所有权的转移:

ini 复制代码
let a = 5;
let b = a;

这段代码将 5 绑定到变量 a; 接着拷贝 a 的值赋给 b,最终 ab 都等于 5。 此时内存中存在了两个5,他们的所有者分别是a和b,这样依然满足我们说的 '每个值都只有一个所有者'。 有人可能会疑问,这样是否消耗性能。当然不,因为他们都在栈上,只有4个字节。如果不复制实际的数字5,而去复制a的指针,所耗费的性能是一样的。

然后再来看一段代码:

ini 复制代码
let a = String::from("hello world");
let b = a;

这是一个类型的代码,发生的事情也是类似的,只不过String是复合类型的数值,是放在堆内存的。 而变量a,对应的数值实际上是String类型实例存放的指针。 而再把a 赋值给b的时候,会把这个指针拷贝给了b。 此时,栈内存中存在了两个一样的指针,他们的所有者分别是a和b,这与上一个例子是一致的。 但是,问题是这两个指针指向的是同一片堆内存,这就有问题了,这意味着这一片内存同时拥有了两个所有者。 此时,就不满足'一个数值只有一个所有者'的要求了,于是这个过程中发生了所有权转移,这片内存的所有权,从a转交到了b,而a立马被遗弃了

css 复制代码
let a = String::from("hello world");
let b = a;
//此时使用a,会报错
println!("{}", a);

那么,如果一个值允许两个所有者,会发生什么呢?

当变量离开作用域后,Rust 会自动调用 drop 函数并清理变量的堆内存。 但是两个 String 变量指向了同一位置,这就有了一个问题:当 ab 离开作用域,它们都会尝试释放相同的内存。这是一个叫做 二次释放(double free) 的错误,也是之前提到过的内存安全性 BUG 之一。 因此,Rust 这样解决问题:s1 赋予 s2 后,Rust 认为 s1 不再有效,因此也无需在 s1 离开作用域后 drop 任何东西,这就是把所有权从 s1 转移给了 s2s1 在被赋予 s2 后就马上失效了

再来看一段代码:

ini 复制代码
fn main() {
    let x: &str = "hello, world";
    let y = x;
    println!("{},{}",x,y);
}

如果参考之前的 String 所有权转移的例子,这段代码应该是有问题的。但实际却是正常的。 这就是之前说的,&str和String是两种不同的类型。"hellow world"本身是存放在数据段的,是常量,不可被更改,在进程结束之前也不会被释放。 Rust认为,对于&str类型,只是引用,不属于所有权,而且因为你无法直接对&str进行写操作,因此对它的引用是安全的。

以上String例子中的转移所有权问题,如果我们确实需要两个变量来表示同一份数据,可以使用clone。

css 复制代码
let a = String::from("hello");
let b = a.clone();

println!("a = {}, b = {}", a, b);

这段代码能够正常运行,因为发生了深拷贝,a和b指向了不用的堆内存空间,当然这也会造成性能的下降。

函数参数和返回值

好了,下面到了最不容易察觉的所有权转移场景了。

在调用函数时,基础类型的参数会被深拷贝,而复合类型的参数只会浅拷贝,这个跟JS是一致的。 我们以一段JS代码为例:

css 复制代码
// 一个函数,有两个参数。参数a为number,参数b为object
const change= (tmp1,tmp2)=>{
    tmp1=a+1;
    tmp2.name='hellow JS '
}
const a = 2;
const b={
 name:'hello Rust'
}
change(a,b)
console.log(a,b.name) //  打印a是2, b.name是'hellow JS'

我们解释一下背后发生的事情, 当调用函数时,a的值被深拷贝了,并赋值给了一个临时变量tmp1,此时对tmp1的修改,不会影响到a。因此,a还是2。 b也被拷贝了,只是浅拷贝,拷贝了b对应的指针,并把这个指针赋值给了临时变量tmp2.但是由于tmp2和b指向了同一片内存,所以修改了tmp2也就修改了b。 这个过程中,发生了拷贝和赋值,自然也就产生了所有权的转移。所以在rust中这样执行:

css 复制代码
fn main() {
    let a = 2; 
    let b = String::from("hello");  
    change(a,b)// b发生了作用域转移,当change函数调用结束后,b对应的值就被释放了
    println!("{},{}",a,b); //这里打印,a是正常的,b就会报错
} 

同样的,函数返回值也有所有权,例如:

css 复制代码
fn change(a:String){
   ....//对a进行修改
   a //这是rust的返回语法,不用写return,仅仅写你想返回的值,并且不加;号即可
}

fn main() {
    let a = String::from("hello");  
    let b = change(a);// a发生了作用域转移,但是所有权又被转移给了b
    println!("{},{}",a); //这里打印,b是正常的,a就会报错
} 

所有权避免了一部分内存安全问题,但是显而易见的带来了问题:我们需要将一个值来来回回的传递。为了解决这个问题,rust引入了另一个设计

引用和借用

常规引用是一个指针类型,指向了对象存储的内存地址。在下面代码中,我们创建一个 i32 值的引用 y,然后使用解引用运算符来解出 y 所使用的值:

ini 复制代码
fn main() {
    let x = 5;
    let y = &x;// 获取存储5的内存的指针

    assert_eq!(5, x);
    assert_eq!(5, *y);
}

变量 x 存放了一个 i325yx 的一个引用。可以断言 x 等于 5。 然而,如果希望对 y 的值做出断言,必须使用 *y 来解出引用所指向的值(也就是解引用 )。 一旦解引用了 y,就可以访问 y 所指向的整型值并可以与 5 做比较。

相反如果尝试编写 assert_eq!(5, y);就就会直接报错,因为它们是不同的类型。

不可变引用

下面的代码,我们用 a 的引用作为参数传递给 change 函数:

rust 复制代码
fn main() {
    let a = String::from("hello");
    change(&a);
     println!("{}",a); //这里打印,a是正常的
}

fn change(s: &String) ->  {
   ...//do something

}

需要注意两点不同:

  1. 传入的是引用,而不是直接传入了a,同时也不用再将a返回回去
  2. change 的参数 s 类型从 String 变为 &String

通过 &a 语法,我们创建了一个指向 a 的引用,但是并不拥有它。因为并不拥有这个值,当引用离开作用域后,其指向的值也不会被丢弃。 于是我们开开信息的想修改借用的变量:

javascript 复制代码
fn main() {
    let s = String::from("hello");

    change(&s);
}

fn change(some_string: &String) {
    some_string.push_str(", world");
}

很不幸,发现报错了

javascript 复制代码
error[E0596]: cannot borrow `*some_string` as mutable, as it is behind a `&` reference
 --> src/main.rs:8:5
  |
7 | fn change(some_string: &String) {
  |                        ------- help: consider changing this to be a mutable reference: `&mut String`
                           ------- 帮助:考虑将该参数类型修改为可变的引用: `&mut String`
8 |     some_string.push_str(", world");
  |     ^^^^^^^^^^^ `some_string` is a `&` reference, so the data it refers to cannot be borrowed as mutable
                     `some_string`是一个`&`类型的引用,因此它指向的数据无法进行修改

正如变量默认不可变一样,引用指向的值默认也是不可变的。

可变引用

只需要一个小调整,即可修复上面代码的错误:

rust 复制代码
fn main() {
    let mut a = String::from("hello");
    change(&mut a);
}

fn change(some_string: &mut String) {
    some_string.push_str(", world");
}

需要注意这几行代码中mut的位置。 对于可变引用,rust有两个重要的限制:

1. 可变引用同时只能存在一个

不过可变引用并不是随心所欲、想用就用的,它有一个很大的限制: 同一作用域,特定数据只能有一个可变引用

ini 复制代码
let mut s = String::from("hello");

let r1 = &mut s;
let r2 = &mut s;

println!("{}, {}", r1, r2);

以上代码会报错:

go 复制代码
同一时间无法对 `s` 进行两次可变借用

这种限制非常费解,但好处就是使 Rust 在编译期就避免数据竞争。如果同时存在多个函数或者线程对同一块内存进行写操作,就难免出现未知的问题,且这种问题难以诊断。rust通过这种方式,来杜绝这种问题,这更像是一种编程范式或者思想了。 这种时候,我们可以手动新增一个作用域:

ini 复制代码
let mut s = String::from("hello");

{
    let r1 = &mut s;

} // r1 在这里离开了作用域,所以我们完全可以创建一个新的引用

let r2 = &mut s;

2.可变引用与不可变引用不能同时存在

ini 复制代码
let mut s = String::from("hello");

let r1 = &s; // 没问题
let r2 = &s; // 没问题
let r3 = &mut s; // 大问题

println!("{}, {}, and {}", r1, r2, r3);

错误如下:

swift 复制代码
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
 // 无法借用可变 `s` 因为它已经被借用了不可变

这种做法自然也是为了数据安全,这样不会存在你读的数据被别人改了的情况。

但是,这里需要注意引用的作用域

注意,引用的作用域 s 从创建开始,一直持续到它最后一次使用的地方,这个跟变量的作用域有所不同,变量的作用域从创建持续到某一个花括号 }

ini 复制代码
fn main() {
   let mut s = String::from("hello");
    let mut b =3;
    let r1 = &s;
    println!("{} and {}", r1);
    //r1作用域在这里结束
    
   let r3 = &mut s;// 因为r1被释放了,所以这里可以再次赋值
   b+=1;
}//b的作用域直到这里才结束

rust这一套所有权和借用的设计,很好的避免了以下问题:

  1. 内存未及时释放引起的泄漏
  2. 数据竞争
  3. 悬垂指针,二次释放等常见的指针问题

不过rust并不是通过运行时去自动解决的,它是通过加上强限制,让程序员写代码的时候更加谨慎和小心一点,最终还是把问题抛给了程序员的。

当程序员克服了这个问题,后期的维护成本和线上风险就降低了很多。

相关推荐
a栋栋栋3 小时前
apifox
java·前端·javascript
请叫我飞哥@4 小时前
HTML 标签页(Tabs)详细讲解
前端·html
Anlici5 小时前
React18与Vue3组件通信对比学习(详细!建议收藏!!🚀🚀)
前端·vue.js·react.js
m0_748251525 小时前
PDF在线预览实现:如何使用vue-pdf-embed实现前端PDF在线阅读
前端·vue.js·pdf
中生代技术5 小时前
3.从制定标准到持续监控:7个关键阶段提升App用户体验
大数据·运维·服务器·前端·ux
m0_748239335 小时前
从零开始:如何在.NET Core Web API中完美配置Swagger文档
前端·.netcore
m0_748232926 小时前
【前端】Node.js使用教程
前端·node.js·vim
hawleyHuo6 小时前
umi 能适配 taro组件?
前端·前端框架
web130933203986 小时前
[JAVA Web] 02_第二章 HTML&CSS
java·前端·html
黑客呀6 小时前
Go Web开发之Revel - 网页请求处理流程
开发语言·前端·web安全·golang·系统安全