要么我说了算,要么我什么也不说 -- 拿破仑
大家好,我是柒八九。
今天,我们继续Rust学习笔记的探索。我们来谈谈关于基础概念的相关知识点。
如果,想了解该系列的文章,可以参考我们已经发布的文章。如下是往期文章。
文章list
你能所学到的知识点
- 变量与可变性 推荐阅读指数 ⭐️⭐️⭐️⭐️⭐️
- 数据类型 推荐阅读指数 ⭐️⭐️⭐️⭐️⭐️
- Rust中函数 推荐阅读指数 ⭐️⭐️⭐️⭐️⭐️
- 流程控制 推荐阅读指数 ⭐️⭐️⭐️⭐️
好了,天不早了,干点正事哇。
变量与可变性
在
Rust
中变量默认是不可变的。
当一个变量是不可变的时,一旦它被绑定到某个值上面,这个值就再也无法被改变。
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
,可以将对这个值执行一系列的变换操作,并允许这个变量在操作完成后保持自己的不可变性。
遮蔽机制
与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]; } 复制代码
另外还有一种更简便的初始化数组的方式。在方括号中指定元素的值并接着填入一个分号及数组的长度。
fn main(){ let a =[3;5]; } 复制代码
以a
命令的数组将会拥有5
个元素,而这些元素全部拥有相同的初始值3
。
访问数组的元素
数组是一整块分配在栈上的内存组成,可以通过索引来访问一个数组中所有元素。
fn main(){ let a =[1,2,3,4,5]; let frist = a[0]; let second = a[1]; } 复制代码
非法的数组元素访问
存在如下代码
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},它们是一种特殊的变量,并被视作函数签名的一部分。当函数存在参数时,你需要在调用函数时为这些变量提供具体的值。
fn main() { another_function(5) } fn another_function(x:i32){ println!("传入函数的变量为:{}",x) } 复制代码
在
函数签名
中,你必须显示地声明每个参数的类型。
函数体重的语句和表达式
函数体由若干语句组成,并可以以一个表达式作为结尾。由于Rust
是一门基于表达式的语言,所以它将{语句|Statement}和{表达式|Expression}区别为两个不同的概念。
- 语句指那些
执行操作但不返回值
的指令 - 表达式是指会
进行计算并产生一个值作为结果
的指令
使用let
关键字创建变量并绑定值
时使用的指令是一条语句。
fn main(){ let y = 6; } 复制代码
这里的函数定义同样是语句,甚至上面整个例子本身也是一条语句。
语句不会返回值
因此,在Rust
中,不能将一条let
语句赋值给另一个变量。
如下代码会产生编译时错误。
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来循环遍历集合
fn main() { let a = [1,2,3,4,5]; for element in a.iter() { println!("当前的值为{}",element) } } 复制代码
for
循环的安全性和简洁性使它成为Rust
中最为常用的循环结构。
后记
分享是一种态度。
参考资料:《Rust权威指南》
全文完,既然看到这里了,如果觉得不错,随手点个赞和“在看”吧。