INTERACTIVE LAB

rust33 个可运行示例1 次浏览

Rust 基础语法详解

系统讲解 Rust 的变量、数据类型、函数、注释与控制流,并配有可编辑运行的代码实验。

变量

基础语法是一门语言的根,虽然简单,但学习它却是必要的。

首先我们要学习的便是变量,如果你有其它语言基础,相信还是比较好理解的。

电脑很重要的一个性能指标便是内存大小,程序运行时确实需要使用内存。

而变量可以理解为一个名称与值之间的绑定。值可能存放在栈、堆中,也可能被编译器直接优化掉,并不是简单地申请一块固定的内存。更详细的介绍可以参考文章:常量与变量。

Rust中声明变量与其它大多数语言不太一样,它提供了两种变量的声明方式:不可变变量、可变变量。

虽然听起来很奇怪,名字明明都是变量了,怎么还能不可变呢?

当深入学习了解Rust之后,大概能理解Rust这样做的目的:显式地限制变量的修改,可以减少意外改变状态的情况,让代码更容易阅读和维护,也便于编译器进行分析和优化。

比如一个从来不会进行任何修改的变量,读代码的人就可以确定它的值不会在后面发生变化。至于它最终是否会被优化成常量,则由编译器根据具体情况决定。

首先是不可变变量,使用关键字let:

rust
待运行⌘↵
编辑仅保存在本页,刷新后恢复

它的特点便是不可再赋值,如果你想要更改它的值,就会直接报错:

image-20231207210327756

vscode中安装了插件后,当你写出错误的代码时,就会直接给你标红。

如果你此时编译运行它,编译器同样会非常精确的给你标注出来你错在了哪里:

image-20231207210427764

会指出哪个文件、第几行、第几列,以及错处的原因,上图中的错误信息便是不能给不可变变量赋值两次。

与不可变变量相对的就是可变变量,使用方法如下:

rust
待运行⌘↵
编辑仅保存在本页,刷新后恢复

只需要在let关键字后面添加一个mut关键字,就可以声明一个可变的变量了。

除了上面两个变量外,Rust中也有常量,也就是不可更改的,需要用到const关键字:

rust
待运行⌘↵
编辑仅保存在本页,刷新后恢复

可以很明显地看到,常量与不可变变量还是有些区别的:

  1. 常量的名称后面必须标注类型,而且按照Rust的命名习惯,常量名一般使用全大写字母和下划线。
  2. const后面不可以添加mut关键字使它可变。
  3. 常量的值必须是编译期可以计算出来的值。

所以总的来说,Rust中声明变量和常量的方式非常简单,主要会用到三个关键字:let、mut、const。

除了这个,对于变量你还需要注意一点,Rust中的变量支持隐藏,比如下面这段代码:

rust
待运行⌘↵
编辑仅保存在本页,刷新后恢复

乍一看,似乎就是让变量a等于两个不同类型的值:数字、字符。

但数字与字符的类型并不相同,对于Rust这种编译型语言来说,一个绑定不能直接被赋予两个不同类型的值。

之所以可以这样写,是因为Rust支持变量隐藏,也叫变量遮蔽。也就是当你后面再用let关键字使用相同的变量名时,前面那个绑定就被隐藏了。

比如这里的a,一旦有了第二句,那么在第二句之后再使用a这个变量,使用到的就是'c'。前一个绑定仍然存在于它的作用域中,但是否继续占用内存由它的生命周期和编译器决定。你只能在这两句的中间使用到数字10:

rust
待运行⌘↵
编辑仅保存在本页,刷新后恢复

这里用到了打印宏println,先记住这个用法即可,后面会再对它进行详细介绍。

这个特性方便了我们这些取名困难户,可以给很多变量取一样的名称,但要注意,下面这样写就是错误的:

fn main() {
    let mut a=10;
    a='c';
}

因为这就不是声明一个新的绑定来存值,而是想要给原本的变量重新赋值。由于两者类型不同,所以必定会失败。

数据类型

上面我们提到了,变量需要绑定一个值,而不同的值有不同的数据类型。

类型不仅决定了值可以进行哪些操作,也会影响它的表示方式和所需的存储空间。

Rust中的数据类型分为两种:标量(scalar)和复合(compound)

标量

首先我们来看看标量,所谓标量,代表的就是单个值,Rust中标量分为四种:整型、浮点型、布尔类型和字符类型。

我们先来看看整型:

长度 有符号 无符号
8-bit i8 u8
16-bit i16 u16
32-bit i32 u32
64-bit i64 u64
128-bit i128 u128
arch isize usize

前面的长度一列,代表这个类型使用多少位来表示,8-bit就是8位,也就是1字节。

而后面的有符号与无符号列,通俗来说就是可以表示负数的整数,以及不能表示负数的整数。

这里的i代表int(integer),即整数,后面的数字则代表它使用的位数。

而u则代表unsigned,即无符号,通俗来说就是不能存负数,但可以存0和正数,后面的数字同样代表它使用的位数。

一般我们最常用的就是i32,即使用32位,也就是4字节来表示一个整数,并且既可以存储正数,也可以存储负数。

至于最后的一行arch,isize与usize,则代表它们的大小取决于当前目标平台的指针宽度。常见的32位目标上,它们分别等同于i32与u32;常见的64位目标上,则分别等同于i64与u64。

正如我们所看见的,即使我们不注明类型,它也能为我们推导出来:

image-20231207210729255

这是因为在没有其它信息时,100这个整数数字面量通常会推导为i32类型,所以编译器就可以自动推导出这个变量的类型。

而一旦不给它赋值,又不注明它的类型,编译器就不知道这个变量需要多大的内存,所以就会直接报错:

image-20231207210758852

这时我们就可以手动给它注明类型,只要编译器知道它是什么类型了,就不会报错:

image-20231207210814482

说完整型,下面就到浮点型了,整型说白了就是整数,而浮点型说白了就是带小数的数字,只有两种:

长度 类型
32-bit f32
64-bit f64

唯一需要注意的是,在没有其它类型信息时,浮点数字面量通常会推导为f64类型,比如:

image-20231207210915490

如果不手动指定,它就会自动推导出f64,紧跟着的便是布尔型,它只有两个可选值:true与false

image-20231207211748217

如果你赋值除了true与false之外的值,它将会直接报错。

在C/C++中只要不为0就是true,这也是两种语言的区别,相比较来看,Rust这样对其加以限定可以更加的安全,但对应的也减少了自由度。

布尔类型与前面两种类型不同,前面两种类型主要用于计算,而布尔类型主要用于判断,比如后面将要介绍的if判断语句,就必须要用到布尔类型。

最后,就是字符类型了,它主要用于表示一个字符,使用起来也非常的简单:

image-20231207211911120

在Rust中,无论是字母、汉字、甚至是表情,一切能用Unicode表示的字符都可以使用,对编码更深入的介绍可以参考文章:编码。

注意:Rust中的char类型固定使用4字节(32-bit)表示,并且字符是用单引号表示,双引号指代的是字符串。

复合类型

所谓复合类型,就是将多个上面提到的标量组合成的一种类型。

而Rust有两个原生的复合类型:元组(tuple)和数组(array)。

首先来看元组,它可以将多个不同的数据类型放在一起组成一个新的类型,且一旦声明,它的长度就不可增加或减少。

使用方法如下:

rust
待运行⌘↵
编辑仅保存在本页,刷新后恢复

比如上面这个代码,就直接将前面提到的四种标量放在一起,组成了一个元组。

注意:元组是以()表示的,里面放入各个元素,且各个元素之间用,分隔。

由于rust可以自动推导出其类型,所以这里我们无需手动为其标注类型:

image-20231207212134535 如果想要手动标注,就可以根据这个自动推导的格式进行标注即可。

我们想要取出里面的值也很简单,使用.加数字即可,就像下面这样:

rust
待运行⌘↵
编辑仅保存在本页,刷新后恢复

注意下标是从0开始的,所以4个元素就只能为0到3。

这里用到了println!宏进行打印输出,其第一个参数为格式化字符串,字符串中的{}叫做占位符。

意思就是将后面紧跟的参数,挨个放入到这些大括号的位置,然后进行输出,后面会再对它进行更加细致的讲解。

得到的输出结果就是:

100 余 true 3.14

如果元组里面没有任何元素,则称它为单元(unit)元组,直接写作(),代表没有有意义的值。

除了元组外,还有数组也很常用,它的使用方法与元组还是有点差距的,其中最大的差别就是,数组中的所有元素的类型必须相同,而上面提到的元组各个元素类型可以不同。

使用方法如下:

rust
待运行⌘↵
编辑仅保存在本页,刷新后恢复

注意它的长度同样是不可变的,想要使用可以动态增长的集合,后面会介绍Vec,这里先略过。

它的声明使用[],访问其中的元素同样是用的[]中间添加下标的方式进行访问,而且也是从0开始的。

如果你想要手动更改元素类型,可以像这里自动推导一样,手写自己想要的类型即可:

image-20231207212441563

比如改为[i64; 8],前面的为数据类型,后面的为元素个数。

除此之外,如果你想要赋值一个数组同样的值,那就可以像下面这样写:

rust
待运行⌘↵
编辑仅保存在本页,刷新后恢复

这代表我要申请一个数组有5个元素,每个元素都等于0。

函数

下面要介绍的便是Rust中的函数了,其实在介绍函数前,我们就已经使用过函数了,最常见的main入口函数,就是一个函数。

对函数更深入宏观的介绍可以参考文章:函数。

函数的目的就是可以让我们重复的使用一些代码,比如每声明一个变量,我就要打印它三次,没有函数时,就得这样写:

rust
待运行⌘↵
编辑仅保存在本页,刷新后恢复

明显打印的这个步骤有些重复了,所以我们就可以将其封装为一个函数,比如下面这样:

rust
待运行⌘↵
编辑仅保存在本页,刷新后恢复

通过将打印的这个步骤封装成为一个函数,那我们就可以方便的调用它来执行任务了。

如果学过C/C++可能会觉得这样写有点奇怪,因为调用函数的地方居然在函数定义的前面,但在Rust中不存在前后关系,它们都同处于一个作用域。

一个最简单的函数长下面这样:

rust
待运行⌘↵
编辑仅保存在本页,刷新后恢复

通过关键字fn、函数名字,()与{}组成,除此之外什么都没有,同样,它也不能做任何事。

为了能让这个函数干一些事情,我们就需要在函数体,也就是{}中写一些代码:

rust
待运行⌘↵
编辑仅保存在本页,刷新后恢复

这时,它就可以执行一些任务了。

但只是这样还不够,在没有任何输入的情况下,这个函数能做的事情基本就写死了:无论任何地方调用它,其结果都是一样的(调用随机数除外)。

所以我们就需要函数参数,也就是从函数外部传入的变量,可以让函数内部来使用,参数写在()中。

rust
待运行⌘↵
编辑仅保存在本页,刷新后恢复

函数的参数可以有任意多个,唯一需要注意的是,各个参数都必须在其后注明类型,并且多个参数之间要用,分隔。按照Rust的命名习惯,函数名和参数名一般使用蛇形命名法。

如果不注明,编译器也无法推断它是什么类型,从而不知道它要占用多大的内存,由此编译失败。

调用它也很简单,写上函数名称、并在()中顺序填写对应的参数就行了

rust
待运行⌘↵
编辑仅保存在本页,刷新后恢复

但仅仅只是这样仍旧不够,比如我想用函数计算两个数字之和,函数计算完成后,得把结果送回来呀!

这就用到了函数的返回值:

rust
待运行⌘↵
编辑仅保存在本页,刷新后恢复

只需要在()与{}之间,用 ->指明这个返回值的类型即可,因为这里要返回数字和,所以用的i32类型。

然后在函数内部,可以通过return关键字,或者直接把最后一个表达式作为返回值,返回运算结果。Rust中更常见的是直接使用最后一个表达式:

当你调用这个函数时,就可以用一个变量来接收它的返回值并输出:

rust
待运行⌘↵
编辑仅保存在本页,刷新后恢复

这就是Rust中最简单的函数用法,但事实上,Rust中的函数还有很多高级特性,这个就留在后面再讲了。

注释

任何语言应该都支持注释,其作用在于解释代码,毕竟很多项目并不是一个人就能完成的,你写的代码别人不一定能看懂。

同样的,你现在写的代码,几个月后你自己也未必能看懂,这时候你就能体会到注释的好处了。

普通注释是给人看的,编译器不会管;相反,编译器会在内部首先清理掉这些注释才会开始编译代码。文档注释则会被Rust处理成文档属性。

但Rust中的注释比其它大部分语言的注释都还要更高级一点,因为它除了正常的注释外,还支持文档注释。

所谓文档注释,就是你在代码中写的注释,cargo可以一键帮你生成文档。通常,///用于给函数、结构体等项目写文档,//!则用于给当前模块或整个crate写文档。

首先来看普通注释:

rust
待运行⌘↵
编辑仅保存在本页,刷新后恢复

一个普通的注释就是这么的简单,只需要将你想要解释的内容,放在 //之后就可以了,而//可以放在文件中的任何位置。

如果你想要写多行注释,那就可以用多个 //

rust
待运行⌘↵
编辑仅保存在本页,刷新后恢复

但这些写比较麻烦,所以我们还可以用 /* ... */

rust
待运行⌘↵
编辑仅保存在本页,刷新后恢复

除了普通的注释,我们还可以写文档注释,用于生成对应的文档。

比如上面介绍函数时,就写了一个求和函数sum,我们就可以为这个函数写文档注释:

rust
待运行⌘↵
编辑仅保存在本页,刷新后恢复

注意文档注释是用的///,直接写在函数的上方即可,并且这里的注释是Markdown格式,比如这里的#代表这是个一级标题,而代码前后的三个反引号,就代表中间写的是代码。

然后我们回到终端运行命令:

cargo doc --open

该文档注释就会被编译,并且自动从电脑默认浏览器打开一个帮助文档:

image-20231207215405232

这里就可以看到我们的函数sum的注释了,然后点进去,你就能看到完整的注释:

image-20231207215426195

上面这种方式是给指定的函数写注释并生成文档的方式。

但有时我们还需要为当前整个库写注释,比如算法库,里面有求和函数、求最大值函数等等。

为了给这个整体库写注释,我们需要用到//!,还是上面的那个例子,为当前程序写个注释:

rust
待运行⌘↵
编辑仅保存在本页,刷新后恢复

然后运行命令 cargo doc --open

image-20231207215502496

就可以看到当前整个库都有注释了。

注意我这里虽然都说的是库,但实际上Rust给编译单元定义了一个名词:crate

翻译过来就是箱子的意思,即装各种东西的箱子。

控制流

控制流,说白了就是控制程序运行流程的,比如通过前面的代码,你应该已经建立起了这样的一个认知:

程序是从main函数的函数体开始,从上到下,遇到一句代码就执行一句代码,遇到一个函数,就跳到函数所在的位置执行完再返回原处,然后继续向下执行。

但很多时候我们并不希望这样,比如我们要判断用户输入的一个数字,如果大于0,我们就执行大于0的语句,小于0,我们就要执行小于0的语句。

这就不能直接顺序执行下来了。

还有前面曾经用过打印语句,只打印了三次还好,但如果要打三百次、三千次呢?我们同样不能手写三千条相同的语句吧!

所以这时候我们就需要改变程序原本顺序执行的流程,首先就是判断语句,例子如下:

rust
待运行⌘↵
编辑仅保存在本页,刷新后恢复

判断语句用到的就是if关键字,后面紧跟要判断的布尔类型,如果为true,则执行紧跟其后{}中的内容;如果为false,则执行关键字else后面{}中的内容。

这样就实现了在不同情况下执行不同语句的目的。

当然,else以及紧跟其后的语句是可以省略的,这样的效果就是只要if后面的表达式不为true,那就什么都不执行。

或者我们也可以增加判断分支数量:

rust
待运行⌘↵
编辑仅保存在本页,刷新后恢复

增加的方式就是在if语句的后面、else语句的前面,增加else if语句,并紧跟需要判断的语句即可。

基本的比较运算符我就不再多讲了,比如这里的 a>0 a==0就是两个比较运算符,分别代表大于、等于,如果确实满足,那么这个运算符就会返回true,否则就会返回false。

不仅仅只是rust,在大部分编程语言中,判断是否等于都是两个=,一个=叫做赋值。

总的来说判断语句还是很简单的,下面我们再来介绍一下Rust中的循环语句,也就是如何能用简短的几句代码,重复执行某些代码无数次。

Rust中的循环语句共有三个:for、while以及loop。

为了简便,我们从loop开始说起,它的使用方法非常简单:

rust
待运行⌘↵
编辑仅保存在本页,刷新后恢复

只需要一个loop关键字后面跟着{},而{}中就是我们想要循环的语句。在线运行时用break演示执行三次后退出。

如果没有break,这个语句就是无限循环,程序会一直打印数字10。

此时可以在终端按Ctrl+C快捷键强制终止运行。

比如在第一章中我提到过,当你点击运行其编译的可执行文件,只会闪过一道黑影,原因就是程序很快执行结束、退出导致的。

而当你在代码的最后一行写入上面这样一个死循环,就意味着程序永远不会退出,此时当你编译它、然后点击运行,就能看到黑窗口了。

如果想要让它停下来,我们还需要用到另一个关键字break,即跳出当前循环的意思:

rust
待运行⌘↵
编辑仅保存在本页,刷新后恢复

上面这段代码的意思就是,每打印一次,我就让a的数字加一,如果等于20了,我就调用break,跳出当前循环,然后程序结束。

但这样还是有点麻烦,每次都需要我自己判断是否跳出,这时候就有了while语句:

rust
待运行⌘↵
编辑仅保存在本页,刷新后恢复

while语句与loop的区别就在于,在while与{}之间,可以填写一个布尔表达式。

我这里填写的是 a!=20,意思就是,只要a不等于20,就会一直循环后面的语句。

但由于我在循环体内部每次都加了一个1,所以当a==20的时候,while后面的布尔表达式为false,然后就自动跳出了循环,不需要我们自己调用break。

当然,如果你想要调break,仍然是可以调用的。

但如果每次遍历一系列的数字都这样写,我们可能仍然还是会觉得麻烦,这时候就有了for语句:

rust
待运行⌘↵
编辑仅保存在本页,刷新后恢复

for循环中有两个关键字,for与in,其中in后面的就是要遍历的对象,通过语法 10..20就可以得到一个从10开始、到20之前结束的范围,并将其中的值依次赋值给for后面的变量i。

然后我们就可以在循环体中使用这个变量名i,一旦遍历完成,就会自动退出。

注意范围运算符..生成的范围对象是左闭右开的,具体来说,10..20中i只会等于10到19,也就是[10,20)。

前期知道它是如何用的就行了,后面有机会再对它介绍。

当你使用一个变量等于它时,也会自动为你推导出它的类型:

image-20231207220143235

除了基本的遍历数字外,它还可以遍历数组,但不能直接遍历元组:

rust
待运行⌘↵
编辑仅保存在本页,刷新后恢复

这里的for i in arr会取得数组元素的所有权。如果后面还要继续使用数组,可以遍历它的引用:

rust
待运行⌘↵
编辑仅保存在本页,刷新后恢复

当然,只要是循环,就支持使用break关键字,for循环也不例外,这里不再过多赘述。

事实上,除却break关键字外,还有一个continue关键字,它不像break会直接跳出当前循环,它仅仅只是跳过当下这一次的循环。

比如,我想要输出0到10之间的奇数:

rust
待运行⌘↵
编辑仅保存在本页,刷新后恢复

%是取余符号,如果一个数字除以2余0,那就是偶数,就调用continue跳过当前循环,不执行后面的打印函数。

否则就是奇数,不执行continue,也就是不跳过当前循环,执行后面的打印语句。

这里介绍的是最基础的语法,Rust中还有很多更加高级的用法,就留到后面章节再介绍了。

总结

Rust中常用let、mut、const来声明变量与常量。

如果为let,代表变量不可变,如果用let mut,则代表变量可变,如果为const,则代表这是一个常量,必须标注数据类型,并且其后不可跟mut使之可变。

然后是数据类型,分为两类:标量与复合类型。

其中标量有四个类型:整型、浮点型、布尔类型和字符类型。

而复合类型只有两个:元组、数组。

其中如果直接给变量赋值的话,可以省略类型注解,因为编译器可以直接推导出来。

但如果你想要手动指定、或者不想赋值,那就可以自己手写类型注解,方法就是在变量名后面添加一个:然后写上类型就行了。

比如:

rust
待运行⌘↵
编辑仅保存在本页,刷新后恢复

注意元组与数组最大的区别有两点:

  1. 元组可以存放不同类型的元素,而数组只能存放同种类型的元素
  2. 元组元素通过.数字索引的方式访问,而数组则使用[数字下标]

相同点就是两者的长度一旦声明了,就不可以再更改了。

然后是函数,用关键字fn声明,既有参数,又有返回值的函数长下面这样:

rust
待运行⌘↵
编辑仅保存在本页,刷新后恢复

然后是注释,Rust中注释主要分为三种:

  1. //或/* ... */:普通注释,一般用于注释特定的几行代码
  2. ///:文档注释,一般用于注释函数,使用的是Markdown格式,可以直接在生成的文档中查看。
  3. //!:文档注释,一般用于注释当前这个crate(箱子,或者理解为库),同样可以在生成的文档中查看。

生成文档的命令为:cargo doc。

生成并在浏览器中打开文档的命令为:cargo doc --open。

最后还有控制流,分为判断语句、循环语句两种

  • 判断语句:可通过if、else if 、else建立分支语句,根据不同情况来执行不同的语句。
  • 循环语句:有loop、while、for,本质都是不断循环一段语句,但其能力是从左到右逐渐增强的。