❝要么我说了算,要么我什么也不说 -- 拿破仑❞
大家好,我是「柒八九」。
今天,我们继续「Rust学习笔记」的探索。我们来谈谈关于「基础概念」的相关知识点。
如果,想了解该系列的文章,可以参考我们已经发布的文章。如下是往期文章。
文章list
- Rust学习笔记之Rust环境配置和入门指南
你能所学到的知识点
❝
- 变量与可变性 「推荐阅读指数」 ⭐️⭐️⭐️⭐️⭐️
- 数据类型 「推荐阅读指数」 ⭐️⭐️⭐️⭐️⭐️
- Rust中函数 「推荐阅读指数」 ⭐️⭐️⭐️⭐️⭐️
- 流程控制 「推荐阅读指数」 ⭐️⭐️⭐️⭐️
❞
好了,天不早了,干点正事哇。
变量与可变性
❝在
Rust
中变量「默认是不可变」的。❞
当一个变量是不可变的时,一旦它被绑定到某个值上面,这个值就再也无法被改变。
代码语言:javascript复制fn main(){
let x =7;
x = 8;
}
保存并通过命令cargo run
来运行代码,会提示如下错误:
这里提示我们cannot assign twice to immutable variable x
(不能对不可变量x
进行二次赋值)
变量默认是不可变的,但你可以通过在「声明的变量名称前」添加mut
关键字来使其可变。
fn main() {
let mut x =7;
println!("x的值是:{}",x);
x = 8;
println!("x的值是:{}",x);
}
保存并通过命令cargo run
来运行代码,输出结果如下:
x的值是 7
x的值是 8
❝设计一个变量的可变性还需要考量许多因素。
当你在使用某些「重型数据结构」时,适当地使用可变性去修改一个实例,可能比赋值和重新返回一个新分配的实例更有效率
当数据结构较为轻量的时候,采用更偏向「函数式」的风格,通过创建新变量来进行赋值,可能会使代码更加易于理解。❞
变量和常量之间的不同
变量的不可变性可能会让你联想到另外一个常见的编程概念:「常量」。
但是,「常量」和「变量」之间还存在着一些细微的差别
❝
- 不能用
mut
关键字来修饰一个常量。- 常量「不仅是默认不可变的,它还总是不可变」的
- 使用
const
关键字而不是let
关键字来声明一个常量- 在声明的同时,「必须显示地标注值的类型」
- 常量可以被声明在任何作用域中,甚至包括全局作用域。
- 这在一个值需要「被不同部分的代码共同引用」时十分有用
- 「只能将常量绑定到一个常量表达式上」,而无法将一个函数的返回值或其他需要在运行时计算的值绑定在常量上。❞
下面是声明常量的例子,数值100
被绑定到了常量MAX_AGE
上。在Rust
中,约定俗成地使用「下划线分隔的全大写字母来命令一个常量」
fn main() {
const MAX_AGE:u32 = 100;
}
遮蔽
在Rust
中,一个「新的声明变量可以覆盖掉旧的同名变量」,我们把这一个现象描述为:「第一个变量被第二个变量遮蔽Shadow了」。这意味着随后使用这个名称时,它指向的将会是第二个变量。
fn main() {
let x =5;
let x = x 1;
let x = x * 2;
println!("x的值为:{}",x)
}
- 这段程序首先将
x
绑定到值为5
上。 - 随后它又通过重复
let x =
语句遮蔽
了第一个x
变量,并将第一个x
变量值加上1
的运行结果「绑定到新的变量」x
上,此时x
的值是6
。 - 第三个
let
语句同样遮蔽
了第二个x
变量,并将第二个x
变量值乘以2
的结果12
绑定到第三个x
变量上。
通过使用let
,可以将对这个值执行一系列的「变换操作」,并允许这个变量在操作完成后保持自己的不可变性。
代码语言:javascript复制❝
遮蔽机制
与mut
的一个区别在于:由于重复使用let
关键字会创建出「新的变量」,所以「可以在复用变量名称的同时改变它的类型」。 ❞
fn main() {
let spaces:&str = "abc";
let spaces:usize= spaces.len();
}
第一个 spaces
变量是一个字符串类型
,第二个 spaces
变量是一个数字类型
。
数据类型
Rust
中「每一个值都有其特定的数据类型」,Rust
会根据数据的类型来决定应该如何处理它们。
我们来介绍两种不同的数据类型子集
:标量类型Scalar和复合类型Compound。
❝
Rust
是一门「静态类型语言」,这意味着它在「编译程序」的过程中需要知道所有变量的具体类型。 ❞
在大部分情况下,编译器都可以根据我们如何绑定、使用变量的值
来「自动推导」出变量的类型。但是,在某些时候,当发生数据类型的转换
时候,就需要「显示」地添加一个类型标注。
下面的test
变量是将String
类型转换为数值类型
。
let test:u32 = "42".parse().expect("非数值类型")
标量类型
❝
标量类型
是「单个值」类型的统称。❞
在Rust
中内建了4
种基础的标量类型:
- 整数
- 浮点数
- 布尔值
- 字符
整数类型
整数
是指那些「没有小数部分的数字」。在Rust
中存在如下内建
整数类型,每一个长度不同的值都存在「有符号」和「无符号」两种变体。
长度 | 有符号 | 无符号 |
---|---|---|
8-bit | i8 | u8 |
16-bit | i16 | u16 |
32-bit | i32(Rust默认) | u32 |
64-bit | i64 | u64 |
arch | isize | usize |
每一个整数类型的变体
都会标明自身是否存在符号,并且拥有一个明确的大小。「有符号和无符号代表了一个整数类型是否拥有描述负数的能力」。
换句话说,
- 对于「有符号」的整数类型来讲,数值需要一个符号来表示当前是否为正
- 有符号数是通过「二进制补码」的形式进行存储的
- 对于「无符号」的整数来讲,「数值永远为正」,不需要符号
❝
- 对于一个位数为
n
的有符号整数类型
,它可以存储从-(2n-1)到(2n-1-1)范围内的「所有整数」。 - 而对于
无符号整数类型
而言,则可以存储从0
到(2n-1)范围内的「所有整数」。
❞
除了指明位数的类型,还有isize
和usize
两种特殊的整数类型,它们的长度取决于程序运行的目标平台。
- 在
64位架构
上,它们就是64位
的 - 在
32位架构
上,它们就是32位
的
Rust
对于整数字面量的「默认推导类型」i32
通常就是一个很好的选择:它在大部分情形下都是运算速度最快的那一个。
❝当
Rust
发生整数溢出时候,会执行「二进制补码环绕」。也就是说,「任何超出类型最大值的整数都会被环绕为类型最小值」。❞
浮点数类型
Rust
还提供了两种基础的浮点数类型,「浮点数也就是带小数点的数字」。这两种类型是f32
和f64
,它们分别占用了32位
和64位
空间。
❝在
Rust
中,默认会将浮点数字面量
的类型推导
为f64
。❞
Rust
的浮点数使用了IEEE-754
标准来进行表述,f32
和f64
类型分别对应这标准中的「单精度」和「双精度浮点数」。
布尔类型
Rust
的布尔类型只拥有两个可能的值true
和false
,它「只会占据单个字节的空间大小」。使用bool
来表示一个布尔类型。
fn main(){
let t = true;
let f:bool = false;
}
字符类型
在Rust
中,char
类型被用于描述语言中最基础的「单个字符」。
fn main(){
let c = 'a';
}
❝
char类型
使用「单引号」指定,字符串
使用「双引号」指定。❞
在Rust
中char
类型「占4字节」,是一个Unicode
标量值,这意味着它可以表示比ASCII
多的字符内容。
复合类型
复合类型Compound可以「将多个不同类型的值组合为一个类型」。在Rust
提供了两个「内置」的基础复合类型:元组Tuple和数组Array
元组类型
元组可以将其他「不同类型的多个值」组合进一个复合类型
中。元组还拥有一个固定的长度
:你「无法在声明结束后增加或减少其中的元素数量」。
为了创建元组
,需要把一系列的值使用「逗号分隔」后放置到一对「圆括号」中。元组「每个位置都有一个类型」,这些类型不需要是相同的。
fn main(){
let tup:(i32,f64,u8) = (500,7.8,1);
}
由于一个元组也被视为一个「单独的复合元素」,所以这里的变量tup
被绑定到了整个元组上。为了从元组中获得单个的值,可以使用「模式匹配」来解构Destructuring元组
fn main(){
let tup:(i32,f64,u8) = (500,7.8,1);
let (x,y,z) = tup;
}
除了解构,还可以通过「索引」并使用点号(.
)来访问元组中的值。
fn main(){
let tup:(i32,f64,u8) = (500,7.8,1);
let firstValue = x.0;
let secondValue = x.1;
}
数组类型
我们同样可以在数组中存储多个值的集合。与元组不同,「数组中每一个元素都必须是相同类型」。 Rust
中「数组拥有固定的长度,一旦声明就再也不能随意更改大小」。
fn main(){
let a = [1,2,3,4,5];
}
当然,Rust
标准库也提供了一个更加灵活的动态数组Vector:它是一个类似于数组的集合结构
,但它允许用户自由的调整数组的长度
。这个我们后面的章节会有详细介绍。
为了写出数组的类型,你可以使用一对「方括号」,并在方括号
中填写数组内所有元素的类型,「一个分号及数组内元素的数量」。
fn main(){
let a:[i32;5] = [1,2,3,4,5];
}
另外还有一种更简便的初始化数组的方式。在「方括号中指定元素的值并接着填入一个分号及数组的长度」。
代码语言:javascript复制fn main(){
let a =[3;5];
}
以a
命令的数组将会拥有5
个元素,而这些元素全部拥有相同的「初始值」3
。
访问数组的元素
数组是「一整块分配在栈上的内存组成」,可以通过「索引」来访问一个数组中所有元素。
代码语言:javascript复制fn main(){
let a =[1,2,3,4,5];
let frist = a[0];
let second = a[1];
}
非法的数组元素访问
存在如下代码
代码语言:javascript复制fn main() {
let a = [1,2,3,4,5];
let index = 10;
let item = a[index];
}
使用cargo run
运行这段代码,会发现程序顺利的通过「编译」,会在「运行时」因为错误而奔溃退出:
实际上,每次通过索引来访问一个元素时,Rust
都会检查这个索引是否小于当前数组的长度。假如索引超出了当前数组的长度,Rust
就会发生panic
。
函数
Rust
代码使用蛇形命名法Snake Case 来作为规范函数和变量名称的风格。蛇形命名法「只使用小写的字母进行命名,并以下画线分隔单词」。
fn main() {
another_function()
}
fn another_function(){
println!("函数调用")
}
❝在
Rust
中,函数定义以fn
关键字开始并紧随函数名称与一对圆括号,还有一对花括号用于标识函数体开始和结尾的地方。❞
可以使用函数名加圆括号的方式来调用函数。Rust
不关心在何处定义函数,只要这些定义对于「使用区域」是可见的既可。
函数参数
还可以在函数声明中定义参数Argument,它们是一种「特殊的变量,并被视作函数签名的一部分」。当函数存在参数时,你需要在「调用函数时为这些变量提供具体的值」。
代码语言:javascript复制fn main() {
another_function(5)
}
fn another_function(x:i32){
println!("传入函数的变量为:{}",x)
}
❝在
函数签名
中,你「必须显示地声明每个参数的类型」。❞
函数体重的语句和表达式
函数体由若干语句组成,并可以「以一个表达式作为结尾」。由于Rust
是一门「基于表达式」的语言,所以它将语句Statement和表达式Expression区别为两个不同的概念。
- 「语句」指那些
执行操作但不返回值
的指令 - 「表达式」是指会
进行计算并产生一个值作为结果
的指令
使用let
关键字创建变量并绑定值
时使用的指令是一条「语句」。
fn main(){
let y = 6;
}
这里的函数定义同样是语句,甚至上面整个例子本身也是一条语句。
❝「语句」不会返回值❞
因此,在Rust
中,不能将一条let
语句赋值给另一个变量。
如下代码会产生「编译时」错误。
代码语言:javascript复制fn main(){
let x = (let y =6);
}
与语句不同,「表达式会计算出某个值来作为结果」。另外,表达式也可以作为语句的一部分。
调用函数
是表达式调用宏
是表达式- 用创建新作用域的花括号(
{}
)同样也是表达式
fn main(){
let x =5;
①let y = {②
let x =3;
③ x 1
};
}
表达式②是一个代码块,它会计算出4作为结果。而这个结果会作为let
语句①的一部分被绑定到变量y
上。
函数的返回值
函数可以向调用它的代码返回值。需要在箭头符号
(->
)的后面声明它的类型。
❝在
Rust
中,「函数的返回值等同于函数体的最后一个表达式」。❞
- 可以使用
return
关键字并指定一个值来提前从函数中返回 - 但大多数函数都「隐式」地返回了最后的表达式
fn five() ->i32{
5
}
fn main() {
let x = five();
println!("子函数返回的值为:{}",x)
}
如上的代码中,five
函数的返回值类型通过-> i32
被指定了。five
函数中的5
就是函数的输出值,这也就是它的返回类型会被声明为i32
的原因。
控制流
在Rust
中用来控制程序执行流
的结构主要是if表达式
和循环表达式
。
if表达式
if表达式
允许根据「条件执行不同的代码分支」。
fn main() {
let number = 3;
if number <5 {
println!("满足条件")
}else{
println!("不满足条件")
}
所有的if表达式
都会使用if
关键字来开头,并紧随一个「判断条件」。其后的花括号
中放置了条件为真时需要执行的代码片段。if
表达式中与条件相关联的代码块被称为分支Arm
❝条件表达式「必须」产生一个
bool
类型的值,否则会触发「编译错误」❞
在Rust
中不会「自动尝试」将非布尔类型的值转换为布尔类型
。必须「显示」地在if表达式
中提供一个「布尔类型作为条件」。
在let 语句中使用if
由于if
是一个表达式
,所以可以在let
语句的「右侧」使用它来生成一个值。
fn main() {
let condition = true;
let number = if condition {
5
} else {
6
};
println!("number的值为:{}",number)
}
❝代码块输出的值就是其中「最后一个表达式的值」。另外,
数字本身也可以作为一个表达式使用
。❞
上面的例子中,整个if表达式
的值取决于究竟哪一个代码块得到执行。
❝「所有」
if分支
可能返回的值都「必须是一种类型」。❞
使用循环重复执行代码
Rust
提供了多种循环Loop工具。一个循环会执行循环体中的代码直到结尾,并紧接着回到开头继续执行。
Rust
提供了3种循环
loop
while
for
使用loop重复执行代码
可以使用loop
关键字来指示Rust
反复执行某一块代码,直到「显示」地声明退出为止。
fn main() {
loop {
println!("重复执行")
}
}
运行这段程序时,除非「手动强制退出程序」,否则重复执行
字样会被反复输出到屏幕中。
从loop循环中返回值
loop
循环可以被用来反复尝试一些可能会失败的操作,有时候也需要将操作的结果传递给余下的代码。我们可以「将需要返回的值添加到break
表达式后面」,也就是用来终止循环表达式后面。
fn main() {
let mut counter = 0;
let result = loop {
counter =1;
if counter ==10 {
break counter *2;
}
};
println!("result的值为:{}",result)
}
上面的代码中,当counter
值为10
时候,就会走break
语句,返回counter *2
。并将对应的值返回给result
。
while 条件循环
另外一种常见的循环模式是「在每次执行循环体之前都判断一次条件」,假如条件为真则执行代码片段,假如条件为假或执行过程中碰到break
就退出当前循环。
fn main() {
let mut counter = 3;
while counter!=0{
println!("{}",counter);
counter = counter -1;
}
}
使用for来循环遍历集合
代码语言:javascript复制fn main() {
let a = [1,2,3,4,5];
for element in a.iter() {
println!("当前的值为{}",element)
}
}
for
循环的安全性和简洁性使它成为Rust
中最为常用的循环结构。
后记
「分享是一种态度」。
参考资料:《Rust权威指南》