Rust学习笔记之基础概念

2023-03-23 19:47:57 浏览数 (1)

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

大家好,我是「柒八九」

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

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

文章list

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

你能所学到的知识点

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

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


变量与可变性

❝在Rust中变量「默认是不可变」的。❞

当一个变量是不可变的时,一旦它被绑定到某个值上面,这个值就再也无法被改变。

代码语言:javascript复制
fn main(){
  let x =7;
  x = 8;
}

保存并通过命令cargo run来运行代码,会提示如下错误:

这里提示我们cannot assign twice to immutable variable x(不能对不可变量x进行二次赋值)

变量默认是不可变的,但你可以通过在「声明的变量名称前」添加mut关键字来使其可变。

代码语言:javascript复制
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中,约定俗成地使用「下划线分隔的全大写字母来命令一个常量」

代码语言:javascript复制
fn main() {
    const MAX_AGE:u32 = 100;
}

遮蔽

Rust中,一个「新的声明变量可以覆盖掉旧的同名变量」,我们把这一个现象描述为:「第一个变量被第二个变量遮蔽Shadow了」。这意味着随后使用这个名称时,它指向的将会是第二个变量。

代码语言:javascript复制
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关键字会创建出「新的变量」,所以「可以在复用变量名称的同时改变它的类型」。 ❞

代码语言:javascript复制
fn main() {
   let spaces:&str = "abc";
   let spaces:usize= spaces.len();
}

第一个 spaces 变量是一个字符串类型,第二个 spaces 变量是一个数字类型


数据类型

Rust「每一个值都有其特定的数据类型」Rust会根据数据的类型来决定应该如何处理它们。

我们来介绍两种不同的数据类型子集:标量类型Scalar和复合类型Compound。

Rust是一门「静态类型语言」,这意味着它在「编译程序」的过程中需要知道所有变量的具体类型。 ❞

在大部分情况下,编译器都可以根据我们如何绑定、使用变量的值「自动推导」出变量的类型。但是,在某些时候,当发生数据类型的转换时候,就需要「显示」地添加一个类型标注。

下面的test变量是将String类型转换为数值类型

代码语言:javascript复制
let test:u32 = "42".parse().expect("非数值类型")

标量类型

标量类型「单个值」类型的统称。❞

Rust中内建了4种基础的标量类型:

  1. 整数
  2. 浮点数
  3. 布尔值
  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)范围内的「所有整数」

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

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

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

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


浮点数类型

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

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

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


布尔类型

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

代码语言:javascript复制
fn main(){
  let t = true;
  
  let f:bool = false;
}

字符类型

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

代码语言:javascript复制
fn main(){
  let c = 'a';
}

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

Rustchar类型「占4字节」,是一个Unicode标量值,这意味着它可以表示比ASCII多的字符内容。


复合类型

复合类型Compound可以「将多个不同类型的值组合为一个类型」。在Rust提供了两个「内置」的基础复合类型:元组Tuple和数组Array


元组类型

元组可以将其他「不同类型的多个值」组合进一个复合类型中。元组还拥有一个固定的长度:你「无法在声明结束后增加或减少其中的元素数量」

为了创建元组,需要把一系列的值使用「逗号分隔」后放置到一对「圆括号」中。元组「每个位置都有一个类型」,这些类型不需要是相同的。

代码语言:javascript复制
fn main(){
  let tup:(i32,f64,u8) = (500,7.8,1);
}

由于一个元组也被视为一个「单独的复合元素」,所以这里的变量tup被绑定到了整个元组上。为了从元组中获得单个的值,可以使用「模式匹配」来解构Destructuring元组

代码语言:javascript复制
fn main(){
  let tup:(i32,f64,u8) = (500,7.8,1);
  
  let (x,y,z) = tup;
}

除了解构,还可以通过「索引」并使用点号(.)来访问元组中的值。

代码语言:javascript复制
fn main(){
  let tup:(i32,f64,u8) = (500,7.8,1);
  
  let firstValue = x.0;
  let secondValue = x.1;
}

数组类型

我们同样可以在数组中存储多个值的集合。与元组不同,「数组中每一个元素都必须是相同类型」Rust「数组拥有固定的长度,一旦声明就再也不能随意更改大小」

代码语言:javascript复制
fn main(){
  let a = [1,2,3,4,5];
}

当然,Rust标准库也提供了一个更加灵活的动态数组Vector:它是一个类似于数组的集合结构,但它允许用户自由的调整数组的长度。这个我们后面的章节会有详细介绍。

为了写出数组的类型,你可以使用一对「方括号」,并在方括号中填写数组内所有元素的类型,「一个分号及数组内元素的数量」

代码语言:javascript复制
fn main(){
  let a:[i32;5] = [1,2,3,4,5];
}

另外还有一种更简便的初始化数组的方式。在「方括号中指定元素的值并接着填入一个分号及数组的长度」

代码语言:javascript复制
fn main(){
  let a =[3;5];
}

a命令的数组将会拥有5个元素,而这些元素全部拥有相同的「初始值」3


访问数组的元素

数组是「一整块分配在栈上的内存组成」,可以通过「索引」来访问一个数组中所有元素。

代码语言:javascript复制
fn main(){
  let a =[1,2,3,4,5];
  
  let frist = a[0];
  let second = a[1];
}

非法的数组元素访问

存在如下代码

代码语言:javascript复制
fn main() {
   let a = [1,2,3,4,5];
   let index = 10;
   let item = a[index];
}

使用cargo run运行这段代码,会发现程序顺利的通过「编译」,会在「运行时」因为错误而奔溃退出:

实际上,每次通过索引来访问一个元素时,Rust都会检查这个索引是否小于当前数组的长度。假如索引超出了当前数组的长度,Rust就会发生panic


函数

Rust代码使用蛇形命名法Snake Case 来作为规范函数和变量名称的风格。蛇形命名法「只使用小写的字母进行命名,并以下画线分隔单词」

代码语言:javascript复制
fn main() {
    another_function()
}

fn another_function(){
    println!("函数调用")
}

❝在Rust中,函数定义以fn关键字开始并紧随函数名称与一对圆括号,还有一对花括号用于标识函数体开始和结尾的地方。❞

可以使用函数名加圆括号的方式来调用函数。Rust不关心在何处定义函数,只要这些定义对于「使用区域」是可见的既可。


函数参数

还可以在函数声明中定义参数Argument,它们是一种「特殊的变量,并被视作函数签名的一部分」。当函数存在参数时,你需要在「调用函数时为这些变量提供具体的值」

代码语言:javascript复制
fn main() {
    another_function(5)
}

fn another_function(x:i32){
    println!("传入函数的变量为:{}",x)
}

❝在函数签名中,你「必须显示地声明每个参数的类型」。❞


函数体重的语句和表达式

函数体由若干语句组成,并可以「以一个表达式作为结尾」。由于Rust是一门「基于表达式」的语言,所以它将语句Statement和表达式Expression区别为两个不同的概念。

  • 「语句」指那些执行操作但不返回值的指令
  • 「表达式」是指会进行计算并产生一个值作为结果的指令

使用let关键字创建变量并绑定值时使用的指令是一条「语句」

代码语言:javascript复制
fn main(){
  let y = 6;
}

这里的函数定义同样是语句,甚至上面整个例子本身也是一条语句。

「语句」不会返回值❞

因此,在Rust中,不能将一条let语句赋值给另一个变量。

如下代码会产生「编译时」错误。

代码语言:javascript复制
fn main(){
  let x = (let y =6);
}

与语句不同,「表达式会计算出某个值来作为结果」。另外,表达式也可以作为语句的一部分。

  • 调用函数是表达式
  • 调用宏是表达式
  • 用创建新作用域的花括号({})同样也是表达式
代码语言:javascript复制
fn main(){
  let x =5;
  
  ①let y = {②
      let x =3;
    ③ x   1
  };
  
}

表达式②是一个代码块,它会计算出4作为结果。而这个结果会作为let语句①的一部分被绑定到变量y上。


函数的返回值

函数可以向调用它的代码返回值。需要在箭头符号(->)的后面声明它的类型。

❝在Rust中,「函数的返回值等同于函数体的最后一个表达式」。❞

  • 可以使用return关键字并指定一个值来提前从函数中返回
  • 但大多数函数都「隐式」地返回了最后的表达式
代码语言:javascript复制
fn five() ->i32{
    5
}
fn main() {
   let x = five();
   println!("子函数返回的值为:{}",x)
}

如上的代码中,five函数的返回值类型通过-> i32被指定了。five函数中的5就是函数的输出值,这也就是它的返回类型会被声明为i32的原因。


控制流

Rust中用来控制程序执行流的结构主要是if表达式循环表达式

if表达式

if表达式允许根据「条件执行不同的代码分支」

代码语言:javascript复制

fn main() {
  let number = 3;
  if number <5 {
    println!("满足条件")
  }else{
    println!("不满足条件")
  }

所有的if表达式都会使用if关键字来开头,并紧随一个「判断条件」。其后的花括号中放置了条件为真时需要执行的代码片段。if表达式中与条件相关联的代码块被称为分支Arm

❝条件表达式「必须」产生一个bool类型的值,否则会触发「编译错误」

Rust中不会「自动尝试」非布尔类型的值转换为布尔类型。必须「显示」地在if表达式中提供一个「布尔类型作为条件」


在let 语句中使用if

由于if是一个表达式,所以可以在let语句的「右侧」使用它来生成一个值。

代码语言:javascript复制

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反复执行某一块代码,直到「显示」地声明退出为止。

代码语言:javascript复制
fn main() {
  loop {
      println!("重复执行")
  }
}

运行这段程序时,除非「手动强制退出程序」,否则重复执行字样会被反复输出到屏幕中。


从loop循环中返回值

loop循环可以被用来反复尝试一些可能会失败的操作,有时候也需要将操作的结果传递给余下的代码。我们可以「将需要返回的值添加到break表达式后面」,也就是用来终止循环表达式后面。

代码语言:javascript复制
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就退出当前循环。

代码语言:javascript复制
fn main() {
    let mut counter = 3;
    
    while counter!=0{
        println!("{}",counter);
        counter = counter -1;
    }
}

使用for来循环遍历集合

代码语言:javascript复制
fn main() {
    let a = [1,2,3,4,5];
    for element in a.iter() {
        println!("当前的值为{}",element)
    }
}

for循环的安全性和简洁性使它成为Rust中最为常用的循环结构。


后记

「分享是一种态度」

参考资料:《Rust权威指南》

0 人点赞