Rust学习笔记2-Rust基础语法
概述
这篇文章介绍了 Rust 语言的基础语法规则,学会了这些语法内容,就可以使用 Rust 编写简单的程序了,文章内容包括 Rust 中最基础的程序结构、注释、基础数据类型、变量定义、函数定义、分支与循环结构。
一个简单的 Rust 程序代码
1 | fn main() { |
Rust 基础代码结构
Rust 是一种大小写敏感的编程语言。
Rust 代码语句使用分号 ; 结尾,代码块使用大括号 {} 包括。
在 Rust 中,使用 fn 关键字定义函数,使用 let 关键字定义变量。
Rust 中程序入口为 main 函数。
Rust 注释
单行注释
和大部分编程语言一样,Rust 使用双斜杠 // 添加单行注释,单行注释可以添加在独立行或行尾。
1 | // 定义变量 |
//会将标记处至后面第一个换行符的内容标识为注释。
多行注释
Rust 使用 /**/ 添加多行注释。
1 | /* |
注:上述程序将输出 abc
文档注释
Rust 中使用三斜杠 /// 添加文档注释,使用文档注释函数后将在 ide 中快捷看到函数的注释信息。
文档注释的内容可以使用 cargo doc 命令生成对应的 html 文档。
文档注释中可以使用 markdown 语法添加文档格式。
1 | fn main() { |
Rust 定义变量
不可变变量与可变变量
Rust 中使用 let 关键字定义变量,以此定义的变量是不可变的,即不能重新赋值。
如果要定义可变的变量,需要使用 let mut 关键字定义。
1 | fn main() { |
以上代码在编译时将报错,提示
cannot mutate immutable variable `a`,意思是不能修改不可变变量 a。
为什么 Rust 中的变量不可变?
在大量的编程实践中,人们发现一个规律,开发者常常会用变量存储一个不可改变的值,仅仅是为了将这个值存储起来,后面好用。这些用途上不可变的变量,往往极大地影响并发程序中对变量的使用,因此 Rust 默认状态下保护了所有的变量。
指定变量类型
Rust 是静态编译语言,在编译时必须知道所有变量的类型。
在 Rust 中,为变量赋值后,编译器会根据赋的值自动推断出变量的类型,如果不赋值则必须指明数据类型,否则编译将报错。
如果可能的类型比较多(如把 String 转成整数的 parse 方法),也必须添加类型标识,否则编译会报错,例如 let x: u32 = "200".parse().expect("Not a number")。
当我们需要指定数据类型时,可以在变量名后添加 : 和类型标识,来指定该变量的数据类型。
1 | let x = 1; // 自动推断类型为 i32 |
注:在给变量赋值时,右侧的具体值称为字面量,字面量值后面可以通过下划线
_加类型方式指定字面量的数据类型,如_u8指定值为u8类型。
Rust 默认将整形数字推断为 i32 类型,将浮点型数字推断为 f64 类型。
变量的隐藏(shadowing)
在 Rust 中,可以使用相同名字声明新的变量,新的变量就会 shadow(隐藏,或称为遮蔽、重影)之前声明的同名变量。
1 | let a = 100; // 首次声明变量 a |
隐藏是 Rust 中比较重要和特别的概念,在其他编程语言没有这种语法规则。
常量(constant)
Rust 中使用 const 定义常量,常量在声明时必须指定数据类型,常量赋值后不可变。
常量名使用全大写字母,多个字母间使用下划线 _ 分隔。
常量在其声明的作用域内一直有效。
1 | const PI: f64 = 3.14; |
常量与不可变变量的区别:
- 常量不可使用
mut,常量永远都是不可变的。- 声明常量使用
const关键字,必须指定数据类型。- 常量可以在任何作用域中声明,包括全局作用域,使用 let 声明的变量只能在函数中使用。
- 常量只能绑定到常量表达式,无法绑定到函数的调用结果或只能在运行时才能计算出的值。
静态变量(static)
静态变量可在函数外声明,被所有函数共享。
使用 static 关键字声明静态变量,声明时必须指定类型和初始值,如 static MAX_CNT:i32 = 100;。
默认静态变量是不可改变的,如果需要修改要使用 static mut 声明,如 static mut CURR_CNT:i32 = 0;。
Rust 认为修改静态变量值是 “不安全” 行为,要放在 unsafe 块或函数中。因为在多线程程序中,同时有多个线程修改静态变量值,会出现不可预测的情况。
1 | static MAX_CNT:i32 = 100; |
Rust 数据类型
字面量
字面量是指在程序中无需变量保存,用于表示固定的值,可直接表示为一个具体的数字或字符串的值,即数据在程序中的书写格式。字面量只能作为右值出现,所谓右值是指等号右边的值。
在 Rust 中,主要有四种字面量类型:整数类型、浮点类型、布尔类型、字符类型。
整数的字面值
整形字面量中间可以加 _ 增强数据可读性,十六进制以 0x 打头,八进制以 0o 打头,二进制以 0b 打头,字节类型以 b'' 表示。
除了 byte 类型,其他数值字面值都允许使用类型后缀,如 25u8 。
| 进制 | 示例 | 备注 |
|---|---|---|
| 十进制 | 10_086,255u8、100_000_086_i64 | |
| 十六进制 | 0xff,0xFFFF_FFFFu32 | |
| 八进制 | 0o177、0o77_i8、0o777_777_u32 | |
| 二进制 | 0b1111_0000 | |
| 字节(u8类型) | b’A’ | 仅限于 u8 类型 |
1 | let a = 10_086; |
浮点型字面量
浮点型也可以使用下划线 _ 增强数据可读性,也可以添加类型后缀。如
1 | let a = 1.0; |
布尔型字面量
Rust 中布尔类型字面量包括 true 和 false 。
字符型字面量
字符型字面量以单引号 '' 包括, 如 A、'你' 、'\t' 。
基础数据类型
整形(Integer)
整形即整数,Rust 中整形分为有符号整形(integer)和无符号整形(unsigned integer),有符号整形标识符以 i 打头,无符号整形标识以 u 打头,后面的数字表示数据占的位数(bit)。
Rust 中默认的整形类型是 i32 。
| 长度 | 有符号整形 | 无符号整形 |
|---|---|---|
| 8bit | i8 | u8 |
| 16bit | i16 | u16 |
| 32bit | i32 | u32 |
| 64bit | i64 | u64 |
| 128bit | i128 | u128 |
| arch | isize | usize |
注:
arch长度由运行程序的计算机系统架构确定,如在 32 位架构中长度为 32bit,在 64 位架构中长度为 64bit。
isize和usize的主要使用场景是对某种集合进行索引操作。
整数的溢出:当为整形变量赋值时,如果超出了该类型的边界,根据程序编译模式会发生以下两种情况:
- 调试模式下编译:Rust会检查整数溢出,如果发生溢出,程序在运行时就会 panic(恐慌,大致可理解成异常)。
- 发布模式下(
--release)编译:Rust 不会检查可能导致 panic 的整数溢出,如果发生溢出, Rust 会执行 “环绕” 操作,如为 u8 变量赋值 256 会变成 0,赋值 257 会变成 1。
浮点型(Floating-Point)
浮点型即小数,Rust 中浮点型有双精度浮点型 f64 和单精度浮点型 f32 ,长度分别为 64bit 和 32bit。
Rust 的浮点型使用 IEEE-754 标准来表述。
Rust 中默认的浮点型类型是 f64 ,在现代计算机处理器中对两种 浮点数的计算速度几乎相同,但 64 位浮点数精度更高。
布尔型(Boolean)
布尔值表示是或否两种状态,Rust 中布尔型为 bool ,占 1 个字节,值为 true 或 false 。
注意,Rust 中布尔值不能是数字,不能像某些编程语言中 0 表示 false,1 表示 true。
字符型(Char)
字符型表示单个字符,在 Rust 中字符型以 char 表示,单个字符是一个 Unicode 字符,长度为 4 个字节,这与其他编程语言有区别。
字符值以单引号包括,如果是特殊字符需要使用转义符 \ 进行转义,如单引号 '\'' 。
字符值可以是中文、藏文、日文、韩文、特殊字符、emoji 表情等合法 Unicode 字符。
1 | let a = '你'; |
字符串(String、&str)
Rust 中字符串有两种类型 String 和 &str ,一般情况下 &str 更加实用,因为它几乎具备 String 的所有常用功能。如果不需要把字符串当做一个可以编辑的数据对象时,建议优先选择使用 &str 数据类型。
Rust 中的字符串常量用双引号 "" 包含,这种数据的类型是字符串切片 &str ,如 "Hello world" 。
字符串类型(String)可以使用 from 方法从字符串常量获得,如 let string = String::from("This is String")。
Rust 中字符串可以灵活地追加字符或其他字符串,使用 push() 方法追加字符,使用 push_str() 方法追加字符串。
字符串变量使用 len() 方法获取字符串长度,使用 eq() 方法比较两个字符串。
1 | fn main() { |
元组(Tuple)
元组是一系列数据的组合,可以包含不同类型数据。元组的长度是固定的,一旦声明就无法改变。
Rust 中元组的值用小括号 () 包括,值之间以逗号 , 分隔;元组的类型定义以小括号 () 包括,元素类型以逗号 , 分隔。
取元组类型的值时,可以使用变量名加 . 索引方式取值,如 a.0 取第一个元素值。
可以使用模式匹配来解构(destructure)元组来获取元素的值。
元组的使用场景是存放按顺序存储的若干数据,而不用像结构体一样为每个数据起一个名字。另外当函数需要返回多个数据时,使用元组也很方便。
1 | fn main() { |
数组(Array)
数组是一组相同类型数据的组合结构,数组在声明后成员数量就固定不变了。
Rust 中数组值以中括号 [] 包括,各元素值之间使用逗号 , 分隔;数组定义使用中括号包含类型和长度 [类型; 数组容量] 表示,如定义一个 3个成员的 i32 类型数组,类型为 [i32; 3]。定义数组时,数组长度可以使用常量指定,作为数组长度的常量数据类型是 usize,不能使用 let 声明的变量。
如果数组成员值相同,可以使用 [值; 数组长度] 方式创建数组,如 let c = [3; 5] 相当于 let c = [3, 3, 3, 3, 3] 。
使用 [索引值] 获取数组中的值,如 a[1] 为数组中第二个值。如果访问的索引超出数组范围,编译会通过,运行时会报错。
1 | fn main() { |
注:数组的数据存放在 stack(栈)上,而不是 heap(堆)上。
Range
Rust 中的 Range 类型用于创建指定范围内的数字集合,创建时指定开始和结束数字,即可创建从开始值到结束值(不包含结束值)的范围数据, 语法为 let a: Range<i32> = 1..4;。
Range 提供了 rev() 方法,用来反转 Range ,方法返回一个迭代器,可用 for 循环遍历。
1 | use std::ops::Range; |
运算
基础数学运算
Rust 中基础数学运算包括加(+)、减(-)、乘(*)、除(/)、取模(%)。
Rust 数学运算支持在运算符后加等号 = 实现自运算,如 x += 1 。
Rust 中基础数学运算必须保证所有参与运算的值是相同类型,如果数据类型不同,将无法计算。不同类型的整形不能做计算,如 1 + 1_i8;整形和浮点型不能之间计算,如 1 + 1.1 。不同类型的数据需要进行类型转换后再参与运算,可以使用 as 关键字进行类型转换,如 1_i8 as f64。
Rust 中不支持 ++ 和 -- 运算,因为这两个运算符会影响代码可读性,开发者难以意识到值可能发生了改变。
1 | fn main() { |
数学运算函数
Rust 中整形和浮点型数据自含一些数学运算函数,如整形的平方、取绝对值,浮点型的三角函数计算、开方、对数计算等。
1 | fn main() { |
注意:三角函数运算时使用的是弧度。
逻辑运算
逻辑运算的结果是布尔型,逻辑运算包括逻辑判断和布尔值之间的逻辑运算。
| 逻辑运算名 | 逻辑运算符 | 示例 | 示例结果 |
|---|---|---|---|
| 大于 | > |
3>2 |
true |
| 小于 | < |
3<2 |
false |
| 等于 | == |
3==2 |
false |
| 大于或等于 | >= |
3>=2 |
true |
| 小于或等于 | <= |
3<=2 |
false |
| 不等于 | != |
3!=2 |
true |
| 与运算 | && |
true && false |
false |
| 或运算 | ` | ` | |
| 非运算 | ! |
!true |
false |
| 异或运算 | ^ |
true^false |
true |
位运算
位运算是指按二进制位,即bit位进行运算,常用位运算包括:与(&)、或(|)、取反(!)、异或(^)、左移(<<)、右移(>>)。
1 | fn main() { |
使用
println!()宏打印输出时,可以指定输出参数的编码格式,方便查看结果。如以上代码示例中的
{:08b}表示将参数值以二进制格式输出(:b),输出内容长度为 8 位,不足 8 位的前面补 0。
Rust 函数定义
在 Rust 中使用 fn 关键字定义函数,函数定义格式为:fn 函数名(参数1:参数1类型, 参数2:参数2类型,...)->返回值类型 {函数体},定义函数时可以不加参数,即定义无参函数,也可以不指定返回值类型。函数调用方式为 函数名(参数1, 参数2,...)。
Rust 中的变量名和函数名使用 snake case 命名规范,即全使用小写字母,单词间使用下划线分隔,如 say_hello。
Rust 函数不区分定义的先后顺序,不像 C 语言要先定义才能使用。
Rust 中函数体可包含一些列语句,可选由一个表达式结束,Rust 中大多数函数会以最后一个表达式的值作为函数的返回值,如果想提前返回可以使用 return 关键字。
语句是执行一些动作的指令,如函数定义或以分号结尾的一句代码。在 Rust 中不允许将语句赋值给一个变量,如
let a = (let b = 1)是不合法的。表达式可执行一些运算,如算式
3+2,最终得到一个值,所有字面值都是表达式,如5、"hello"。
Rust 中有函数对象(Function object)的概念,类似 C/C++ 中的函数指针,可以将函数当做变量或参数进行传递和使用,它使得动态标记函数变为可能。
1 | fn main() { |
闭包(Closure)
闭包、Lambda表达式、匿名函数,描述的是同一个概念,是一种快捷传递函数的方式,广泛用于异步编程。
Rust 中闭包的定义方式是 |参数1,参数2,...|->返回值类型{函数体} ,其中返回值类型支持自动推断,可以省略。
1 | fn main() { |
Rust 分支结构
if-else 分支
if-else 用于分支判断,Rust 中 if-else 条件语句的格式如下:
1 | if 判断条件 { |
Rust 中的判断条件表达式不需要使用小括号包含,代码语句必须使用大括号包含。
Rust 中条件表达式必须是布尔类型(bool),不能是数字。
1 | fn main() { |
三元表达式
在 Rust 中不支持三元表达式,但 Rust 中可以使用 if-else 来实现相同效果。
1 | let a = 65; |
match 语句
Rust 中没有 switch 语句,Rust 中的多分支判断使用 match ,match 每个分支结束时不用 break 跳出。
Rust 中的 match 判断项也不需要使用小括号包含,多个匹配项使用 | 分隔,每个匹配项后使用 =>{} 指定执行代码,多个匹配项之间使用逗号 , 分隔。
Rust 中 match 必须匹配所有可选项,在最后可以使用下划线 _ 匹配所有剩余项,类似其他编程语言中的 default 。
Rust 中 match 也可以返回值。
1 | fn main() { |
Rust 循环结构
loop 循环
Rust 中使用 loop 创建无限循环。
Rust 中的 loop 循环可以作为表达式,在 break 关键字后指定循环的返回值。
1 | fn main() { |
while 循环
Rust 中 while 循环与其他编程语言的 while 循环用法类似,先判断条件是否满足,条件为真则进入循环体,直到条件不满足退出循环,在循环体中可使用 continue 跳过本次循环,使用 break 跳出整个循环。
Rust 中条件表达式不使用小括号包含。
1 | fn main() { |
for 循环
Rust 中的 for 循环是 foreach 循环,是专门用来遍历可迭代对象的语法,用法是 for 变量名 in 迭代器 {函数体}。
迭代器是一类对象,如数组、Vector等,其实现了迭代器相关的方法。
1 | fn main() { |
参考资料
- 樊少冰, 孟祥莲. 《Rust编程从入门到实战》. 2022