Rust学习笔记之基础概念

5,207

要么我说了算,要么我什么也不说 -- 拿破仑

大家好,我是柒八九

今天,我们继续Rust学习笔记的探索。我们来谈谈关于基础概念的相关知识点。

如果,想了解该系列的文章,可以参考我们已经发布的文章。如下是往期文章。

文章list

  1. Rust学习笔记之Rust环境配置和入门指南

你能所学到的知识点

  1. 变量与可变性 推荐阅读指数 ⭐️⭐️⭐️⭐️⭐️
  2. 数据类型 推荐阅读指数 ⭐️⭐️⭐️⭐️⭐️
  3. Rust中函数 推荐阅读指数 ⭐️⭐️⭐️⭐️⭐️
  4. 流程控制 推荐阅读指数 ⭐️⭐️⭐️⭐️

好了,天不早了,干点正事哇。


变量与可变性

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

设计一个变量的可变性还需要考量许多因素。

  • 当你在使用某些重型数据结构时,适当地使用可变性去修改一个实例,可能比赋值和重新返回一个新分配的实例更有效率
  • 当数据结构较为轻量的时候,采用更偏向函数式的风格,通过创建新变量来进行赋值,可能会使代码更加易于理解。

变量和常量之间的不同

变量的不可变性可能会让你联想到另外一个常见的编程概念常量

但是,常量变量之间还存在着一些细微的差别

  1. 不能用mut关键字来修饰一个常量。
    • 常量不仅是默认不可变的,它还总是不可变
  2. 使用const关键字而不是let关键字来声明一个常量
    • 在声明的同时,必须显示地标注值的类型
  3. 常量可以被声明在任何作用域中,甚至包括全局作用域。
    • 这在一个值需要被不同部分的代码共同引用时十分有用
  4. 只能将常量绑定到一个常量表达式上,而无法将一个函数的返回值或其他需要在运行时计算的值绑定在常量上。

下面是声明常量的例子,数值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种基础的标量类型:

  1. 整数
  2. 浮点数
  3. 布尔值
  4. 字符

整数类型

整数是指那些没有小数部分的数字。在Rust中存在如下内建整数类型,每一个长度不同的值都存在有符号无符号两种变体。

长度有符号无符号
8-biti8u8
16-biti16u16
32-biti32Rust默认)u32
64-biti64u64
archisizeusize

每一个整数类型的变体都会标明自身是否存在符号,并且拥有一个明确的大小。有符号和无符号代表了一个整数类型是否拥有描述负数的能力

换句话说,

  • 对于有符号的整数类型来讲,数值需要一个符号来表示当前是否为正
    • 有符号数是通过二进制补码的形式进行存储的
  • 对于无符号的整数来讲,数值永远为正,不需要符号
  • 对于一个位数为n有符号整数类型,它可以存储从-(2n-1)到(2n-1-1)范围内的所有整数
  • 而对于无符号整数类型而言,则可以存储从0到(2n-1)范围内的所有整数

除了指明位数的类型,还有isizeusize两种特殊的整数类型,它们的长度取决于程序运行的目标平台。

  • 64位架构上,它们就是64位
  • 32位架构上,它们就是32位

Rust对于整数字面量的默认推导类型i32通常就是一个很好的选择:它在大部分情形下都是运算速度最快的那一个。

Rust发生整数溢出时候,会执行二进制补码环绕。也就是说,任何超出类型最大值的整数都会被环绕为类型最小值


浮点数类型

Rust还提供了两种基础的浮点数类型,浮点数也就是带小数点的数字。这两种类型是f32f64,它们分别占用了32位64位空间。

Rust中,默认会将浮点数字面量类型推导f64

Rust的浮点数使用了IEEE-754标准来进行表述,f32f64类型分别对应这标准中的单精度双精度浮点数


布尔类型

Rust的布尔类型只拥有两个可能的值truefalse,它只会占据单个字节的空间大小。使用bool来表示一个布尔类型。

fn main(){
  let t = true;
  
  let f:bool = false;
}

字符类型

Rust中,char类型被用于描述语言中最基础的单个字符

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

char类型使用单引号指定,字符串使用双引号指定。

Rustchar类型占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种循环

  1. loop
  2. while
  3. 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权威指南》

全文完,既然看到这里了,如果觉得不错,随手点个赞和“在看”吧。