Rust基础(通用编程概念)

1. 变量

1. 不可变量(默认)

Rust的变量默认是不可变的,Rust这一设计的目的是为了可以方便的写出复杂的甚至是并行的代码。当然Rust的变量也不是一直不变的,Rust也提供了可以修改变量的方法。

接下来会用一段简单的代码去证书Rust的变量默认是不可变的特性,用Cargo new一个叫variables的项目

cargo new variables
vim variables/src/main.rs
fn main() {
    let x = 5;
    println!("The value of x is: {}",x);
    x = 6;
    println!("The value of x is: {}",x);
}
cd variables/src
cargo run

在 variables/src 目录下在终端执行 cargo run命令,会得到下面的结果

error[E0384]: cannot assign twice to immutable variable `x`
 --> src/main.rs:4:5
  |
2 |     let x = 5;
  |         -
  |         |
  |         first assignment to `x`
  |         help: make this binding mutable: `mut x`
3 |     println!("The value of x is: {}",x);
4 |     x = 6;
  |     ^^^^^ cannot assign twice to immutable variable

error: aborting due to previous error

For more information about this error, try `rustc --explain E0384`.
error: could not compile `rust_for_test`.

To learn more, run the command again with --verbose. 

编译器给出了一个错误 "cannot assign twice to immutable variable x"意思是不能对不可变比变量进行二次赋值,代码中x = 6;这句话就对变量进行了二次赋值。(其实在还没有执行cargo run的命令的时候IDE的Rust的插件在x = 6;已经用红线下滑标注错误了,我使用的是vscode)或许对于其他的语言,如Java,golang,python等等这些高级语言.按照这么写编译器不会给出这样的错。Rust这么设计的原因就是在很多情况下我们的代码很可能是依赖于绑定在这个变量上不可变的值,一旦这个值改变了,我们的程序很可能就不会按照我们的意愿去执行最后得到一个我们不期望得到的结果。而且这种bug往往是难以定位的,特别是修改操作在某些条件下偶然发生的时候。Rust的编译器可以准确的定位出问题对于开发人员来说是非常重要的,即便开发人员都不喜欢看到这种编译提示信息。

正因为Rust编译器可以保证那些申明为不可变的值一定不会发生改变。这就意味着你无需在阅读和编写代码时追踪一个变量会如何变化,从而简化的代码的逻辑的理解和推导。

2. 可变量

在上面也提到过Rust的变量也可以是可变的,需要你在申明变量的时候在前面加上mut关键字,有了这个关键字Rust就可以知道改变量是可变的。mut这个关键字除了使变量值可变之外,mut还会向阅读代码的人暗示其它代码可能会改变这个变量的值。

eg:我们继续修改上面的代码

fn main() {
    let mut x = 5;
    println!("The value of x is: {}",x);
    x = 6;
    println!("The value of x is: {}",x);
}

运行上述代码结果如下

cargo run
Compiling rust_for_test v0.1.0 (/Users/StevenLv/codes/rust/rust_for_test)
    Finished dev [unoptimized + debuginfo] target(s) in 1.50s
     Running `target/debug/rust_for_test`
The value of x is: 5
The value of x is: 6

因为mut关键字我们可以合法的将x绑定的值从5修改成6,相对于不可变量而言,可变变量会让代码变得更加易于编写。

除了避免出现bug,设计一个变量的可变性还要考量许多因素。例如当你在使用某些重型数据结构时,适当的使用可变性去修改一个实例,可能比赋值和重新返回一个新分配的实例要更有效率;而当数据结构较为轻量的时候,采用更加偏向函数式的风格,通过创建新变量来进行赋值,可能会使代码更加易于理解。在类似这样的情形下,为了可读性而损失少许的性能也许也是值得的。

2. 常量

与变量相对的概念就是常量(constant),Rust中的常量与变量中的不可变量一样,绑定到常量上的值无法被其他代码修改,但是需要注意的常量与变量的不可变量还是存在着细微的差别。

3. 变量与常量直接的差别

1. 语法层面

  1. 常量是不可以用mut的语法去修饰的。常量不仅默认是不可变的,不管发生什么情况,Rust中的常量无论如何都是不变的。

  2. 常量使用const关键字而不是使用let.

  3. 常量总是需要标注数据类型。

  4. 只能将常量绑定到常量表达式上,无法将一个函数的返回值或者其他需要在运行时计算的值绑定到常量上。

2. 函数作用域

常量可以可以被声明在任何的作用域中,甚至包括全局的作用域。这在一个值需要被不同部分的代码共同引用时十分有用。

3. 常量声明语法示例

eg:

const MAX_POINTS: u32 = 100_000;

常量在这个程序运行的过程中都在自己声明的作用域内有效,这使得常量可以被用于在程序的不同代码之间共享值。

4. 常量的意义

将整个程序中硬编码的值声明为常量并给予其有意义的名字,可以帮助后来维护去理解这些值的意义,而使用同一常量来索引相同的硬编码值也能为将来的修改提供方便。

4. 隐藏

fn main() {
  let  x = 5;
  let x = x + 1;
  let x = x * 2;
  println!("The value of x is: {}",x)
}
cargo run 
Compiling rust_for_test v0.1.0 (/Users/StevenLv/codes/rust/rust_for_test)
  Finished dev [unoptimized + debuginfo] target(s) in 1.16s
   Running `target/debug/rust_for_test`
The value of x is: 12

上述的代码的执行结果正常输出,用let定义了一个叫X的变量,从上面的讲解中我们可以知道Rust中的变量默认是不可改变的,而且这里也没有使用mut关键字,结果是编译正常通过,而且x的值被修改了得到了我们期待的结果。用Rust的语法去理解上述的代码就要引出一个概念--隐藏(shadow)。第一个变量被第二个变量隐藏了。这意味着我们随后使用这个变量时,值一直是最后指向的值。我们可以重复使用let关键字并配以相同的名称来不断的隐藏变量。

上面的代码将x绑定到值5上。随后它又通过重复let x = 语句隐藏了第一个x的变量,并将第一个x变量值加上1的运算结果绑定到新的变量x上,这时x的值是6.第三个let的语句同样隐藏了第二个x变量,并将第二个x变量值乘以2的结果12绑定到第三个x变量上。

1. shadow与mut的区别

1. 区别1

隐藏机制不同于将一个变量声明为mut,因为如果不是在使用let关键字的情况下重新为这个变量赋值,则会导致编译错误。通过使用let,我们可以对这个值执行一系列的变换操作,并允许这个变量在操作完成后保持自己的不可变性。

2. 区别2

隐藏于mut的另一个区别在于: 由于重复使用let关键字会创建出新的变量,所以我们可以在复用变量名称的同时改变它的类型。例如,假设程序需要根据用户输入的空格数来决定文本之间的距离,那么我们可能会把输入的空格存储为一个独立的数值:

let spaces = "  ";
let spaces = spaces.len();

这段代码可以生效是因为声明的第一个变量spaces是字符串类型,而第二个spaces虽然与第一个spaces的名称相同但是数据类型不一样,即隐藏机制运行我们复用spaces这个简单的名字,无需做出诸如spaces_num和spaces_str之类的区分。mut不支持这种方式

let mut spaces = "  ";
spaces = spaces.len();
error[E0308]: mismatched types
 --> src/main.rs:3:14
  |
3 |     spaces = spaces.len();
  |              ^^^^^^^^^^^^ expected `&str`, found `usize`

error: aborting due to previous error

For more information about this error, try `rustc --explain E0308`.
error: could not compile `rust_for_test`.

To learn more, run the command again with --verbose.

从编译器给出的结果可以看出编译器是拒绝我们修改变量的类型的。

5. 数据类型

Rust是一门静态语言,所以每一个值都有其特定的数据类型,Rust会根据数据的类型来决定该如何处理它们。我们会讨论两种不同的数据类型型子集:标量类型(scalar)和复合类型(compound)。

因为Rust是一门静态语言,所有的静态语言在编译中都需要知道变量的具体数据类型。在大部分情况下,编译器都可以根据我们如何绑定,使用变量的值来自动推导出变量的类型。如:

let guess: u32 = "42".parse().expect("Not a number!");

假设我们不提前指定数据类型

let guess = "42".parse().expect("Not a number!");

编译器会给我这样一个结果

 --> src/main.rs:2:9
  |
2 |     let guess = "42".parse().expect("Not a number!");
  |         ^^^^^ consider giving `guess` a type

error: aborting due to previous error

For more information about this error, try `rustc --explain E0282`.
error: could not compile `rust_for_test`.

To learn more, run the command again with --verbose.

编译器告诉我们无法推导出变量的类型,为了便面混淆,它需要我们手动地添加类型标注。接下来,你会看到不同数据类型的类型标注方式。

1. 标量类型

标量类型是单个值类型的统称。Rust中内建了4种基础的标量类型:整数,浮点数,布尔值以及字符。其他高级语言也一定含有这几种最最基础的数据类型。下面会介绍这几个数据类型在Rust中是怎么工作的。

1. 整数类型

整数是指那些没有小数部分的整数。

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

每一个整数类型的变体都会标明自身是否存在符号,并且拥有一个明确的大小。有符号和无符号代表一个整数类型是否拥有描述负数的能力。对于有符号的整数类型来讲,数值需要一个符号来表示当前是否为正,而对于无符号类型而言,数值永远为正,不需要符号。有符号数是通过二进制补码的形式来存储的。

对于一个位数为n的有符号类型整数,它可以存储从-(2n-1)到2n - 1范围内所有的整数。

除了指明位数的类型,还有isize和usize两种特殊的整数类型,它们的长度取决于程序运行的目标平台。在64位架构上,就是64位的,而在32位架构上就是32位的。

Rust中整数的字面量

整数字面量 示例
Decimal 98_222
Hex 0xff
Octal 0o77
Binary 0b1111_0000
Byte(u8 only) b'A'

在这么多的整数类型中,具体怎么选择,如果拿不定主意,Rust对于整数字面量的默认推到类型i32通常是一个很好的选择:他在大部分情形下都是运算速度最快的那一个,即便是在64位系统上也是如此。较为特殊的两个整数类型就是isize和usize则主要用于某些集合的索引。

2. 浮点数类型

除了整数,Rust还提供了两种基础的浮点数类型,浮点数也就是带小数的数字。这两种类型f32和f64,他们分别占用32和64位空间。由于在现代CPU中f64与f32的运行效率几乎相差无几,却拥有更高 的精度,所以在Rust中,默认会将浮点数字面量的类型推到为f64.

src/main.rs 
fn main(){
let x = 2.0 // f64
let y:f32 = 3.0 // f32
}

上面的代码展示了Rust的浮点数用了IEEE-754标准来进行表述,f32和f64类型分别对应着标准中的单精度浮点数数和双精度浮点数。

3. 数值运算

Rut支持常见的数学运算:加法,减法,乘法,除法,以及取余。下面的代码展示了如何在let语句中使用这些运算

src/main.rs 
fn main(){
  // 加法
  let sum = 5 + 10;
  //减法
  let difference = 99.5 - 4.5;
  // 乘法
  let product = 4 * 30;
  // 除法
  let quptient = 56.7/32.2;
  //取余
  let remainder = 43%5;
}

4. 布尔类型

和其他语言一样,Rust的布尔类型也只有两个值:true和false,占用两个字节的空间大小,我们用bool来表示一个布尔类型,例如

src/main.rs fn main(){
  let t = true;
  let f:bool = false; // 附带了显示类型标准的语句
}

布尔类型主要是用在if表达式中作为条件使用,主要用于代码逻辑的控制流。

5. 字符类型

Rust和其他语言一样同样也有相应的字符类型支持,在Rust中,char类型用于描述语言中最基础的单个字符。char类型使用单引号指定,而不同于字符串使用双引号指定。

src/main.rs
fn main(){
    let c = 'x';
    let z = 'Z';
    let pig = '🐷';
}

Rust中char类型占4个字节,是一个Unicode标量值,可以比ASCII表示更多的字符内容。拼音,中文,日文,韩文,零长度空字符串,甚至是emoji表情都可以作为一个有效的char类型值。实际上Unicode标量可以描述从U+0000到U+D7FF以及从U+E000到U+10FFFF范围内的所有值。由于Unicode中没有”字符“的概念,所以你现在从直觉上认为的”字符“也许与Rust的概念并不相符。在后续的分享中会仔细展开讨论。

2. 复合类型

符合类型(compound type)可以将多个不同类型的值组合为一个类型。Rust提供了两种内置的基础复合类型: 元组(tuple)和数组(array).

1. 元组类型

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

为了创建元组,我们需要一系列的值使用逗号分隔后放到有个圆括号中。元组每个位置的值都有一个类型,这些类型不需完全相同。如下代码

src/main.rs 
fn main(){
    let tu
}

由于一个元组也被视作一个单独的复合元素,所以这里的变量tup被绑定到了整个元组上。为了从元组中获得单个的值,我们可以像如何的代码示例中来解构元组:

src/main.rs
fn main(){
    let tup:(i32,f64,u8) = (500,6.4,1);
    let (x,y,z) = tup;
    println!("The value of y is:{}",y)
}

这段程序首先创建了一个元组,并将其绑定到了变量tup上。随后,let关键字的右侧使用了一个模式将tup拆分为3个不同的部分:x,y和z,这个操作也被称为解构(destructing)。最后,程序将变量y的值,6.4打印了出来。

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

fn main(){
    let tup:(i32,f64,u8) = (500,6.4,1);
    let five_hundred = tup.0;
    let six_point_four = tup.1;
    let one = tup.2;
}

这段程序首先创建了元组x,随后又通过索引访问元组的各个元素,并将它们的值绑定到新的变量上。和大多数编程语言一样,元组的索引从0开始。

2. 数组类型

我们同样可以在数组中存储刀哥值的集合。与元组不同,数组中的每一个元素都必须是相同的类型。Rust中的数组拥有固定的长度,一旦声明了就再也不能随便改变大小,这与其他某些语言有所不同。

在Rust中,你可以将逗号分隔的值放置在一对方括号内来创建一个数组:

src/main.rs
fn main(){
    let array = [1,2,3,4,5];
}

通常而言,当你想在栈上而不是堆上为数据分配空间时,或是想要确保总有固定数量的元素时,数组是一个非常有用的工具。当然Rust的标准库也提供了一个更加灵活的动态数组(vector)类型。动态数组是一个类似于数组的集合结构,但他允许用户自由地调整整数组长度。这一点和golang的Slice(切片)类似。如果你不确定什么时候该使用数组什么时候该使用动态数组,默认就先使用动态数组。

在下面这张情况下,你也许会选择使用数组而非动态数组。假设在某一个程序中需要知道一年中每个月的名字,我们就可以使用数组来存储这个名字列表。因为我们知道它有且仅有12个元素,且不太可能添加或删除月份。

let months = ["January","February","March","April","May","June","July","August","September","October","November","December"];

为了写出数组的类型,你需要一对方括号,并在方括号中填写数组内所有元素的类型,一个分号及数组内元素的数量,如下:

let a:[i32;5] = [1, 2, 3, 4, 5];

示例中i32便是数组内所有元素的类型,而分号之后的5则表示数组的容量,示例中表示该数组可以塞5个元素。

这样撰写数组类型的方式有些类似于另一种初始化数组的语法,即假如你想要创建一个含有相同元素的数组,那么你可以再方括号中指定元素的值,并接着填入一个分号以及数组的长度,如下:

let a = [3;5]

以a命名的数组将会拥有5个元素,而这些元素全部拥有相同的初始值3.这就等价于

let a = [3, 3, 3, 3, 3]
1. 访问数组的元素

数组由一整块分配在栈上的内存组成,你可以通过索引来访问一个数组中的所有元素,就像下面的演示:

src/main.rs
fn main(){
    let array = [1,2,3,4,5];
    let first = array[0];
    let second = array[1];
}

在这个例子中,first变量会被赋值为1,这正是数组中索引[0]对应的那个值。同样,second变量将会获得数组中索引[1]对应的那个值,也就是2.

2. 非法的数组访问

尝试访问数组结尾之后的元素会发生些什么,我们可以把例子改成这样:

src/main.rs
fn main(){
    let array = [1,2,3,4,5];
    let index = 10;
    let element = array[index];
    println!("The value of element is:{}",element);
}

使用cargo run命令运行,我们会看到如下的结果

error: this operation will panic at runtime
 --> src/main.rs:4:19
  |
4 |     let element = array[index];
  |                   ^^^^^^^^^^^^ index out of bounds: the len is 5 but the index is 10
  |
  = note: `#[deny(unconditional_panic)]` on by default

error: aborting due to previous error

error: could not compile `rust_for_test`.

To learn more, run the command again with --verbose.

编译没有问题,但是程序却抛出了一个运行时的错误。实际上,每次通过索引来访问一个元素时,Rust都会检查这个索引是否小于当前数组的长度。加入索引超出了当前数组的长度,Rust就会发生panic。

这是第一个涉及Rust安全原则的示例。有许多底层语言没有提供类似的检查,一旦尝试使用非法索引,你就会访问到某块无效的内存。在这种情况下,逻辑上的错误常常会蔓延至程序的其他部分,进而产生无法料想的结果。通过立即中断程序而不是自作主张的去继续运行,Rust帮助我们避开了类似的错误。

6. 函数

函数在任何的编程语言中都是一个及其重要的概念,Rust也是一样.我们知道Rust的入口就是main.rs文件中的main函数,不只是rust,大部分的程序开始入口也都是从main开始,比如C,比如go......Rust 函数的关键字是fn,可以用它来声明一个函数。

Rust代码使用蛇形命名法(snake case)来作为函数规范和变量名称的风格。蛇形命名法只用小写的字母进行命名,并以下划线分隔单词。下面是一个函数定义的示例:

src/main.rs
fn main(){
    println!("Hello World!");
    another_function();
}

fn another_function(){
    println!("Another function.")
}

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

我们可以使用函数名加圆括号的方式来调用函数。在上面的示例中,由于another_function被定义为了函数,所以我们可以在main函数体内调用它。需要注意的是,我们在这个例子中将another_function定义在了main函数之后,其实就算单把它放到main函数之前其实也没有影响,编译的结果是一样的。Rust不关心你在何处定义函数,只要这些定义对于使用区域是可见的即可。

现在我们创建一个新的二进制项目function来实践一下函数相关的功能。将上面another_function示例中的内容复制到文件src/main.rs中并运行它,下面试运行结果:

    Finished dev [unoptimized + debuginfo] target(s) in 0.10s
     Running `target/debug/rust_for_test`
Hello World!
Another function.

正如我们预料的那样,代码以它们出现在main函数中的顺序依次执行了出来。首先,"Hello World"这条信息将被打印出来,紧接着another_function函数得到执行,将函数体内的信息也打印出来。

1 . 函数参数

我们可以在函数声明中定义参数(parameter),它们是一种特殊的变量,并被视作函数签名的一部分。当函数存在参数时,你需要在调用函数时为这些变量提供具体的值。在文档中,参数变量和传入的具体参数值有自己分别对应的名称parameter和argument,但我们通常会混用两者并将它们统一称为参数而不加以区别。

下面重写后的another_function函数展示了Rust中参数的样子。

src/main.rs
fn main(){
    another_function(5);
}

fn another_function(x:i32){
    println!("The value of x is {}.",x);
}

尝试运行这段程序可以得到如下所示的输出:

 Compiling rust_for_test v0.1.0 (/Users/StevenLv/codes/rust/rust_for_test)
  Finished dev [unoptimized + debuginfo] target(s) in 0.34s
   Running `target/debug/rust_for_test`
The value of x is 5.

这里定义的another_function有一个名为x且类型为i32的参数。当5被传入another_function时,println!宏会将5放入格式化字符串中的特定位置并打印出来。

在函数签名中,必须显示的声明每个参数的类型。这是在Rust设计中设计者们经过慎重考虑后作出的决定:由于类型被显示的注明了,因此编译器不需要通过其他部分的代码进行推导就能明确的知道意图。

另外,你可以像下面一样,通过使用逗号分隔符来为函数声明多个参数:

src/main.rs
fn main(){
    another_function(5,6);
}

fn another_function(x:i32,y:i32){
    println!("The value of x is {}.",x);
    println!("The value of y is {}.",y);
}

这了的示例创建一个拥有两个参数的函数,这个函数会依次打印出这两个参数。需要注意的是,函数参数可以是不同类型的,示例代码中恰好使用了两个i32类型的参数而已。

   Compiling rust_for_test v0.1.0 (/Users/StevenLv/codes/rust/rust_for_test)
    Finished dev [unoptimized + debuginfo] target(s) in 0.83s
     Running `target/debug/rust_for_test`
The value of x is 5.
The value of y is 6.

由于我们在调用函数时,将5和6分别做为x和y的值穿传入了函数,所以这两个字符串与他们的值被相应的打印出来。

2. 函数体中的语句和表达式

函数体由若干条语句组成,并可以以一个表达式作为结尾。虽然我们在语句中见到了许多表达式,但到目前为止,我们都还没有使用过表达式来结束一个函数。由于Rust是一门基于表达式的语言,所以它将语句(statement)与表达式(expression)区别为两个不同的概念。这与其他某些语言不同。因此,让我们首先来看一下语句和表达式究竟是什么,接着再进一步讨论他们之间的区别会如何影响函数体的定义过程。

1. 语句

语句是指那些执行操作但不返回值的指令

2.表达式

表达式则是指令进行计算并产生一个值作为结果的指令。

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

src/main.rs
fn main(){
   let y = 6;
}

重点是语句在Rust中绝对不会返回值,要是将一条let语句赋值给另外一个变量就会产生编译错误如

src/main.rs
fn main(){
   let x = (let y = 6);
}  
  Compiling rust_for_test v0.1.0 (/Users/StevenLv/codes/rust/rust_for_test)
error[E0658]: `let` expressions in this position are experimental
 --> src/main.rs:2:14
  |
2 |     let x = (let y = 6);
  |              ^^^^^^^^^
  |
  = note: see issue #53667 <https://github.com/rust-lang/rust/issues/53667> for more information
  = help: add `#![feature(let_chains)]` to the crate attributes to enable

error: `let` expressions are not supported here
 --> src/main.rs:2:14
  |
2 |     let x = (let y = 6);
  |              ^^^^^^^^^
  |
  = note: only supported directly in conditions of `if`- and `while`-expressions
  = note: as well as when nested within `&&` and parenthesis in those conditions

warning: unnecessary parentheses around assigned value
 --> src/main.rs:2:13
  |
2 |     let x = (let y = 6);
  |             ^^^^^^^^^^^ help: remove these parentheses
  |
  = note: `#[warn(unused_parens)]` on by default

error: aborting due to 2 previous errors; 1 warning emitted

For more information about this error, try `rustc --explain E0658`.
error: could not compile `rust_for_test`.

To learn more, run the command again with --verbose.

由于let y = 6语句没有返回任何值,所以变量x就没有可以绑定的东西。这里的行为与某些语言不同,例如C语言或者是Ruby重的赋值语句会返回所赋值的值。在这些语言中,可以编写类似于x = y = 6这样的语句,并使得x和y变量同时拥有6这个值,但这在Rust中可行不通。

与语句不同,表达式会计算初某个值来作为结果,你在Rust中编写大部分代码都会是表达式。以简便的数字运算 5 + 6 为例,这就是一个表达式,并且会计算出值为11。另外,表达式本身也可以作为语句的一部分,在前面的示例代码中有提到let y = 6;字面量6就是一个表达式,它返回6作为自己的计算结果。调用函数式表达式,调用宏是表达式,我们用来创建新作用域的花括号({})同样也是表达式,如

src/main.rs
fn main(){
    let x = 5; 
    ① let y = { ②
        let x = 3;
        ③ x + 1
    };
    println!("the value of y is {}",y)
}

②是一个代码块。在这个例子中,会计算出4为计算结果。而这个结果会作为let语句①的一部分被绑定到变量y上。注意结尾处③的表达式x+1没有添加分号,这与我们之前见过的大部分代码不同。假如我们在表达式的末尾加上了分号,这一段代码就变成了语句而不会返回任何值。

3. 函数的返回值

函数可以向调用它的代码返回值。虽然你不用为这个返回值命名,但需要在肩头符号(->)的后面声明它的类型。在Rust中,函数的返回值等同于函数体最后一个表达式的值。熟悉Ruby的朋友应该就很熟悉了。你也可以使用return关键字并指定一个值来提前从函数中返回,但大多数函数都隐式返回最后的表达式,如:

src/main.rs
fn five() -> i32{
    5
}

fn main(){
    let x = five();
    println!("The value of x is {}",x)
}

five函数除了5,没有任何其他的函数调用,宏调用,甚至是let语句,但它在Rust中确实是一个有效的函数。five函数的返回值通过-> i32被绑定了,运行代码会看到如下的输出:

   Compiling rust_for_test v0.1.0 (/Users/StevenLv/codes/rust/rust_for_test)
    Finished dev [unoptimized + debuginfo] target(s) in 1.74s
     Running `target/debug/rust_for_test`
The value of x is 5

five函数中5就是函数的输出值,这也是它的返回值,返回值被声明成i32.这里有两个需要注意的地方。首先,语句let x = five();使用函数的返回值来初始化左侧的变量。由于five函数总是返回5,所以该代码等价于:

let x = 5;

其次,这里的five的函数没有参数,仅仅定义了返回的类型。函数中除了5之外没有任何的东西,5即是我们想要的结果作为返回的表达式。再如

src/main.rs
fn plus_one(x:i32) -> i32{
    x+1
}

fn main(){
    let x = plus_one(5);
    println!("The value of x is {}",x)
}

运行结果

   Compiling rust_for_test v0.1.0 (/Users/StevenLv/codes/rust/rust_for_test)
    Finished dev [unoptimized + debuginfo] target(s) in 0.32s
     Running `target/debug/rust_for_test`
The value of x is 6

运行结果是The value of x is 6,如果将plus_one()函数改为

fn plus_one(x:i32) -> i32{
    x+1;
}

表达式就会因为变味语句而导致编译错误。

src/main.rs
fn plus_one(x:i32) -> i32{
    x+1;
}

fn main(){
    let x = plus_one(5);
    println!("The value of x is {}",x)
}
 Compiling rust_for_test v0.1.0 (/Users/StevenLv/codes/rust/rust_for_test)
error[E0308]: mismatched types
--> src/main.rs:1:23
|
1 | fn plus_one(x:i32) -> i32{
|    --------           ^^^ expected `i32`, found `()`
|    |
|    implicitly returns `()` as its body has no tail or `return` expression
2 |     x+1;
|        - help: consider removing this semicolon

error: aborting due to previous error

For more information about this error, try `rustc --explain E0308`.
error: could not compile `rust_for_test`.

To learn more, run the command again with --verbose.

我们看到错误的第二行的内容error[E0308]: mismatched types意思是类型不匹配,和明显的可以看出在定义plus_one的过程中声明它会返回一个i32类型的值。由于语句不会产生值,所以Rust就默认返回了一个空元祖,也就是上述编译结果中的().这与我们预先设置的返回i32类型相矛盾,所以导致了这样的编译错误。

1. 题外话

Rust编译器的错误提示信息中还提示了一个可能修正错误的方案,它建议我们去掉函数末尾的分号去解决这个问题。

4. 注释

有一定代码基础的同学对注释(comment)这个概念再熟悉不过了,注释的存在的意义也在于程序员可以在源代码中留下记录,编译器在编译时回忽略这些注释,使得阅读代码的人可以通过注释更好的理解你的意图,如:

// Hello,World.

在Rust中,注释必须使用两到斜杠开始,并持续到本行末尾。对于那些超过一行的注释,必须在每一行前面都加上//.

注释同样也可以放在代码行的末尾处。

src/main.rs
fn main(){
    let kucky_number = 7; // I'm feeling lucky today.
}

不过你可能会更加常见到下面这种格式,在需要说明的代码上方需单独放置一行注释:

src/main.rs
fn main(){
    // I’m feeling lucky today
    let lucky_number = 7;
}

5 控制流

通过条件来执行或重复执行默写代码是大部分编程语言的基础组成部分。在Rust中用来控制程序执行流的结构主要就是if表达式与循环表达式。

1. if表达式

if表达式允许我们根据条件执行不同的代码分支。我们提供一个条件,并且做出声明:"假如这个条件满足,则运行这段代码。假如条件没有被满足,则跳过相应的代码。"

src/main.rs
fn main(){
    let number = 3;
    if number < 5 {
        println!("condition was true");
    }else{
        println!("condition was false");
    }
}

所有的if表达式都会用if关键字来开头,并紧随一个判断条件。上面的代码的使用if判断number对象的值是否小于5。花括号中放置了条件为真时需要执行的代码片段。if表达式中与条件相关的代码块也称为分支(arm)。与Rust中的match表达式一样。

执行结果:

   Compiling rust_for_test v0.1.0 (/Users/StevenLv/codes/rust/rust_for_test)
    Finished dev [unoptimized + debuginfo] target(s) in 5.06s
     Running `target/debug/rust_for_test`
condition was true

修改number的值

let number = 7;

执行结果

   Compiling rust_for_test v0.1.0 (/Users/StevenLv/codes/rust/rust_for_test)
    Finished dev [unoptimized + debuginfo] target(s) in 0.80s
     Running `target/debug/rust_for_test`
condition was false

if表达式的使用很简单,但需要注意一点。代码中的条件表达式必须产生一个bool类型的值,否则会产生编译错误。

   Compiling rust_for_test v0.1.0 (/Users/StevenLv/codes/rust/rust_for_test)
error[E0308]: mismatched types
 --> src/main.rs:3:8
  |
3 |     if number {
  |        ^^^^^^ expected `bool`, found integer

error: aborting due to previous error

For more information about this error, try `rustc --explain E0308`.
error: could not compile `rust_for_test`.

To learn more, run the command again with --verbose.
The terminal process terminated with exit code: 101

这个错误表明Rust期待在条件表达式中得到一个bool值,而不是一个整数。这与Ruby或者JavaScript等语言不用,Rust不会自动尝试将非布尔类型的值转为布尔类型。你必须显示地在if表达式中提供一个布尔类型作为条件,加入你想要if代码块只在数字不等于0时运行。那么我们可以将if表达式修改为:

src/main.rs
fn main(){
    let number = 7;
    if number!=0 {
        println!("number was something other than zero");
    }
}

运行这段代码将会输出number was something other than zero。

使用else if实现多重判断,你可以使用组合if,else以及else if表达式实现多重陪条件判断。如:

src/main
fn main(){
    let number = 6;
    if number % 4 ==0{
        println!("number is divisible by 4");
    }else if number % 3 == 0{
        println!("number is divisible by 3");
    }else if number % 2 == 0{
        println!("number is divisible by 2");
    }else{
        println!("number is divisible by 4,3 or 2");
    }
}

这段程序拥有4条可能执行的路径,运行后可以看到结果

   Compiling rust_for_test v0.1.0 (/Users/StevenLv/codes/rust/rust_for_test)
    Finished dev [unoptimized + debuginfo] target(s) in 0.76s
     Running `target/debug/rust_for_test`
number is divisible by 3

这段程序运行时,它会依次检查每一个if表达式,并执行条件受限被判断为真的代码片段。尽管6可以被2整除,但我们既没有看到输出 println!("number is divisible by 2");也没有看到else代码块中的println!("number is divisible by 4,3 or 2");被输出。这是因为Rust会且仅会执行第一个条件为真的代码块,一旦发现满足条件,它便不会在继续检查剩下的那些条件分支了。

当然,过多的else if表达式可能会使我们的代码变得杂乱无章。后面会讲到match的用法,用match可以处理这种条件过多的情况。

在let 语句中使用if

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

src/main.rs
fn main(){
 let condition = true;
 let number = if condition {
     5
 }else{
     6
 };
 println!("The value of number is:{}",number);
}

上面的代码的number变量被绑定到了if表达式的输出结果上面。运行这段代码可以看到如下的结果:

   Compiling rust_for_test v0.1.0 (/Users/StevenLv/codes/rust/rust_for_test)
    Finished dev [unoptimized + debuginfo] target(s) in 0.51s
     Running `target/debug/rust_for_test`
The value of number is:5

代码块输出的值就是其中最后一个表达式的值。另外,数字本身也可以作为一个表达式使用,在上面的例子中,整个if表达式的值取决于究竟哪一个代码块得到了执行。这也意味着,所有if分支可能返回的值都必须是一种类型的;上面的代码if分支与else分支的结果都是i32类型的整数。假如分支表达式产生的类型无法匹配,那么就会触发编译错误,如下:

src/main.rs
fn main(){
 let condition = true;
 let number = if condition {
     5
 }else{
     "six"
 };
 println!("The value of number is:{}",number);
}

结果如下:

   Compiling rust_for_test v0.1.0 (/Users/StevenLv/codes/rust/rust_for_test)
error[E0308]: `if` and `else` have incompatible types
 --> src/main.rs:6:6
  |
3 |    let number = if condition {
  |  _______________-
4 | |      5
  | |      - expected because of this
5 | |  }else{
6 | |      "six"
  | |      ^^^^^ expected integer, found `&str`
7 | |  };
  | |__- `if` and `else` have incompatible types

error: aborting due to previous error

For more information about this error, try `rustc --explain E0308`.
error: could not compile `rust_for_test`.

To learn more, run the command again with --verbose.
The terminal process terminated with exit code: 101

第一个if返回一个i32类型的整数,else返回了一个值为“six”的字符串,变量只能拥有一种类型,所以这段代码无法通过编译。为了对其他使用number变量的代码进行编译时类型检查,Rust需要在编译时确定number的具体类型。如果Rust能够使用运行时确定的number类型,那么它就不得不记录所有可能出现的类型,这会使得代码编译器的实现更加复杂,并丧失许多代码安全保障。

2. 使用循环重复执行代码

我们常常需要重复执行同一行代码,针对这张场景,Rust提供了循环(loop)工具,一个循环会执行循环中的代码直到结尾,并紧接着回到开头继续执行。

Rust提供了三种循环:loop,while和for。

1 使用loop循环

我们可以使用loop关键字指示Rust反复执行一段代码,直到我们显示的退出为止。

src/main.rs
fn main(){
    loop{
        println!("again")
    }
}

运行这段程序时,除非我们手动强制退出程序,否则again会被反复地stdout输出。大部分终端都支持Ctrl+C来终止这种死循环。

    Finished dev [unoptimized + debuginfo] target(s) in 0.01s
     Running `target/debug/rust_for_test`
again
again
again
....... //全是again的输出
^Cagain

这里的符号C就是我们按下了快捷键Ctrl+C。在C后面可能看得到again输出也有可能看不到,这取决于程序在接收到退出信号时执行到了哪一步。

让然Rust提供了另外一种更加可靠的循环退出方式,你可以再循环中使用break关键字来通知程序退出循环。

2 从loop循环中返回值

loop循环可以被用来反复尝试一些可能会失败的操作,比如检查某个线程是否完成了工作。不管怎么样,你也许需要将该操作的结果传递给余下的代码。为了实现这一目的,我们返回值添加到brake表达式之后,也就是我们用来终止循环表达式的后面。接着你可以再代码中使用loop循环来返回值了。如

src/main
fn main(){
    let mut counter = 8;

    let result = loop{
        counter += 1;

        if counter == 10{
            break counter * 2;
        }
    };

    println!("The result is {}",result);
}

我们在循环前声明了变量counter并将其初始化为0。接着我们声明了一个名为result的变量来存储循环中返回的值。该循环会在每次迭代时给count变量的值加1,并检查计数器是否已经增至10.一旦条件符合,我们便使用break关键字返回counter * 2.在循环之后我们还是用了一个分号来结束当前的语句,这回将循环的返回结果赋值给result。最终,我们会打印出result内存储的值,也就是20

执行结果

    Finished dev [unoptimized + debuginfo] target(s) in 0.14s
     Running `target/debug/rust_for_test`
The result is 20
3 while条件循环

另外一种常见的循环就是在每次执行循环体之前都判断一次条件,假如条件为真则执行代码片段,假如条件为假或在执行过程中碰到break就退出当前循环。这种模式可以通过loop,if,else以及break关键字的组合使用来实现。

由于这种模式太过于常见,Rust提供了一个内置的语言结构:While循环。如下面的代码示例,程序会执行3次,每次减1,在循环结束之后打印出特定的消息并退出。

src/main.rs
fn main(){
    let mut number = 3;
    while number!=0{
        println!("{}!",number);

        number = number - 1;
    }
    println!("LIFTOFF!!!")
}

如果使用if,else,break去实现上面的逻辑,会有很多的冗余代码,使用while会使代码的逻辑结构更加清晰。当条件为真时,来实行循环体中的代码,否则就退出循环。

执行结果

   Compiling rust_for_test v0.1.0 (/Users/StevenLv/codes/rust/rust_for_test)
    Finished dev [unoptimized + debuginfo] target(s) in 1.41s
     Running `target/debug/rust_for_test`
3!
2!
1!
LIFTOFF!!!
4 用for来循环遍历集合

你可以使用while结构来遍历诸如数组之类的集合中的元素,如:

src/main.rs
fn main(){
    let a = [10,20,30,40,50];
    let mut index = 0;
    while index < 5 {
        println!("the value is:{}",a[index]);
        index += 1;
    }
}

上面的代码会对数组中的所有元素进行一个遍历的输出,index从0开始,每次经过一次循环index都会加1,知道加到5就退出。

执行结果

   Compiling rust_for_test v0.1.0 (/Users/StevenLv/codes/rust/rust_for_test)
    Finished dev [unoptimized + debuginfo] target(s) in 0.56s
     Running `target/debug/rust_for_test`
the value is:10
the value is:20
the value is:30
the value is:40
the value is:50

如同我们预料的那样,数组中的5个元素都被输出到了终端上,尽管index会在摸个时候变成5,但是循环会在我们尝试越界去访问数组的第六个数值之前停止。

需要指出的是,类似的代码非常容易出错,可能会因为使用了不正确的索引长度而使程序崩溃。而且,由于我们运行时代码来对每一次遍历做出判断啊,所以这段代码的运行效率会比较低。

你可以使用for循环让这种循环变得更加的简单明了。如:

src/main.rs
fn main(){
    let a = [10,20,30,40,50];
    for element in a.iter(){
        println!("the value is:{}",element)
    }
}

执行结果:

   Compiling rust_for_test v0.1.0 (/Users/StevenLv/codes/rust/rust_for_test)
    Finished dev [unoptimized + debuginfo] target(s) in 0.90s
     Running `target/debug/rust_for_test`
the value is:10
the value is:20
the value is:30
the value is:40
the value is:50

结果和用while实现的代码的结果一样,但是重要的是我们增强了代码的安全性,不会出现越界访问或者是漏掉某些元素之类的问题。

例如在这段代码中

src/main.rs
fn main(){
    let a = [10,20,30,40,50];
    let mut index = 0;
    while index < 5 {
        println!("the value is:{}",a[index]);
        index += 1;
    }
}

假如我们从a数组中移除了某个元素,却忘记将循环中的条件更新为while index<4,那么在运行代码就会发生崩溃。而使用for循环的话,就不需要市场惦记着在更新数组元素数量时还要去修改别的代码。

for循环的安全性和简洁性成为了Rust中最为常见的循环结构。即便是为了实现类似如下的代码,要执行特定次数的循环任务,大部分的

src/main.rs
fn main(){
    let mut number = 3;
    while number!=0{
        println!("{}!",number);

        number = number - 1;
    }
    println!("LIFTOFF!!!")
}

Rust开发者也会优先选择使用for循环实现。我们可以配合标准库中提供的Range来实现这一目的,它被用来生成一个数字开始到另一个数字结束之前的所有数字序列。

上述的代码我想通过另外一个还未提及过的rev的方法来翻转Range生成的序列

src/main.rs
fn main(){
for number in (1..4).rev(){
    println!("{}",number);
}
println!("LIFTOFF!");
}

执行结果

   Compiling rust_for_test v0.1.0 (/Users/StevenLv/codes/rust/rust_for_test)
    Finished dev [unoptimized + debuginfo] target(s) in 0.75s
     Running `target/debug/rust_for_test`
3
2
1
LIFTOFF!

看上去是更加简洁了。

7. 总结

以上是Rust基本语法中的全部内容,讲述了变量,标量和复合数据类型,函数,注释,if表达式,while循环以及for循环,掌握了以上的语法知识可以对Rust有一个初步的了解,下一章将讲述Rust很有的趣的所有权的内容。

posted @ 2021-02-09 22:35  ttlv  阅读(613)  评论(0编辑  收藏  举报