Rust所有权

GoLang之Rust所有权

1.Rust所有权介绍

众所周知,Rust是一门以内存安全且高性能而著称的系统级编程语言。在Rust的世界里,没有GC,因此也没有内存回收时带来的STW问题,故而提高程序的运行效率;但是它也不需要我们手动管理内存而增加开发人员的负担。而在这内存安全和高性能的背后,得益于Rust的独有的所有权系统。
备注: 拥有GC(垃圾回收)语言的典型代表——Java, Go; 需要开发者手动管理内存的C/C++。

2.变量是分配在堆上还是分配在栈上?

在一些高级编程语言中,定义变量时我们很少关心这个变量是被分配在栈上还是分配在堆上,比如在Go, Python,几乎是不需要关心这个问题的。但是在Rust中不同,我们时时刻刻都要想着这个变量是分配在栈上还是在堆上,因为这会影响Rust的运行性能以及行为,甚至决定了我们的程序能否正常编译通过。

和栈是程序运行时可用的内存部分,他们最本质的区别在于数据组织结构不同。
栈是一个后进先出的组织结构,存储在栈上的数据大小是明确的,在程序编译期间就已经确定。我们说把变量分配在栈上,其实就是一个压栈的过程。
在编译期间无法确定占用内存大小的数据,或者数据大小是动态变化的,都需要分配在堆上。程序向操作系统申请在堆上分配内存时,会返回一个指针,因为指针的大小是固定的,因此指针会被存储在栈上面。当我们需要访问堆上的数据时,通过栈中的指针就能访问了。

备注:
入栈比在堆上分配内存更快——即写数据。因为入栈位置始终在栈的顶部,操作系统无需搜索可用空间;但是堆上分配内存时,操作系统是需要找出足够用的内存块的,因此相对较慢。
访问栈上的数据比访问堆上的数据更快——即读数据。因为我们必须先从栈上拿到指向堆的指针,才能继续访问堆上的数据(有这功夫栈上数据都已经返回了);此外,栈上的数据往往可以直接存储在CPU高速缓存中,而堆上的数据只能在内存里面待着。众所周知…高速缓存的访问速度比从内存中读取快多了。

3.堆和栈上的数据何时回收?

压入栈中的数据,会在栈返回时依次移除。如函数调用时传递的参数会被依次入栈,在函数结束时会反向移除。
但是堆上的数据是没有组织结构的,想要回收数据释放内存就麻烦了。前面提到过两种方法:GC和手动管理。
GC的弊端就是性能差,存在STW(Stop The World)问题。手动管理就更麻烦了,对程序员要求很高,需要我们一直关注内存的分配和释放,否则会出现很多内存安全问题,如悬空指针、多个引用同时释放导致的不确定行为等。
而今天的主角Rust,则是另辟蹊径,用一个全新方案——所有权系统,来解决内存管理问题。

4.Rust的所有权

所有权是Rust程序如何管理内存的一组编译规则,在编译期间由编译器负责检查并给出检查结果。

4.1所有权规则要牢记

1.Rust中的每一个值都有名为 owner(所有者) 的变量
2.Rust中的每一个值 有且只有 一个所有者
3.当所有者离开作用域范围时,这个值就会被释放(drop)

4.2作用域

Rust 作用域与其他编程语言类似,一个变量的作用域基本是在一个 {} 范围内有效,超过 } 这个范围就无效了。

fn main() {
  let _s1 = "hello";
  {
    let _s2 = "world";
  }  // 变量 _s2 在这里被释放
  
  // 取消s2的输出会报错:cannot find value `s2` in this scope
  // println!("{}", s2);
  println!("{}", s1);
} // 变量 _s1 在此处被释放

4.3 变量与数据的交互方式

Rust 之所以能做到每个值只有一个所有者,是因为其所有权转移机制。在Rust中,变量和数据的交互有两种方式:move 和 clone。

所有权转移: move

对于分配在堆内存上的变量,当将其赋值给另一个变量时,就会发生所有权的转移。所有权转移时, 其实是将拷贝了值在栈中的指针地址、长度和容量,并没有赋值实际的数据。 如下示例,s1 分配在堆上, 将其赋值给s2后,所有权就转移到s2了,当再次访问s1时编译器就会提示 s1已经被move掉了。

fn main() {
    // String 是字符串类型,分配在堆上
    let s1 = String::from("hello");
    println!("{}", s1);

    let s2 = s1;
    println!("{}", s2);

    // 注释掉对 s1 的访问,程序就能正常编译了
    println!("{}", s1);  //  borrow of moved value: `s1`
}

如果有其他语言的使用经历(比如Python)就会发现,Rust中的move机制,与对象的浅拷贝类似。但是在Rust中,被"拷贝"的原变量会被删除,因此称之为move是在再合适不过了。

数据拷贝:clone

上面的move中的示例代码,如果需要让他编译通过,只需要做稍许改动即可,如下:

fn main() {
    let s1 = String::from("hello");
    println!("{}", s1);

    // 注意此处。使用字符串的 clone方法进行变量的赋值
    let s2 = s1.clone();
    println!("{}", s2);

    println!("{}", s1);
}

这里的 s2 = s1.clone(); 是没有所有权转移的,因为clone方法将s1指向的数据完整的赋值一份并由s2接管。因此在内存中, 此时是会有两个 “hello” 字符串。clone时,同样会有堆内存的分配过程,因此 clone相对move来说,是逻辑较重的行为,性能也自然更低了。
对此,Rust的设计是:对于分配在堆内存上的数据,Rust进行赋值时默认都是使用 move。

栈数据拷贝
fn main() {
   let x = 5;
   let y = x;
   println!("x = {}, y = {}", x, y);
}

根据前面的经验,这个示例应该会报所有权的错误。而实际上这段代码是能正常编译的…
前面说过,对于编译时大小是固定的数据,我们是能把数据分配在栈内存上的,没必要放到堆上。而栈上的数据拷贝是非常快的,因此Rust实现中,对于编译时能知道大小的数据,直接放栈内存上即可,栈上数据拷贝也是拷贝一份完整数据。因此栈上的数据是没有深拷贝和浅拷贝这种说法,换句话说就是没有move和clone这种概念。
Rust 有一个特殊的注解,称为Copy Trait,我们可以将它放在像整数一样存储在堆栈中的类型上。如果一个类型实现了Copy Trait,一个变量在赋值给另一个变量后仍然有效。
对于何种类型可以Copy,您可以检查给定类型的文档确定。作为一般规则:
任何一组简单的基本类型都可以实现Copy;没有任何需要分配或某种形式的资源可以实现Copy。以下是一些实现的类型Copy:
1.所有整数类型,例如u32
2.布尔类型
3.所有浮点类型,例如f64
4.字符类型,char
元组——元组中的元素也需要实现Copy 特征。 例如,(i32, i32)实现了Copy,但(i32, String)没有。

4.4函数与所有权

将变量传递给函数时一样会有 移动 和 复制 的行为。下面示例展示了函数调用时传递栈上的数据和堆上的数据会有什么不同。


fn main() {
    let s = String::from("hello");  // s 进入作用域
    takes_ownership(s);  // s指向的值发生所有权转移,被move进函数
    // println!("s: {}", s);  // 尝试打印x会报错:borrow of moved value: `s`

    let x = 5;  // x进入作用域
    makes_copy(x);  // x是i32类型,实现了Copy特征,
                    // 值被拷贝一份传给函数

    println!("x: {}", x)  
} // x 超出作用域;然后是s。但是因为s的值已经被move走,因此不会有清理动作
fn takes_ownership(some_string: String) { // some_string 进入作用域
    println!("{}", some_string);
} // some_string 超出作用域并且被drop

fn makes_copy(some_integer: i32) { // some_integer 进入作用域
    println!("{}", some_integer);
} // some_integer 超出作用域

这里需要注意的是:s的值的所有权被move进takes_ownership函数并赋值给some_string。takes_ownership 函数结束时,some_string会被drop掉。但是当main函数返回时,由于 s已经失效了,因此不会有针对s的drop动作。
类似的,函数的返回值也有所有权,如:


fn main() {
    let s1 = gives_ownership();  // gives_ownership 将返回值移给 s1
    let s2 = String::from("hello");     // s2 进入作用域

    // s2 被移动到
    // takes_and_gives_back 中,                  
    // 它也将返回值移给 s3
    let s3 = takes_and_gives_back(s2);                                  
} // 这里, s3 移出作用域并被drop。s2 也移出作用域,但已被移走,
  // 所以什么也不会发生。s1 移出作用域并被drop

// gives_ownership 将返回值移动给调用它的函数
fn gives_ownership() -> String {  
     // some_string 进入作用域.
    let some_string = String::from("hello");
    
    // 返回 some_string 并move给调用方
    some_string                              
}

// takes_and_gives_back 将传入字符串并返回该值
fn takes_and_gives_back(a_string: String) -> String { // a_string 进入作用域
    a_string  // 返回 a_string 并移出给调用的函数
}

可以看到,如果需要在函数间传值、返回值时,会一直涉及到所有权调用,这代码写起来就折腾人了,真的让人头大。那Rust有没有更好的一个方法呢?且听下回分解…

5.总结

Rust创造性的引入了所有权来解决由于程序员的不靠谱而导致的内存安全相关问题,把人为导致的不安全因素在编译器就全部暴露出来,确实是奥利给!而且还抛弃了GC,简直是6的不行。但同样的,设计的复杂性也带来了使用的复杂性,所有权系统也是很难掌握的,需要不断的被编译器教育才行…Rust出了名的难学(难到用Rust实现一个链表都能出一本书),所有权系统当记一功!

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Rust是一种系统级编程语言,其设计的核心是所有权(Ownership)和生命周期(Lifetime),这两个概念确保了内存安全并避免了空指针异常。以下是Rust所有权规则的主要要点: 1. **所有权**:每个值在任何时候只能被一个变量拥有。这意味着Rust的每一个值都有一个明确的所有者,一旦所有权转移,原所有者对该值就不再有任何权利。 2. **生命周期**:生命周期描述了一个变量在其作用域内存在的时间段。当一个变量的生命周期结束,其拥有的值也自动销毁,以防止悬挂引用(dangling references)。 3. **移动(Move)**:值传递默认是移动(move),这意味着当一个变量的值转移给另一个变量时,原始变量失去对那块内存的访问,而不是复制。 4. **借用(Borrowing)**:对于暂时需要某个变量的内容但不希望转移所有权的情况,可以使用借用。Rust提供了`&`和`&mut`两种借用方式,分别是常量借用(不可变)和可变借用(可修改)。 5. **引用(References)**:`&T`是Rust的引用语法,它可以指向T类型的值,但不改变所有者。引用本身也有生命周期,它必须活得比所指向的值更长。 6. **生命周期关联**:Rust通过生命周期标记(lifetime annotations)来管理引用的生命周期,确保引用在引用值存在期间始终有效。 7. **智能指针(Smart Pointers)**:如`Rc`(引用计数)和`Arc`(原子引用计数)用于处理共享所有权,以及`Box`和`Vec`的内部实现。 8. **析构函数(Destructors)**:当一个值离开作用域或所有权转移时,析构函数被调用,执行清理工作。 **相关问题--:** 1. Rust如何处理不同生命周期的变量? 2. 为何Rust强调移动而不是复制? 3. 何时使用`Rc`和`Arc`,它们的区别是什么?

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

GoGo在努力

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值