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