基础入门
变量与常量
通过let
关键字声明变量,变量遵循先声明后使用的原则。
// 类型自动推断(整型字面量默认推断为i32类型)
let num = 18;
// 后跟冒号,显式指定变量的类型
let l :i16 = 2021;
注意,let
声明的变量默认是不可变的,在第一次赋值后不能通过再次赋值来改变它的值,即声明的变量是只读的。如需声明可变的变量,则需加mut
关键字,告诉编译器这个变量是可以重新赋值的:
let mut n = 10;
// 可重新赋值
n = 20;
Rust允许在同一个代码块中声明一个已存在的同名变量,这个新变量会遮蔽之前的变量,即无法再去访问前一个同名变量,这称为变量遮蔽。其实质是声明了一个新变量,只是名称恰巧与前一个变量名相同而已,但它们是两个完全不同的变量,处于不同的内存空间,值不同,值的类型也可以不同:
// 变量遮蔽
let a = 8;
let a = a + 1;
let a = a * 2;
println!("a is {}",a);
let a = "Hello,World!";
println!("a is {}",a)
Rust中使用const
关键字来声明常量。常量名通常是大写字母,且必须显式指定常量的数据类型:
// 声明常量
const MIN_VALUE:u8 = 0;
const PI : f32 = 3.1415;
小结,不可变变量与常量的区别:
- 变量遮蔽的方式可以模仿出不可变变量被重新赋值的形式(本质上是新的变量,只是变量同名而已)。但是,常量不能遮蔽,不能重复定义,常量一旦定义后就永远不可变更和重新赋值。
- 常量只能被赋值为常量表达式或数学表达式,不能是函数返回值,或是其他在运行时才能确定的值。
数据类型
标量类型是单个值类型的统称。Rust中内建了4种基础的标量类型:整数、浮点数、布尔值及字符。其中,isize
和usize
是两种特殊的整数类型,它们的长度取决于程序运行的目标平台。在64位架构上,它们就是64位的,而在32位架构上,它们就是32位的。注意,它们主要用作某些集合的索引。
Rust对于整数字面量的默认推导类型是i32
。除了整数,Rust还提供了两种基础的浮点数类型,分别是f32
和f64
,它们分别占用32位和64位空间。在Rust中,默认会将浮点数字面量的类型推导为f64
。
Rust也提供了相应的字符类型支持——char
。char
类型使用单引号指定,而不同于字符串使用的双引号。注意,Rust中的char
类型占4字节,是一个Unicode标量值,这也意味着它可以表示比ASCII多得多的字符内容。拼音字母、中文、日文、韩文、零长度空白字符,甚至是emoji表情都可以作为一个有效的char
类型值。
复合类型可以将多个不同类型的值组合为一个类型。Rust提供了两种内置的基础复合类型:元组(tuple)和数组(array)。
元组
元组是一种相当常见的复合类型,它可以将其他不同类型的多个值组合进一个复合类型中。元组还拥有一个固定的长度:你无法在声明结束后增加或减少其中的元素数量。
// 一个元组也被视作一个单独的复合元素
let tup:(i32,f64,u8) = (100,3.14,1);
// 解构元组
let (x,y,z) = tup;
除了解构,还可以通过使用点号.
加索引来访问元组中的值:
println!("{} {} {}",tup.0,tup.1,tup.2);
数组
与元组不同,数组中的每一个元素都必须是相同的类型。并且Rust中的数组是固定长度的,一旦声明就不能更改大小。
// 冒号后显式标注该数组的类型
let arr:[i32;5] = [1,2,3,4,5];
// 自动推断
let a = [1,3,5,7];
为了标注出数组的类型,你需要使用一对方括号,并在方括号中填写数组内所有元素的类型、一个分号及数组内元素的数量。
还有另一种初始化数组的语法:
let array = [9;3];
// 等价于 let array = [9, 9, 9];
// 分号前指定初始化数组的值,分号后指定数组长度
注意,数组由一整块分配在栈上的内存组成,你可以通过索引来访问一个数组中的所有元素。
切片
切片是一个没有所有权的数据类型,它允许你引用序列中一段连续的元素,而不用引用整个序列。Rust中的切片用法和其他其他语言类似:
fn main() {
let arr = [1, 3, 5, 7, 9,11];
// 对数组切片
let slice = &arr[1..4];
println!("{:?}",slice); // [3, 5, 7]
}
切片中的索引语法,参见Range
,我们可以对数组、字符串等序列类型进行切片。
结构体
结构体,是一种自定义数据类型,它允许我们命名多个相关的值并将它们组成一个有机的结合体。
// 定义一个结构体
struct User{
username:String,
email:String,
active:bool
}
// 创建一个实例
let user = User{
username:String::from("Bruce"),
email:String::from("xxx@163.com"),
active:true
};
当参数与结构体字段拥有完全一致的名称时,我们可以使用简化的结构体字段初始化语法:
// 结构体作为返回值
fn new_user(email:String,username:String)->User{
User{
email,
username,
active:true
}
}
元组结构体
除了上面的定义方式,还可以使用另外一种类似于元组的方式定义结构体,这种结构体也被称作元组结构体。元组结构体同样拥有表明自身含义的名称,但无须在声明它时对其字段进行命名,仅保留字段的类型即可。
// 定义元组结构体
struct Point(i32,i32,i32);
struct Color(i32,i32,i32);
// 实例化
let origin = Point(0,0,0);
let black = Color(0,0,0);
注意,这里的black
和origin
是不同的类型,因为它们两个分别是不同元组结构体的实例。我们定义的每一个结构体都拥有自己的类型,即便结构体中的字段拥有完全相同的类型。
Range
Rust中的所谓Range
表示一个左闭右开区间,是std::ops::Range
结构体的一个实例。区间中的值满足:start <= x < end
fn main() {
// 快速声明一个Range变量
let dozen = 0..9;
// 显式指定变量类型
let range1: std::ops::Range<usize> = 1..8;
// 从结构体实例化一个Range
let range2 = std::ops::Range{ start: 3, end: 5 };
println!("{:?},{},{},{}",dozen,dozen.start,dozen.end,dozen.len());
println!("{:?},{},{},{}",range1,range1.start,range1.end,range1.len());
println!("{:?},{},{},{}",range2,range2.start,range2.end,range2.len());
}
可以看到,..
其实是一个语法糖,我们可以直接实例化std::ops::Range
结构体获得一个Range对象。它实际上是一个步长为1的迭代器,常用于循环语句:
fn main() {
for i in 0..5 {
print!("{} ", i);
}
}
std::ops
中除了Range
之外,还有一些其他的表示区间的结构体:
RangeFrom
:只包括开始边界的区间 (start..
)RangeFull
:无边界区间 (..
)RangeInclusive
:包括开始和结束边界的区间 (start..=end
)RangeTo
:一个大于结束边界的区间 (..end
)RangeToInclusive
:只包括结束边界的区间 (..=end
)
// 等价于 r1 = std::ops::RangeFrom{ start:0 }
// 从0开始无限迭代
let r1 = 0..;
// 等价于 r2 = std::ops::RangeFull
// 无限迭代
let r2 = ..;
// 等价于 r3 = std::ops::RangeInclusive::new(0, 9)
// 全闭区间
let r3 = 0..=9;
// 等价于 r4 = std::ops::RangeTo{end: 5}
// 不能用于迭代,主要用于切片索引
let r4 = ..5;
// 等价于 r5 = std::ops::RangeToInclusive{ end: 8 }
// 同上,但包含end
let r5 = ..=8;
枚举
枚举类型是一个自定义数据类型,通过enum
关键字加自定义命名来定义。其包含若干枚举值,可以使用“枚举名::枚举值”访问枚举值:
// 定义枚举
enum Auth {
Enabled(i32),
Disabled(i32)
}
用值初始化枚举
#[derive(Debug)]
enum Auth {
Enabled(i32),
Disabled(i32)
}
fn main() {
let yes = Auth::Enabled(1);
let no = Auth::Disabled(0);
println!("yes: {:?}", yes);
println!("no: {:?}", no);
}
上例,在变量yes
和no
中,分别把1赋给Enabled
,0赋给Disabled
。我们不能直接打印枚举。需要实现std::fmt::Debug
,枚举上面的#[derive(Debug)]
语句会自动做这件事。
通用枚举Option
Option 枚举是Rust中预定义的通用枚举,允许枚举返回一个值。正因为如此,Option 枚举是一个泛型,这意味着它有一个类型的占位符。Option 是预定义的,只有两个值:
- Some,它返回一个值
- None,基本上是返回NULL
注意,Rust不支持关键字NULL
,所以None
被用来允许一个函数返回一个空值。
enum Option<T> {
Some(T), // 返回T的类型
None // 返回一个空值
}
在尖括号中,指定Option枚举可能返回的类型。 Some之后的(T)是我们指定要返回值的位置。
enum Option<bool> {
Some(true),
None
}
fn is_even(num:i32) -> Option<bool> {
if num % 2 == 0 {
return Some(true);
} else {
return None;
}
}
fn main() {
println!("{:?}", is_even(5));
println!("{:?}", is_even(88));
}
字符串
Rust中的字符串是极为复杂的,其中常用的字符串类型有两种:一种是语言核心字符串类型,即字符串切片str
,通常以借用形式&str
出现,用于表示固定长度的字符串字面量;另一种是可变长度的字符串对象String
。该类型被定义在了Rust标准库中而没有被内置在语言的核心部分,它本质是一个字段为Vec<u8>
类型的结构体。当在Rust中提到“字符串”时,通常指的是String
与字符串切片&str
这两种类型,而不仅仅只是其中的一种。
// 创建字符串&str类型变量
let greet:&str = "hello";
// 转换为String类型变量
let data:String = greet.to_string();
// 声明空String对象
let mut s1 = String::new();
s1.push_str("hi,Bob");
// 从给定字面量创建String对象
let s2= String::from("content");
注意,Rust字符串是基于UTF-8编码的,我们可以将任何合法的数据编码进字符串中。
此外,Rust还包含CStr
、CString
、OsStr
、OsString
等等类型字符串。
运算符
算术运算符:
运算符 | 描述 |
---|---|
+ |
加法 |
- |
减法 |
* |
乘法 |
/ |
除法 |
% |
取模 |
按位运算符:
运算符 | 描述 |
---|---|
& |
按位与 |
| |
按位或 |
^ |
按位异或 |
! |
按位非 |
<< |
左移 |
>> |
右移 |
比较运算符:
运算符 | 描述 |
---|---|
> |
大于 |
< |
小于 |
>= |
大于等于 |
<= |
小于等于 |
== |
等等于 |
!= |
不等于 |
赋值运算符:
运算符 | 描述 |
---|---|
= |
等于 |
+= |
加等于 |
-= |
减等于 |
*= |
乘等于 |
/= |
除等于 |
%= |
模等于 |
逻辑运算符:
运算符 | 描述 |
---|---|
&& |
逻辑与 |
|| |
逻辑或 |
! |
逻辑非 |
条件分支与循环
let age = 19;
// 复合if判断
if age > 18 {
println!("age > 18");
}else if age == 18 {
println!("age == 18");
}else {
println!("age < 18");
}
let is_birthday = true;
// 获取返回值
let message = if is_birthday {
"Happy birthday"
} else {
"No cake for you"
};
println!("{}", message);
// 由于Rust没有三目表达式语法,可以使用如下语法模拟
print("if execution") if 1==1 else print("else execution")
// 等价于以下代码
if 1==1:
print("if execution")
else:
print("else execution")
注意,在Rust中,match模式匹配也可用于流程控制,检查当前值是否匹配一系列模式中的某一个。模式可由字面值、变量、通配符和其他内容构成。每一个模式都是一个分支,程序根据匹配的模式执行相应的代码。
let num = 10;
match num {
0 => println!("num error!"),
1..=10 => println!("num in 1~10"),
11..=20 => println!("num in 11~20"),
21..=30 => println!("num in 21~30"),
_ => println!("No match!"),
}
let grade = "B";
// 获取返回值
let _result = match grade {
"A" => { println!("Fantastic, you got an A!"); },
"B" => { println!("Great job, you got a B!"); },
"C" => { println!("Good job, you got a C"); },
"D" => { println!("You got a D, you passed"); },
"F" => { println!("Sorry, you failed"); },
_ => { println!("Unknown grade, please see the teacher"); }
};
Rust没有switch
分支,但它的match模式匹配与之有些相似。需要注意,match模式匹配是穷尽式的,即必须穷举所有的可能性,否则会导致程序错误。switch
语法会使用default
分支来处理所有条件都匹配不上的情况,match模式匹配也有类似做法,将通配符_
放置在最后,那么通配符_
就会匹配之前没有指定的所有可能的模式。
Rust 有3种循环
loop
循环。这种循环没有循环条件,会无限次重复执行一段代码,直到调用break语句退出循环。break语句是循环控制语句,用于退出循环并将返回值返回while
循环。在每次执行代码之前进行条件判断,只要条件表达式的值为true
就会重复执行代码for
循环。使用for…in…
语法格式,是一种重复执行指定次数的循环。常用于对范围类型或集合类型的遍历。
let mut i = 0;
// 1. loop循环
loop {
if i > 10 {
break;
}
println!("loop: i = {}",i);
i += 1;
};
let mut i = 0;
// 2. while循环
while i < 5 {
println!("while: i = {}",i);
i += 1;
}
// 3. for循环
for j in 1..=5 {
println!("for: j = {}",j);
}
需要注意,break
关键字也可以返回一个值
let n = loop {
break 123;
};
println!("{}", n); // 输出“123”
可以使用循环标签,来退出嵌套循环
'outer: for x in 0.. {
for y in 0.. {
for z in 0.. {
if x + y + z > 1000 {
break 'outer;
}
// …
}
}
}
函数
Rust 代码使用蛇形命名法(snake case)来作为规范函数和变量名称的风格。蛇形命名法只使用小写的字母进行命名,并以下画线分隔单词。
Rust 中的函数体由若干条语句组成,并可以以一个表达式作为结尾。由于Rust是一门基于表达式的语言,所以它将语句(statement)与表达式(expression)区别为两个不同的概念。区别在于,语句指那些执行操作但不返回值的指令,而表达式则是指会进行计算并产生一个值作为结果的指令。
let x = 3;
let y = {
let x = 1;
x + 1
};
如上例,y
右边的代码块是一个表达式,它会计算一个结果并返回。需要注意x+1
后面是没有分号的,如果添加分号,则变为一个语句,不会有任何返回值。
在Rust 函数签名中,必须显式地声明每个形参的类型。
Rust中函数定义的格式:
fn 函数名(形参列表)->返回值元组{
}
示例
fn add(a:i32,b:i32)-> i32 {
return a + b;
}
fn sub(a:i32,b:i32)->i32 {
a - b
}
// 定义多个返回值的函数
fn sum(a:i32,b:i32,c:i32)->(i32,i32){
return (a + b + c,3);
}
println!("{}",add(10,8));
println!("{}",sub(10,8));
let r = sum(1,2,3);
println!("result is {},number is {}",r.0,r.1);
函数的类型
在Rust中,函数是被当作值来使用的,这就意味着它们也有自己的类型 :
fn add(a:i32,b:i32)-> i32 {
return a + b;
}
fn main() {
let f:fn(i32,i32)->i32 = add;
println!("{}",f(3,3));
}
如上,add
函数的类型就是fn(i32,i32)->i32
。Rust中的fn
值是函数在内存中的地址,它就相当于C语言中的函数指针。因此函数可以保存到一个变量中,也可以作为参数传递到另一个函数:
fn calc(f :fn(i32,i32)->i32){
println!("{}",f(3,3));
}
fn main() {
calc(add);
}
小结
- 函数体由一系列语句和一个可选的结尾表达式构成。如
add
函数,使用return
关键字返回计算结果;return
是可以省略的,如sub
函数,a - b
是一个表达式而非语句,函数会自动将表达式的值作为返回值。但要注意,表达式的结尾是没有分号的!如果加上分号,它就变成了语句,而语句没有返回值,因此会报错。 - Rust中每个函数都有返回值,即使是没有显式返回值的函数,也会隐式地返回一个空元组
()
。当我们需要返回多个值时,返回值列表应加上圆括号。 - 函数可以被当作值使用,保存到变量中。
错误处理
Rust将错误分为两个主要类别:可恢复错误和不可恢复错误。
可恢复错误是指可以被捕获的且能够合理解决的问题,比如读取不存在文件、权限被拒绝等情况。一旦捕捕获到可恢复错误,Rust可以通过选择一个备用的操作来矫正错误让程序继续运行。不可恢复错误是指会导致程序崩溃的错误,可视为程序的漏洞,比如数组的越界访问。一旦发生不可恢复错误,程序就会立即停止。
Rust提供了分层式错误处理方案:
Option<T>
:用于处理有值和无值的情况。比如在HashMap中指定一个键,但不存在对应的值,此时应返回None,开发者应该对None进行相应的处理Result<T, E>
:用于处理可恢复错误的情况Panic
:用于处理不可恢复错误的情况。如果在主线程中引发了Panic
,则会造成应用程序以非零退出码退出进程,即发生崩溃Abort
:用于处理会发生灾难性后果的情况。使用abort
函数可以将进程正常中止。
Option
主要用于消除了空指针问题。在枚举章节已经简单介绍过Option:
// unwrap_or 返回包装的值,若为None,则返回
assert_eq!(Some("Car").unwrap_or("Bike"), "Car");
Result
程序中的大部分错误并不需要停止程序,比如打开一个不存在的文件所产生的错误,可能更期望的是创建一个新文件,而不是中止程序。标准库提供的Result<T, E>
可用于处理这类可恢复错误,它使用枚举来封装正常返回的值和错误信息。
Result<T, E>
枚举包含两个值——Ok
和Err
。当值为Ok
时,泛型类型T作为调用成功返回的值的数据类型。当值为Err
时,泛型类型E作为调用失败返回的错误类型。
Result<T, E>
的常规处理方法是使用match
模式匹配:
use std::fs::File;
fn main(){
let f = File::open("test.txt");
let file = match f{
Ok(file) => file;
Err(error)=> {
panic!("{:?}",error);
}
}
}
match
虽然能对返回值进行处理,但是代码有些冗长。因此Result<T, E>
类型提供了unwrap
和expect
方法可以实现相似的功能:
use std::fs::File;
fn main(){
let f = File::open("test.txt").unwrap()
// let f = File::open("test.txt").expect("Failed to open!")
}
若Result的值是Ok,unwrap
方法会返回Ok
中的值。反之值是Err
,unwrap
方法会自动做Panic处理并输出默认的错误消息。expect
函数功能类似,但它可以自定义错误消息。
使用match
处理不同类型的错误:
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let f = File::open("hello.txt");
let f = match f {
Ok(file) => file,
Err(error) => match error.kind() {
ErrorKind::NotFound => match File::create("hello.txt") {
Ok(fc) => fc,
Err(e) => panic!("Problem creating the file: {:?}", e),
},
other_error => panic!("Problem opening the file: {:?}", other_error),
},
};
}
如上,大量嵌套match
匹配的代码很冗长,可以使用unwrap_or_else
方法处理,它可以执行一个闭包(语法详见闭包章节):
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let f = File::open("hello.txt").unwrap_or_else(|error|{
if error.kind() == ErrorKind::NotFound{
File::crate("hello.txt").unwrap_or_else(|error|{
panic!("Failed to create")
})
}else{
panic!("Failed to open")
}
});
}
传播错误
当编写的函数中包含可能会失败的操作时,除了在这个函数中处理错误外,还可以把错误交给该函数的调用者处理,这被称为传播错误。
use std::io;
use std::io::Read;
use std::fs::File;
fn read_username() -> Result<String, io::Error> {
let f = File::open("hello.txt");
let mut f = match f {
Ok(file) => file,
Err(e) => return Err(e),
};
let mut s = String::new();
match f.read_to_string(&mut s) {
Ok(_) => Ok(s),
Err(e) => Err(e),
}
}
传播错误是很常见的,Rust提供了?
操作符来简化代码。它与match
匹配有着类似的工作方式,若Result的值是Ok
,返回Ok
中的值并继续执行下面代码;若Result的值是Err
,则Err
中的值将作为整个函数的返回值传给调用者。
用?
操作符重写上例:
use std::io;
use std::io::Read;
use std::fs::File;
fn read_username() -> Result<String, io::Error> {
let mut f = File::open("hello.txt")?;
let mut s = String::new();
f.read_to_string(&mut s)?;
Ok(s)
}
在?
操作符之后,我们还可以使用链式方法调用进一步简化代码:
use std::io;
use std::io::Read;
use std::fs::File;
fn read_username() -> Result<String, io::Error> {
let mut s = String::new();
File::open("hello.txt")?.read_to_string(&mut s)?;
Ok(s)
}
Panic
某些bug或无法合理处理,会导致程序崩溃,对于这种情况,程序会自动调用panic
打印错误信息并清理栈数据后退出。
fn main() {
panic!("crash and burn");
}
捕获Panic
Rust提供了panic::catch_unwind
函数让开发者捕获Panic,以便程序可以继续执行而不被中止。需要注意,我们应避免滥用catch_unwind
方法,因为可能会导致内存不安全。
use std::panic
fn main(){
let vc = vec![1,2,3];
println!("{}",vc[0]);
let r = panic::catch_unwind(||{
println!("{}",v[9]);
});
assert!(result.is_err());
println!("{}",v[1]);
}
所有权体系
所有权机制的核心:
-
变量是值的所有者。每块内存空间都有其所有者,所有者具有这块内存空间的释放和读写权限。
-
每个值在任一时刻有且仅有一个所有者。
-
当所有者(变量)离开作用域,这个变量将被丢弃。
Rust使用let
关键字声明变量,这里的“变量”不是传统意义上的变量,本质上是一种绑定语义。将一个变量与一个值绑定在一起,这个变量就拥有了这个值的所有权。也就是将变量与存储这个值的内存空间绑定,从而让变量对这块内存空间拥有所有权。并且Rust确保对于每块内存空间都只有一个绑定变量与之对应,不允许有两个变量同时指向同一块内存空间。
一个变量的生命周期是指它从创建到销毁的整个过程。每个let关键字声明的变量都会创建一个默认的词法作用域,变量在作用域内有效。当变量离开作用域,它所绑定的资源就会被释放,变量也会随之无效并被销毁。
所有权转让
Rust一个值只允许有一个所有者,那么当我们把一个变量的值分配给另一个变量时会发生什么?
fn main() {
let a = String::from("Hello");
let b = a;
println!("A: {}", a);
println!("B: {}", b);
}
当我们将a赋值给b时,该值被移到了b上,而不是被复制。因为一个值只能有一个所有者,当我们试图访问a时,编译器将引发一个错误:
let a = String::from("Hello");
| - move occurs because `a` has type `std::string::String`, which does not implement the `Copy` trait
4 | let b = a;
| - value moved here
5 |
6 | println!("A: {}", a);
| ^ value borrowed here after move
上面的错误解释了String
类型不能被复制,所以值被移到了b上,即所有权从a转移到了b。
fn main() {
let a = 10;
let b = a;
println!("A: {}", a);
println!("B: {}", b);
}
看上例,当我们将a赋值给b时,它创建了一个值10的副本并将其赋值给a。它们并不共享值10,每个值都在内存中有自己的空间。当我们尝试用String
类型做同样的事情时,值被移动,而不是被复制。
一个值的所有权被转移给另外一个变量绑定的过程,叫作所有权转移。上例中,String
类型变量a
赋值给变量b
时,就发生了所有权转移。当发生所有权转移时,原来的变量a
就不能再被访问,这种类型被称为移动语义类型。相对的,如果一个类型实现了Copy,则称为复制语义类型。对于实现 Copy 的复制语义类型来说,所有权不会发生转移。上例中 let a = 10
,则变量a
是复制语义类型,将a
赋值给b
不会发生所有权转移,所以变量a
、b
都可以被访问。
注意,对于自定义类型(枚举、结构体等),默认是没有实现Copy的,因此属于移动语义类型,如果有必要,可以手动实现Copy,使之成为复制语义类型。但并不是所有的类型都可以实现Copy,Rust规定,对于自定义类型,只有所有成员都实现了Copy,这个类型才有资格实现Copy 。
小结:
- Rust的内存管理使用所有权和借用来代替运行时的垃圾收集,并在编译时做出内存安全保证
- 有三个所有权规则:
- 一个变量拥有一个值。
- 在任何时候,只允许有一个所有者
- 如果所有者超出了作用域,值就会丢失
- 复制语义类型(像
i32
这样的)是在内存中复制的。它们不会转移所有权 - 当值不支持复制语义时,将转移所有权
借用和引用
变量对其管理的内存拥有所有权。这个所有权不仅可以被转移,还可以被借用。
如果我们想在一个函数中使用一个值,而不把所有权转移给该函数,我们可以从其所有者那里暂时借用该值。当函数用完这个值后,它就会被还给所有者。借用允许我们在不破坏 "单一所有者 "概念的情况下对一个单一的值有一个或多个引用。
当我们借用一个值时,我们用&
操作符引用它的内存地址。Rust中的引用是一个地址,它被作为参数传递给一个函数。
引用(Reference)是一种语法(本质上是Rust提供的一种指针语义),而借用(Borrowing)是对引用行为的一种描述。引用分为不可变引用和可变引用,对应着不可变借用和可变借用。
如何引用和借用一个变量的值?
通过一个例子来看看这是如何工作的:
fn main() {
let a = String::from("Hello");
// 使用 & 符引用
let str_len = get_str_length(&a);
println!("String length: {}", str_len);
}
fn get_str_length(s:&String) -> usize {
s.len()
}
在上面的例子中,变量a
是字符串 "Hello "的所有者。如果我们在一个函数中使用这个变量,它将把所有权传递给函数。在这种情况下,我们不希望这样,所以解决方案是引用所有者a来代替。
在函数定义的参数列表中,我们在类型前添加了&参数,以告诉Rust,进来的值将是一个值的引用,而不是实际的值。这个函数只是借用了这个值,并在函数执行完毕后将其归还。当我们调用这个函数时,我们必须再次添加&运算符,这次是在我们传递的值前面添加。
如何改变一个借来的值?
借用值的作用几乎和普通变量一样,它是不可变的。但是,可以用mut
关键字使它可变。
fn main() {
let mut a = 5;
mutate_value(&mut a);
println!("{}", a);
}
fn mutate_value(num:&mut i32) {
*num = 3;
}
可变借用必须在变量初始化、函数定义(在参数列表中)和函数调用中添加mut
关键字。&
符号表示只读借用,&mut
符号则表示可读可写借用。
注意,可变借用需要原有变量自身使用关键字mut
进行修饰声明。如上例中,如果变量a
未被mut
修饰,则&mut a
操作是不允许的。
如何解引用(获取其值)?
当我们使用引用 &
操作符时,我们得到该值的内存地址。为了获得该地址的实际值,可以使用解引用操作符*
。
fn mutate_value(num:&mut i32) {
*num = 3;
}
在函数定义中,使用解引用运算符来获取参数的内存地址对应的值,并对其进行修改。
借用必须遵循三个规则:
- 借用的生命周期不能比出借方(拥有所有权的对象)的生命周期更长
- 可变借用
&mut
只能指向本身具有mut
修饰的变量,对于只读变量,不可以有&mut
借用 - 可变借用存在的时候,被借用的变量本身会处于“冻结”状态,因为可变借用具有独占性
借用只能临时地拥有对这个变量读或写的权限,没有义务管理这个变量的生命周期。因此,借用的生命周期绝对不能大于它所引用的原来变量的生命周期,否则就出现了悬空指针。对于第2、3条,可以概括为一句话:共享不可变,可变不共享。
不可变借用和可变借用就相当于内存的读写锁,同一时刻,只能拥有一个写锁,或者多个读锁,不能同时拥有。
生命周期标记
Rust 在编译的时候能够自动推断变量的生命周期之间是否合理覆盖。如果变量 a 需要引用变量 b 的数据,那么 b 的生命周期必须覆盖a。否则 b 释放了,a 就成了非法引用了。然而有时候编译器无法做出正确的生命周期推断,如下例:
fn get_str(s1: &str, s2: &str) -> &str {
if s2.len() > s1.len() {
s2
} else {
s1
}
}
fn main() {
let s1 = String::from("abc");
let s2 = String::from("de");
let r = get_str(&s1, &s2);
println!("get_str:{} ", r);
}
编译以上代码会产生报错,这是因为get_str
函数返回值的生命周期,可能与 s1
相同,也可能与 s2
相同。因此,该函数返回值的生命周期在编译阶段是无法推断的。于是,编译器报错提示我们添加相关标记说明,帮助编译器计算该函数返回值的生命周期。
为解决此问题,Rust 引入了生命周期标记,其实就是帮助编译器在函数内部,及函数调用两个地方进行生命周期分析。
生命周期标记的语法与泛型语法类似,标记则使用单引号开头,后跟一个合法的名称:
fn get_str<'a>(s1: &'a str, s2: &'a str) -> &'a str {
if s2.len() > s1.len() {
s2
} else {
s1
}
}
上例中,使用函数泛型的尖括号,声明了一个生命周期标记 'a
。参数变量 s1
、s2
指定了相同的生命周期标记 'a
。如果多个输入变量(函数参数)拥有相同的生命周期标记,那么最终生命周期取其交集。此处,'a
代表 s1
、s2
生命周期的交集。接着输出变量(函数返回值)的生命周期也指定了'a
。因此,函数的返回值的生命周期就是 s1
、s2
生命周期的交集。如此,编译器就能确定返回值的生命周期,再次编译报错解决。
小结:
-
生命周期标记并不能改变任何引用的生命周期长短,它只用于编译器的借用检查,来防止悬垂指针
-
生命周期标记参数位于引用符号
&
之后,并使用空格来分隔生命周期标记和类型 -
禁止在没有任何输入参数的情况下返回引用,因为会造成悬垂指针
// 编译会报错,因为s在离开函数作用域后就被释放了 fn get_str2<'a>()-> &'a str{ let s = String::from("rust"); &s }
-
从函数中返回一个引用,其生命周期必须与函数参数的生命周期相关联,否则,生命周期标记毫无意义
// 返回值的生命周期与参数并无关联 fn get_str3<'a>(s1: &'a str, s2: &'a str)-> &'a str{ let s = String::from("rust"); s.as_str() }
-
生命周期可以存在包含关系,比如
'a
可以包含'b
,标记为'a : 'b
// s1 包含 s2, s2 包含返回值 fn get_str<'a, 'b, 'c>(s1: &'a str, s2: &'b str) -> &'c str where 'a : 'b, 'b : 'c { if s2.len() > s1.len() { s2 } else { s1 } }
如果
'a : 'b, 'b : 'a
,这就表示 s1、s2 生命周期相同 -
全局变量的生命周期为
'static
,它是一个特殊的生命周期,比其他任何生命周期都长,对于任意生命周期'a
,都有'static : 'a
关注公众号:编程之路从0到1
了解更多技术干货