所有权是 rust 语言独有的特性,它保证了在没有垃圾回收机制下的内存安全,所以理解 rust 的所有权是很有必要的。接下来,我们来讨论所有权和它的几个特性:借用、切片和内存结构。

什么是所有权

Rust 的核心特性是所有权。各种语言都有它们自己管理内存的方式,有些是使用垃圾回收机制,有些是手动管理内存,而 rust 使用的是所有权机制来管理内存。

所有权规则

所有权规则如下:

  • rust 中的每个值都有一个自己的变量。
  • rust 值在同一时间只能绑定一个变量。
  • 变量超出作用域,值会自动被销毁。

不懂没关系,跳过往后看。

变量作用域

rust 语言的变量作用域和其他语言是类似的,看例子:

{                      // 变量 s 还没有被声明,s 在这里是无效的
let s = "hello"; // 变量 s 是这里声明的,从这里开始生效 // 从这里开始,可以使用 s 做一些工作
} // 变量 s 超出作用域,s 从这里开始不再生效

可以总结两点重要特性:

  • 当变量 s 声明之后开始生效
  • 当变量 s 出了作用域失效

String 类型

在章节三中学习的数据类型都是存储在内存的栈空间中,当它们的作用域结束时清空栈空间,我们现在学习一下内存的堆空间中存储的数据是在何时被 rust 清空的。

我们在这里使用 String 类型作为例子,当然只是简单的使用,具体的内容后文介绍。

let s = "hello";

这个例子是把 hello 字符串硬编码到程序中,我们把它叫做 字符串文字 (string literals 我不知道别人是怎么翻译的,我实在想不到合适的词,先这样叫着吧),字符串文字很方便,但是它不能适用于任何场景,比如我们想要输入一个文本串的时候,原理如下:

  • 字符串文字是不可修改的
  • 编码(编译)时不确定文本串的内容 (比如保存用户的输入)

    在这种情况下,我们会使用字符串的第二种类型——String。它是存储在堆内存中的,而且允许在编译期间不知道字符串的大小,我们先使用 from 函数从字符串文字中创建一个 String 类型的字符串。
let s = String::from("hello");

这种双冒号的语法细节下个章节再说,这里先聚焦于字符串,例子中创建的字符串是可以改变的,比如:

let mut s = String::from("hello");

s.push_str(", world!"); // push_str() 在上个字符串后追加一个字符串

println!("{}", s); // 这里会打印 `hello, world!`

内存分配

字符串文字是在编译期间就有确定的字符内容,所以文本可以直接硬编码到程序中,这就是字符串文字快捷方便的原因。但是字符串文字又是不可变的,我们没办法分配内存给编译期间未知大小及变化的字符串。

字符串类型则支持字符串的修改和增长,即使编译期间未知大小,也可以在堆内存分配一块空间用于存储数据,这意味着:
* 内存必须是在运行时,从操作系统中请求 (和大多数编程语言类似,当我们调用 String::from 方法时,就完成了对内存的请求)
* 当不使用这块内存时,我们才会把它返还给操作系统 (和其它语言不同,其它语言是使用垃圾回收机制或手动释放内存)

rust 则是另一种方式:一旦变量超出作用域,程序自动返还内存,通过下面的例子来看这个概念:

{
let s = String::from("hello"); // s 从这里开始生效 // 利用 s 做一些事情
} // s 超出作用域,不再生效

当 s 超出作用域,rust 会自动帮我们调用一个特殊的函数—— drop 函数,它是用于返还内存的,当程序执行到 大括号右半块 } 的时候自动调用该函数。

变量和数据交互方式:移动

在 rust 中,可以使用不同的方式在多个变量间交互相同的数据,比如:

let x = 5; // 把 5 绑定到 x 上
let y = x; // 把 x 的值复制给 y,此时,x 和 y 的值都是 5

再比如:

let s1 = String::from("hello"); // 创建一个字符串绑定到 s1 上
let s2 = s1; // 把 s1 的值移动给 s2,此时,s1 就失效了,只有 s2 是有效的

s1 为什么失效,因为字符串是存储在堆内存中的,这里只是把栈内存中的 s1 的数据移动给 s2,堆内存不变,这种方式叫做浅克隆,也叫移动。如果想让 s1 仍然有效,可以使用深克隆。

变量和数据交互方式:克隆

关于深克隆,我们直接看例子吧:

let s1 = String::from("hello"); // 创建一个字符串绑定到 s1 上
let s2 = s1.clone(); // 把 s1 的值克隆给 s2,此时,s1 和 s2 都是有效的 println!("s1 = {}, s2 = {}", s1, s2); // 打印 s1 和 s2

下一章节再详细介绍这种语法。

只有栈数据:复制

我们再回到前面的例子中:

let x = 5; // 把 5 绑定到 x 上
let y = x; // 把 x 的值复制给 y,此时,x 和 y 的值都是 5 println!("x = {}, y = {}", x, y); // 打印 x 和 y

这里没有使用 clone 这个方法,但是 x 和 y 都是有效的。因为 x 和 y 都是整型,整型存储在栈内存中,即使调用了 clone 方法,也是做相同的事。下面总结一下复制的类型:

  • 所有的整型,像 u32,i32
  • 布尔类型,像 bool,值是 true, false
  • 所有的浮点型,像 f32,f64
  • 字符类型,像 char
  • 元组,但是仅仅是包含前 4 种类型的元组,像 (u32, i32),但是 (u32, String) 就不是了

所有权和函数

这里直接放一个例子应该就说清楚了,如下:

fn main() {
let s = String::from("hello"); // s 进入作用域 takes_ownership(s); // s 移动到函数里 // s 从这里开始不再生效,如果还使用 s,则会在编译期报错 let x = 5; // x 进入作用域 makes_copy(x); // x 复制(移动)到函数里,
// 但由于 x 是 i32 ,属于整型,仍有效
// 使用 x 做一些事情 } // x 超出作用域,由于 s 被移动到了函数中,这里不再释放 s 的内存 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 超出作用域,但是整型不需要释放堆内存

返回值和作用域

这块也直接放个例子,如下:

fn main() {
let s1 = gives_ownership(); // 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 { // gives_ownership 将移动它的返回值给调用者 let some_string = String::from("hello"); // some_string 进入作用域 some_string // some_string 是返回值,移出调用函数
} // takes_and_gives_back 移入一个字符串,移出一个字符串
fn takes_and_gives_back(a_string: String) -> String { // a_string 进入作用域 a_string // a_string 是返回值,移出调用函数
}

引用和借用

前面都是把值传入传出函数,这里我们学习一下引用,看个例子:

fn main() {
let s1 = String::from("hello"); // 创建一个字符串 s1 let len = calculate_length(&s1); // 把 字符串 s1 的引用传给函数 println!("The length of '{}' is {}.", s1, len); // 这里可以继续使用 s1
} fn calculate_length(s: &String) -> usize { // 接收到 字符串 s1 的引用 s
s.len() // 这里返回函数的长度
} // s 超出作用域,但是这里没有字符串的所有权,不释放内存

&s1 语法是创建一个指向 s1 的值的引用,而不是 s1 本身,当引用超出作用域不会释放引用指向值的内存。被调用函数声明参数的时候,参数的类型也需要使用 & 来告知函数接收的参数是个引用。

修改引用

在上述例子中,如果在 calculate_length 函数中修改字符串的内容,编译器会报错,因为传入的引用在默认情况下是不可变引用,如果想要修改引用的内容,需要添加关键字 mut,看例子:

fn main() {
let mut s = String::from("hello"); change(&mut s); // mut 同意函数修改 s 的值
} fn change(some_string: &mut String) { // mut 声明函数需要修改 some_string 的值
some_string.push_str(", world"); // 追加字符串
}

可变引用有一个很大的限制:在特定作用域中针对同一引用,只能有一个可变引用。比如:

let mut s = String::from("hello");

let r1 = &mut s; // 这是第一个借用的可变引用
let r2 = &mut s; // 这是第二个借用的可变引用,这里会编译不通过 println!("{}, {}", r1, r2);

这个限制的好处是编译器可以编译期间阻止数据竞争,数据竞争发生在如下情况:

  • 两个或多个指针同时访问相同的数据
  • 多个指针在写同一份数据
  • 没有同步数据的机制

数据竞争会造成不可预知的错误,而且在运行时修复是很困难的,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; // 不可变引用,没问题
let r2 = &s; // 不可变引用,没问题
let r3 = &mut s; // 可变引用,会发生大问题,因为后面还在使用 r1 和 r2 println!("{}, {}, and {}", r1, r2, r3);

当存在不可变引用时,就不能再借用可变引用了。不可变引用不会修改引用的值,所以可以借用多个不可变引用。但是如果不可变引用都不使用了,就又可以借用可变引用了,比如:

let mut s = String::from("hello");

let r1 = &s; // 不可变引用,没问题
let r2 = &s; // 不可变引用,没问题
println!("{} and {}", r1, r2);
// r1 和 r2 从这里开始不再使用了 let r3 = &mut s; // 可变引用,也没问题了,因为后面没使用 r1 和 r2 的了
println!("{}", r3);

悬空引用

在一些指针语言中,很容易就会错误地创建悬空指针。悬空指针就是过早地释放了指针指向的内存,也就是说,堆内存已经释放了,而指针还指向这块堆内存。在 rust 中,编译器可以保证不会产生悬空引用:如果有引用指向数据,编译器会确保引用指向的数据不会超出作用域,比如:

fn main() {
let reference_to_nothing = dangle();
} fn dangle() -> &String { // 这里希望返回字条串的引用
let s = String::from("hello"); // 创建字符串 &s // 这里返回了字符串的引用 } // 这里会把字符串的内存释放,因为 s 在这里超出作用域

这个函数做个简单的修改就好了,看如下例子:

fn no_dangle() -> String { // 不返回字符串引用了,直接返回字符串
let s = String::from("hello"); s // 返回字符串
}

引用规则

引用的规则如下:

  • 任何时候,只能有一个可变引用或多个不可变引用
  • 引用必须总是有效的

切片类型

另一个没有所有权的数据类型是切片。切片是集合中相邻的一系列元素,而不是整个集合。

做个小小的编程题目:有一个函数,输入一个英文字符串,返回第一个单词。如果字符串没有空格,则认为整个字符串是一个单词,返回整个字符串。

我们来思考一下这个函数结构:

fn first_word(s: &String) -> ?   // 这里应该返回什么

在这个函数中,字符串引用作参数,函数没有字符串的所有权,那我们应该返回什么呢?我们不能返回字符串的一部分,那么,我们可以返回第一个单词结束位置的索引。看实现:

fn first_word(s: &String) -> usize { // 返回一个无符号整型,因为索引不会小于 0

    let bytes = s.as_bytes(); // 把字符串转换化字节类型的数组

    // iter 方法用于遍历字节数组,enumerate 方法用于返回一个元组,元组的第 0 个元素是索引,第 1 个元素是字节数组的元素
for (i, &item) in bytes.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,但是我们不能得到单词 hello 了,这里使用 word,编译器也不会报错,但是这真的是一个 bug
}

还有一个问题,如果我们想得到第二个单词,应该怎么办?函数声明应该是:

fn second_word(s: &String) -> (usize, usize) {

如果想得到很多个单词,又应该怎么办?

字符串切片

字符串切片就是字符串一部分的引用,如下:

let s = String::from("hello world");

let hello = &s[0..5];
let world = &s[6..11];

就是取字符串 s 中的一部分组成一个新的变量,取值区间左闭右开,就是说,包括左边的索引,不包括右边的索引。

如果左边索引是0,可省略:

let s = String::from("hello");

let slice = &s[0..2];
let slice = &s[..2]; // 和上一行等价

如果右边索引是字符串末尾,可省略:

let s = String::from("hello");

let len = s.len();

let slice = &s[3..len];
let slice = &s[3..]; // 和上一行等价

如果取整个字符串,可以把两边都省略:

let s = String::from("hello");

let len = s.len();

let slice = &s[0..len];
let slice = &s[..]; // 和上一行等价

我们现在看一下一开始讨论的题目应该怎么做:

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 second_word(s: &String) -> &str {

我们再来看一下字符串清空的问题是否还存在:

fn main() {
let mut s = String::from("hello world"); let word = first_word(&s); // 不可变借用在这里声明 s.clear(); // 这里会报错,因为这里在修改 s 的内容 println!("the first word is: {}", word); // 不可变借用在这里使用
}

不可变借用的声明和使用之间是不能使用可变借用的。

字符串文字是切片

前面我们讨论过字符串文字的存储问题,现在我们学习了切片,我们可以理解字符串文字了:

let s = "Hello, world!";

s 的类型是 &str,这是一个指向了二进制程序特殊的位置的切片,这就是字符串文字不可变的原是,&str 是一个不可变引用

字符串切片作为参数

前面学习了字符串文字切片和字符串类型切片,我们来提高 first_word 函数的质量。有经验的 rust 开发者会把函数参数类型写成 &str,因为这样可以使得 &String 和 &str 使用相同的函数。好像不太好理解,直接上例子:

fn main() {
let my_string = String::from("hello world"); // 创建字符串 // first_word 使用 &String切片
let word = first_word(&my_string[..]); let my_string_literal = "hello world"; // 创建字符串 // first_word 使用 &str切片
let word = first_word(&my_string_literal[..]); // 因为字符串文字已经是字符串切片了,可以不使用切片语法
let word = first_word(my_string_literal);
}

其它切片

我们只举一个 i32 类型切片的例子吧:

let a = [1, 2, 3, 4, 5];

let slice = &a[1..3];  // 值是: [2, 3]

rust 使用了所有权、借用、切片,在编译期确保程序的内存安全。rust 语言提供了和其他编程语言相同的方式来控制内存,而不需要我们编写额外代码来手动管理内存,当数据超出所有者的作用域就会被自动清理。

欢迎阅读单鹏飞的学习笔记

Rust 入门 (四)的更多相关文章

  1. 【原创】NIO框架入门(四):Android与MINA2、Netty4的跨平台UDP双向通信实战

    概述 本文演示的是一个Android客户端程序,通过UDP协议与两个典型的NIO框架服务端,实现跨平台双向通信的完整Demo. 当前由于NIO框架的流行,使得开发大并发.高性能的互联网服务端成为可能. ...

  2. Rust入门篇 (1)

    Rust入门篇 声明: 本文是在参考 The Rust Programming Language 和 Rust官方教程 中文版 写的. 个人学习用 再PS. 目录这东东果然是必须的... 找个时间生成 ...

  3. python学习笔记--Django入门四 管理站点--二

    接上一节  python学习笔记--Django入门四 管理站点 设置字段可选 编辑Book模块在email字段上加上blank=True,指定email字段为可选,代码如下: class Autho ...

  4. Swift语法基础入门四(构造函数, 懒加载)

    Swift语法基础入门四(构造函数, 懒加载) 存储属性 具备存储功能, 和OC中普通属性一样 // Swfit要求我们在创建对象时必须给所有的属性初始化 // 如果没办法保证在构造方法中初始化属性, ...

  5. Thinkphp入门 四 —布局、缓存、系统变量 (48)

    原文:Thinkphp入门 四 -布局.缓存.系统变量 (48) [控制器操作方法参数设置] http://网址/index.php/控制器/操作方法 [页面跳转] [变量调节器] Smarty变量调 ...

  6. DevExpress XtraReports 入门四 创建 Web 报表

    原文:DevExpress XtraReports 入门四 创建 Web 报表 本文只是为了帮助初次接触或是需要DevExpress XtraReports报表的人群使用的,为了帮助更多的人不会像我这 ...

  7. 微服务(入门四):identityServer的简单使用(客户端授权)

    IdentityServer简介(摘自Identity官网) IdentityServer是将符合规范的OpenID Connect和OAuth 2.0端点添加到任意ASP.NET核心应用程序的中间件 ...

  8. Spring Boot入门(四):开发Web Api接口常用注解总结

    本系列博客记录自己学习Spring Boot的历程,如帮助到你,不胜荣幸,如有错误,欢迎指正! 在程序员的日常工作中,Web开发应该是占比很重的一部分,至少我工作以来,开发的系统基本都是Web端访问的 ...

  9. 脑残式网络编程入门(四):快速理解HTTP/2的服务器推送(Server Push)

    本文原作者阮一峰,作者博客:ruanyifeng.com. 1.前言 新一代HTTP/2 协议的主要目的是为了提高网页性能(有关HTTP/2的介绍,请见<从HTTP/0.9到HTTP/2:一文读 ...

随机推荐

  1. Solr入门(一)

    一丶Solr入门1.Solr的启动Solr各版本下载老版本的时候,需要将war包放到tomcat中,现在只需解压,由于自带jetty容器,可以直接启动 [root@aaa bin]# ./solr s ...

  2. 深入理解.NET Core的基元(二) - 共享框架

    原文:Deep-dive into .NET Core primitives, part 2: the shared framework 作者:Nate McMaster 译文:深入理解.NET Co ...

  3. 重磅!微软发布 Visual Studio Online:Web 版 VS Code + 云开发环境

    北京时间 2019 年 11 月 4 日,在 Microsoft Ignite 2019 大会上,微软正式发布了 Visual Studio Online (VS Online)公开预览版! 如今发布 ...

  4. Spring Cloud gateway 网关四 动态路由

    微服务当前这么火爆的程度,如果不能学会一种微服务框架技术.怎么能升职加薪,增加简历的筹码?spring cloud 和 Dubbo 需要单独学习.说没有时间?没有精力?要学俩个框架?而Spring C ...

  5. Alibaba 镜像

      <mirrors> <mirror> <id>alimaven</id> <name>aliyun maven</name> ...

  6. SROP的一个实例

    以前一直只是大概看过这种技术,没实践过,今天刚好遇到一道题,实践了一波,确实很方便 unmoxiao@cat ~/s/pd_ubuntu> r2 -A smallest 00:54:15 War ...

  7. [Usaco 2012 Feb]Cow coupons牛券:反悔型贪心

    Description Farmer  John  needs  new  cows! There  are  N  cows  for  sale (1 <= N <= 50,000), ...

  8. CSPS_103

    被sdfz踩爆了! %%%kai586123 %%%Gekoo %%%sdfz_yrt T1 我以为是水题!一直在肝! 而且为什么每次我的考场暴力都是考后才调出来啊!! 先记录一下正解的大神做法: 按 ...

  9. CPU负载和CPU使用率

    参考CSDN博客:https://blog.csdn.net/ffzhihua/article/details/87257607 一.概念(本人理解) CPU负载:平均负载(load average) ...

  10. 『题解』Codeforces1142B Lynyrd Skynyrd

    更好的阅读体验 Portal Portal1: Codeforces Portal2: Luogu Description Recently Lynyrd and Skynyrd went to a ...