Rust的所有权(Ownership)
1. 什么是Ownership
Rust的所有权,是一个跨时代的理念,是内存管理的第二次革命。Ownership是Rust的一个核心概念。
每种编程语言都有自己的一套内存管理的方法。有些需要显式的分配和回收内存(如C),有些语言则依赖于垃圾回收器来回收不使用的内存(如Java)。而Rust不属于以上任何一种,它有一套自己的内存管理规则,叫做Ownership。
Rust中常规数据类型,其数据都是存储在栈中,而像String或一些自定义的复杂数据结构(我们以后会对它们进行详细介绍),其数据则存储在堆内存中。
2. Ownership的规则
Rust的所有权并不难理解,它有且只有如下三条规则:
- 在Rust中,每一个值都有对应的变量,这个变量称为值的owner
- 一个值在某一时刻只能有一个owner
- 当owner超出作用域后,值会被销毁
3. 变量作用域
Ownership的规则中,有一条是owner超过范围后,值会被销毁。那么owner的范围又是如何定义的呢?在Rust中,花括号通常是变量范围作用域的标志。最常见的在一个函数中,变量s的范围从定义开始生效,直到函数结束,变量失效。
fn main() { // s is not valid here, it’s not yet declaredlet s = "hello"; // s is valid from this point forward// do stuff with s
} // this scope is now over, and s is no longer valid
这个这和其他大多数编程语言很像,对于大多数编程语言,都是从变量定义开始,为变量分配内存。而回收内存则是八仙过海各显神通。对于有依赖GC的语言来说,并不需要关心内存的回收。而有些语言则需要显式回收内存。显式回收就会存在一定的问题,比如忘记回收或者重复回收。为了对开发者更加友好,Rust使用自动回收内存的方法,即在变量超出作用域时,回收为该变量分配的内存。
4. Ownership的移动
前面我们提到,花括号通常是变量作用域隔离的标志(即Ownership失效)。除了花括号以外,还有其他的一些情况会使Ownership发生变化,先来看两段代码。
let x = 5;
let y = x;
println!("x: {}", x);
let s1 = String::from("hello");
let s2 = s1;
println!("s1: {}", s1);
这两段代码看起来唯一的区别就是变量的类型,第一段使用的是整数型,第二段使用的是字符串型。而执行结果却是第一段可以正常打印x的值,第二段却报错了。这是什么原因呢?
我们来分析一下代码。对于第一段代码,首先有个整数值5,赋给了变量x,然后把x的值copy了一份,又赋值给了y。最后我们成功打印x。看起来比较符合逻辑。实际上Rust也是这么操作的。
对于第二段代码我们想象中,也可以是这样的过程,但实际上Rust并不是这样做的。先来说原因:对于较大的对象来说,这样的复制是非常浪费空间和时间的。那么Rust中实际情况是怎么样呢?
首先,我们需要了解Rust中String类型的结构:
上图中左侧是String对象的结构,包括指向内容的指针、长度和容量。这里长度和容量相同,我们暂时先不关注。后面详细介绍String类型时会提到两者的区别。这部分内容都存储在栈内存中。右侧部分是字符串的内容,这部分存储在堆内存中。
既然复制内容会造成资源浪费,那我只复制结构这部分好了,内容再多,我复制的内容长度也是可控的,而且也是在栈中复制,和整数类型类似。这个方法听起来不错,我们来分析一下。按照上面这种说法,内存结构大概是这个样子。
这种会有什么问题呢?还记得Ownership的规则吗?owner超出作用域时,回收其数据所占用的内存。在这个例子中,当函数执行结束时,s1和s2同时超出作用域,那么上图中右侧这块内存就会被释放两次。这也会产生不可预知的bug。
Rust为了解决这一问题,在执行let s2 = s1;
这句代码时,认为s1已经超出了作用域,即右侧的内容的owner已经变成了s2,也可以说s1的ownership转移给了s2。也就是下图所示的情况。
另一种实现:clone
如果你确实需要深度拷贝,即复制堆内存中的数据。Rust也可以做到,它提供了一个公共方法叫做clone。
let s1 = String::from("hello");
let s2 = s1.clone();println!("s1 = {}, s2 = {}", s1, s2);
clone的方法执行后,内存结构如下图:
5. 函数间转移
前面我们聊到的是Ownership在String之间转移,在函数间也是一样的。
fn main() {let s = String::from("hello"); // s 作用域开始takes_ownership(s); // s's 的值进入函数// ... s在这里已经无效} // s在这之前已经失效
fn takes_ownership(some_string: String) { // some_string 作用域开始println!("{}", some_string);
} // some_string 超出作用域并调用了drop函数// 内存被释放
那有没有办法在执行takes_ownership函数后使s继续生效呢?一般我们会想到在函数中将ownership还回来。然后很自然的就想到我们之前介绍的函数的返回值。既然传参可以转移ownership,那么返回值应该也可以。于是我们可以这样操作:
fn main() {let s1 = String::from("hello"); // s2 comes into scopelet s2 = takes_and_gives_back(s1); // s1 被转移到函数中// takes_and_gives_back,// 将ownership还给s2
} // s2超出作用域,内存被回收,s1在之前已经失效// takes_and_gives_back 接收一个字符串然后返回一个
fn takes_and_gives_back(a_string: String) -> String { // a_string 开始作用域a_string // a_string 被返回,ownership转移到函数外
}
这样做是可以实现我们的需求,但是有点太麻烦了,幸好Rust也觉得这样很麻烦。它为我们提供了另一种方法:引用(references)。
6. 引用和借用
引用的方法很简单,只需要加一个&
符。
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()
}
这种形式可以在没有ownership的情况下访问某个值。其原理如下图:
这个例子和我们在前面写的例子很相似。仔细观察会发现一些端倪。主要有两点不同:
在传入参数的时候,s1前面加了&符。这意味着我们创建了一个s1的引用,它并不是数据的owner,因此在它超出作用域时也不会销毁数据。
函数在接收参数时,变量类型String前也加了&符。这表示参数要接收的是一个字符串的引用对象。
我们把函数中接收引用的参数称为借用。就像实际生活中我写完了作业,可以借给你抄一下,但它不属于你,抄完你还要还给我。
另外还需要注意,我的作业可以借给你抄,但是你不能改我写的作业,我本来写对了你给我改错了,以后我还怎么借给你?所以,在calculate_length中,s是不可以修改的。
7. 可修改引用
如果我发现我写错了,让你帮我改一下怎么办?我授权给你,让你帮忙修改,你也需要表示能帮我修改就可以了。Rust也有办法。还记得我们前面介绍的可变变量和不可变变量吗?引用也是类似,我们可以使用mut关键字使引用可修改。
fn main() {let mut s = String::from("hello");change(&mut s);
}fn change(some_string: &mut String) {some_string.push_str(", world");
}
这样,我们就能在函数中对引用的值进行修改了。不过这里还要注意一点,在同一作用域内,对于同一个值,只能有一个可修改的引用。这也是因为Rust不想有并发修改数据的情况出现。
如果需要使用多个可修改引用,我们可以自己创建新的作用域:
let mut s = String::from("hello");{let r1 = &mut s;} // r1 超出作用域let r2 = &mut s;
另一个冲突就是“读写冲突”,即不可变引用和可变引用之间的限制。
let mut s = String::from("hello");let r1 = &s; // no problem
let r2 = &s; // no problem
let r3 = &mut s; // BIG PROBLEMprintln!("{}, {}, and {}", r1, r2, r3);
这样的代码在编译时也会报错。这是因为不可变引用不希望在被使用之前,其指向的值被修改。这里只要稍微处理一下就可以了:
let mut s = String::from("hello");let r1 = &s; // no problem
let r2 = &s; // no problem
println!("{} and {}", r1, r2);
// r1 和 r2 不再使用let r3 = &mut s; // no problem
println!("{}", r3);
Rust编译器会在第一个print语句之后判断出r1和r2不会再被使用,此时r3还没有创建,它们的作用域不会有交集。所以这段代码是合法的。
8. 空指针
对于可操作指针的编程语言来讲,最令人头疼的问题也许就是空指针了。通常情况是,在回收内存以后,又使用了指向这块内存的指针。而Rust的编译器帮助我们避免了这个问题(再次感谢Rust编译器)。
fn main() {let reference_to_nothing = dangle();
}fn dangle() -> &String {let s = String::from("hello");&s
}
来看一下上面这个例子。在dangle函数中,返回值是字符串s的引用。但是在函数结束时,s的内存已经被回收了。所以s的引用就成了空指针。此时就会报expected lifetime parameter的编译错误。
参考:
https://cloud.tencent.com/developer/article/1596815
https://www.csdn.net/gather_26/MtTakg4sODE4MTUtYmxvZwO0O0OO0O0O.html
https://doc.rust-lang.org/stable/rust-by-example/scope/move.html
Rust的所有权(Ownership)相关推荐
- 理解Rust的所有权
Time: 20190921 所有权是Rust中最独特的特征,有了它就能保证Rust内存安全,且无需垃圾回收机制.因此,理解Rust的所有权机制非常重要.和所有权一起讲到的其他几个概念是: 引用,借用 ...
- rust笔记2 OwnerShip基础概念
首先,要搞清楚栈内存和堆内存对应了那些类型.rust的整型.浮点型.bool型.字面字符串型和tuple型都是栈内存上的:如果使用=,那么这些数据会拷贝一份新的内容. 然后,要了解rust的变量作用域 ...
- rust的所有权与引用
所有权 所有权是rust最独特的特性,它让rust无需GC就可以保证内存安全. 什么事所有权 rust的核心特性就是所有权 所有程序在运行时都必须管理他们使用计算机内存的方式 有些语言有垃圾收集机制, ...
- Rust的所有权与可变性
Rust与其他语言的比较 文章目录 Rust与其他语言的比较 特性 所有权 直接转移 间接转移 引用.借用 可变性与不可变性 特性 所有权 在Rust中,若声明有类似于Java或C++中的引用传递类型 ...
- Rust的前景怎么样?值不值的学—Rust对比、特色和理念
前言 其实我一直弄不明白一点,那就是计算机技术的发展,是让这个世界变得简单了,还是变得更复杂了. 当然这只是一个玩笑,可别把这个问题当真. 然而对于IT从业者来说,这可不是一个玩笑.几乎每一次的技术发 ...
- rust实现wss访问_Rust的所有权,第2部分
仍然没有问题. 上次查看Rust的所有权时,我们查看了Rust如何使用范围来确定何时应该删除或释放内存中的资源/数据. 我们看到,对于具有"复制特征"的类型(即,其数据可以存储在堆 ...
- Rust是如何实现内存安全的--理解RAII/所有权机制/智能指针/引用
不带自动内存回收(Garbage Collection)的内存安全是Rust语言最重要的创新,是它与其他语言最主要的区别所在,是Rust语言设计的核心. Rust希望通过语言的机制和编译器的功能,把程 ...
- Rust学习:5_所有权与借用
Rust学习:5_所有权与借用 前言 为了学习Rust,阅读了github上的Rust By Practice电子书,本文章只是用来记录自己的学习过程,感兴趣的可以阅读原书,希望大家都能掌握Rust! ...
- 连续 3 年最受欢迎:Rust,香!
简介:我们在选择一种开发语言时会综合考量各方面的特性,根据实际的需求适当取舍.鱼和熊掌往往不可兼得,要想开发效率高,必然要牺牲性能和资源消耗,反之亦然.但是Rust却出其不意,令人眼前一亮!本文将从性 ...
最新文章
- 迁移学习之DenseNet121(121层),DenseNet169(169层),DenseNet201(201层)(图像识别)
- 安卓高手之路之 GDI图形引擎篇
- NSBundle介绍
- mysql windows集群_Mysql集群windows服务器版搭建过程
- 给asp.net mvc小白扫盲用的
- HTML label标签学习笔记
- IntelliJ IDEA 常用快捷键和设置
- 关于apache和tomcat集群,线程是否占用实验
- Linux——cmake使用示例与整理总结
- Ouibounce – 在用户离开你网站时显示模态弹窗
- wine手动安装wine-mono和wine-gecko组件
- Ant Design UI 框架的的安装及使用
- J-Link V9固件修复
- Photoshop 换脸大法
- 修改网卡地址 突破一些与MAC绑定服务的限制 突破封锁 应对病毒 等
- 【魔改蜗牛星际】B双主板变“皇帝板”扩展到8个SATA口
- 案例:通过空气质量指数AQI学习统计分析并进行预测(上)
- 天津科技大学计算机科学与信息工程学院,天津科技大学计算机科学与信息工程学院简介...
- 多线程顺序执行四种方案
- 理解sklearn.processing.scale中使用有偏总体标准差
热门文章
- 树莓派智能小车python_人工智能-树莓派小车(1)——DuerOS语音唤醒
- C++雾中风景4:多态引出的困惑,对象的拷贝?
- 微信小程序周报(第十一期)-极乐商店出品
- NHibernate剖析:Mapping篇之Mapping-By-Code(1):概览
- Impinj增强ItemSense软件功能,简化RFID方案部署
- Basic INFO: InstallShield中如何获取所调用Exe的返回值
- KlayGE 3.10.0发布!
- SQL Server外连接、内连接、交叉连接
- 管理刀片服务器的KVM切换器
- Windows Server Failover Cluster 使用的协议和端口