老前端学Rust - 第三课(所有权)

各位伙伴们大家好,鄙人又来了。。。照例先给各位汇报下这周的工作情况。这周农药上了十几颗星~哈哈哈。周末喊朋友们吃了顿水浒烤肉,真™的贵。还有就是朋友都是骑车来的,自行车骑行这么火了嘛,咱被拉着去捷安特看了看车,真™贵,但是心动了。。。

好了,扯闲篇过去了,咱们进入今天的课题,rust的所有权。

什么是所有权

js中是通过垃圾回收来管理我们的代码的内存空间的,一般是标记清除和引用计数等方案,然后新老生代的优化方案来实现的。

所有语言都需要对内存进行管理,js、java通过垃圾回收机制,C、C++手动的分配和释放内存,C++还通过RAII(资源获取即初始化)的原则,在构造函数和析构函数来管理内存。而rust则使用了另外一种独有的方案,就叫所有权他使用包含特定规则的所有权系统来管理内存,这套规则允许编译器在编译过程中执行检查工作,而不产生运行时开销。

说人话就是rust遵循了一套规则,你写的代码在代码写完的时候根据规则去判断你的变量的所有权交给了谁,有没有被释放掉啥的。

所有权规则

  • rust中的每一个值都有一个对应的变量作为他的拥有者
  • 在同一时间,值有且仅有一个所有者
  • 当所有者离开自己的作用域时,它持有的值就会被释放掉

解释下来就是在堆或者栈(一般来说,基础类型、容易确定占用内存大小的值会在栈中,不容易定义内存大小的值一般在堆中,如元组等,类似js,栈中会存有对堆中值的地址的指针)中的每一个值,都有对应的所有者,而且在同一时间只有一个拥有者,不会出现一个值即属于变量A又属于变量B,并且当所有者离开作用域,即类似es6的块级作用域或者函数作用域后,值的内存就会被释放掉。有点类似RAII,下面我会举一些例子来表示:

rust 复制代码
fn main() {
    let x = 1;
}

这个例子,在执行到let x = 1时,值1会有一个所有者x,当main结束时,值1的栈内存会释放。即第一个和第三个原则,rust中的每一个值都有一个对应的变量作为他的拥有者,当所有者离开自己的作用域时,它持有的值就会被释放掉。

rust 复制代码
fn main() {
    let x = String::from("hello");     // 往堆中推入一个字符串类型的数据,且x拥有所有权
    let y = x;                         // x将所有权交给y
    println!("{}", x);                 // error❎,这时x已经失去了字符串hello的所有权
} // 作用域结束,这里只需要释放y指向的值的内存

fn other() {
    let x = 5;    // 栈中有一个5,并且x拥有所有权
    let y = x;    // 栈中额外推入一个5,并且y拥有这个5的所有权
    println!("{}", x) // 正确
}

这个例子中有两个函数

第一个函数中,::符号是表示使用String命名空间下的某个函数,后面会讲到,现在就当一个函数看就行了。他会在执行的时候创建一个String实例,并将所有权交给x,这里的String实例是存储在堆中的,因为String可以在后面x.push_str("end")等操作,所以他是可变的数据结构,会存储在堆中,x是指向堆地址的一个指针,这时候let y = x会转移所有权,根据第二原则,在同一时间,值有且仅有一个所有者 ,所以在这时候只有y才有String实例的所有权,x已经没了,所以后面打印就是报错。这里是为了避免重复分配新的内存,同时也是为了内存安全,rust就在这里简单的将x废弃掉了。有点像js的浅拷贝,但是又废弃了x,所以在Rust中有个专门的术语,叫做移动(move)

第二个函数中,当执行到let y = x;时候,因为5是一个确定内存大小的值,所以其实会有两个5被推进栈中,即这时是有两个内存地址完全不同的5被两个变量所有,所以不违反第二个原则。

这里其实我们也可以想到语言作者的设计思路,栈上内容的拷贝就算是浅拷贝也是值拷贝,并且存储在栈中的值大小确定,且栈内存的存取速度都很快,所以没有必要阻止x保留其所有权,而第一个的例子的堆内存的则不同,如果深拷贝堆内存的数据,一个是空间不确定,得申请足够大的空间,且堆上的内存的存取本来就比栈内存差很多,所以只保留一个所有权就变得很有必要,可能这也是作者在考虑栈和堆内存的所有权转移的一些考量吧。

这里隐含了另外一个设计原则: Rust永远不会自动的创建数据的深拷贝,所以任何自动的赋值操作都可以被视为是高效的。

下面还有几个例子:

所有权可以被转移进函数:

rust 复制代码
fn main() {
    let s = String::from("hello"); // 变量s进入作用域
    
    takes_ownership(s); // s的值被移动进了函数
                        // 所以这里开始s不在有效,他已经失去了所有权
    
    let x = 5;          // 变量x进入作用域
    
    makes_copy(x);      // 变量x被传递给函数,由于x是栈内存,根据我们上面的例子,x不会丢失所有权
    
    println!("s={}, x={}", s, x);     //这里如果打印s会报错,但是只打印x会有值
}     // x离开作用域,释放内存,然后是s离开作用域,但是s的所有权被转移,所以无事发生

fn takes_ownership(some_string: String) {
    println!("{}", some_string); 
} // 这里some_string占用的内存被释放

fn makes_copy(some_integer: i32) {
    println!("{}", some_integer);
} 

函数的返回值也会转移所有权:

rust 复制代码
fn main() {
    let s1 = gives_ownership();    // 返回值移动至s1
    
    let s2 = String::from("hello");  // s2进入作用域
    
    let s3 = takes_and_gives_back(s2);  // s2被移动进函数,然后返回值又被移动给了s3
   
} // 离开作用域,先释放s3的内存,然后s2因为已经转移,所以无事发生,再释放s1的内存

fn gives_ownership() -> String{
    let some_string = String::from("hello"); // some_string进入作用域
    some_string     // some_string作为返回值移动至调用函数
}

fn takes_and_gives_back(a_string: String) -> String {
    a_string
}

上面的例子可能大家发现了一个开发的问题,假设我要执行takes_and_gives_back,但是我又要s2保留所有权,怎么办呢?就得在takes_and_gives_back中将所有权再还回来,假设我的函数的逻辑是要返回其他数值,那就得函数返回一个元组,其中包括需要的值和s2的原本的值,let (s3, others) = takes_and_gives_back(s2),这样就很麻烦,那有没有什么好办法在传给函数后还保留所有权呢?Rust提供了一个功能,引用

引用

rust 复制代码
fn main() {
    let s1 = String::from("hello");
    
    let len = calculate_len(&s1);
    
    println!("the length of {} is {}", s1, len);
}

fn calculate_len(s: &String) -> usize {
    s.len()
}

&代表的就是引用语义,它们允许在不获取所有权的前提下使用值。由于引用不持有值的所有权,所以当引用离开当前作用域时,它指向的值也不会丢弃。

这种通过引用传递参数给函数的方式也被称为借用

因为你的值是被借用过去的,没有所有权,所以你无权对我的值做修改操作,下面的例子就会报错:

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

fn change(s: &String) {
    s.push_str("sell");   // 错误❎, 借你车还想卖了变现,真是狗,车的所有权是我的,只是借你开开,无权做任何变更
}

rust也有一种方式去支持可变引用 ,跟let一样,使用关键字mut

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

fn change(s: &mut String) {
    s.push_str("sell");   // 正确✅
}

不过可变引用是有限制的,为了避免数据竞争,rust只允许在特定作用域的特定数据只可以声明一个可变引用

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

let r1 = &mut s;
let r2 = &mut s; //error❎

同时,不可变引用与可变引用也不能同时存在,毕竟,我拿的是不可变的数据,我不希望不可变的数据在我眼皮子底下突然变化了。

什么是数据竞争:

  • 两个或两个以上的指针同时访问同一空间
  • 其中至少会有一个指针向该空间中写入数据
  • 没有同步数据访问的机制

这有点像做分布式系统的时候,数据库或者redis如何保持数据的一致性。rust从根上避免了这个问题,就是让你编译都通过不了。。。rust都是为了你好啊你知道吗。

Rust还有另一种不持有所有权的数据类型:切片(slice)。我留到下一讲再聊它。我感觉今天这篇的字数已经达标了,再多可能就不会读下去了~哈哈哈。保持每篇文章5000字以内,一般小几分钟就看完了,碎片化,挺好,整个长文大部分人很难看下去。

那今天就这样了,下一节老规矩,各位大佬们,下周再见👋🏻

ps: 有没有大佬有推荐函数式编程的文章跟课程,总感觉虽然很刻意地往上靠,但是还是比较浅~

相关推荐
文军的烹饪实验室37 分钟前
ValueError: Circular reference detected
开发语言·前端·javascript
Martin -Tang2 小时前
vite和webpack的区别
前端·webpack·node.js·vite
迷途小码农零零发2 小时前
解锁微前端的优秀库
前端
王解3 小时前
webpack loader全解析,从入门到精通(10)
前端·webpack·node.js
老码沉思录3 小时前
写给初学者的React Native 全栈开发实战班
javascript·react native·react.js
我不当帕鲁谁当帕鲁3 小时前
arcgis for js实现FeatureLayer图层弹窗展示所有field字段
前端·javascript·arcgis
那一抹阳光多灿烂3 小时前
工程化实战内功修炼测试题
前端·javascript
放逐者-保持本心,方可放逐4 小时前
微信小程序=》基础=》常见问题=》性能总结
前端·微信小程序·小程序·前端框架
毋若成6 小时前
前端三大组件之CSS,三大选择器,游戏网页仿写
前端·css
红中马喽6 小时前
JS学习日记(webAPI—DOM)
开发语言·前端·javascript·笔记·vscode·学习