[The RUST Programming Language]Chapter 3. Common Programing Concepts (1)

Foreword 前言

在本章中将会介绍那些几乎在每种编程语言中都出现的概念,并告诉你它们在Rust中如何使用。大多数编程语言其实核心内容是类似的,本章中的概念并不是Rust独有。但是我们今天只介绍它们在Rust上下文环境下的表现,并按照惯例解释下如何使用这些概念。

特别说明,在本章中你会学习有关变量,基本类型,函数,注释和控制流。它们是每一个Rust程序的基础,早早的学习它们可以为你的Rust学习之旅提供强大的核心保障。

关键字
就像其它的编程语言一样,Rust有许多保留用的关键字。请始终牢记,任何情况下都不要将这些关键字用作你变量和函数的名字。大部分关键字都有特殊的意义,你的Rust程序使用它们来完成各种各样的任务;有一些关键字目前还不提供相关的功能,但它们也同样保留了下来,未来或许会被添加进Rust中。你可以在附录A中找到完整的关键字清单。

Variables and Mutability 变量和可修改性

在第二章中,我们提到过,缺省情况下,变量是不可被修改的。这是众多Rust语言的举措之一,旨在促使你通过这种方式,方便的获得Rust提供的程序安全性和并发性能力,这也是Rust不同于其它编程语言的优点之一。然而,你还是有将变量改为可修改的权利。那么让我们来探索下为什么Rust鼓励你爱上不可修改,以及为什么有时候你可能会气的想摔键盘。

当一个变量是不可修改的时候,你就无法修改绑定到它上面的值。为了说明这点,请在我们的projects目录下通过cargo new variables创建一个新的项目variables

接着进入新的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);
}

保存你的程序,然后运行cargo run,你应该会收到像下面这样的报错消息:

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

这个例子显示了编译器怎样帮助你找到你程序中的错误。诚然,编译失败很令人沮丧,但它意味着你的程序不能安全的执行你所期望的功能,并不意味着你不是个合格的程序员。哪怕是Rustacean里面的老司机,也常常会在编译时翻车。

这里的错误消息标识出了错误的原因cannot assign twice to immutable variable x(不能给不可修改变量x二次赋值)

当我们尝试修改一个不可修改变量的值时,编译器就会报错,因为这常常是容易导致发生bug的情形。譬如我们程序的另一个部分有用到这个变量,并且假定这个变量的值是永远不会变的,如果这时我们在它前面修改了这个变量,那程序就不会按照它设计的样子去执行。这种bug原因事实上是非常难跟踪的,尤其是这个变量的值只是在某些情况下会被修改的时候。

在Rust中,编译器会担保,只要你在语句中说明了某个变量是不可修改的,那么它是真的永远不能被修改。这意味着,当你阅读和编写代码的时候,你无须时刻关注是否在某个时间点某个地方,这个变量的值会被修改。你的代码将因此受益而变得易于推理。

可修改性也是非常有用的。变量只是缺省情况下不可修改,就像你在第二章中做的那样,只要你在变量名前添加了mut这个关键字,你就可以修改这些变量了。mut会向未来阅读代码的人传递一个意图,表明在程序的其它某一部分,这个变量的值会被修改到。

现在我们修改下示例代码:

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 variables v0.1.0 (file:///projects/variables)
    Finished dev [unoptimized + debuginfo] target(s) in 0.30 secs
     Running `target/debug/variables`
The value of x is: 5
The value of x is: 6

我们被允许修改变量的值了,x5变味了6,当mut使用的时候。某些情形下,你希望某个变量可修改,因为相比于不可修改变量,它能使你的代码更加易于书写。

对于可修改还是不可修改,你须要去好好权衡,考量。举个例子,如果你有一个结构,包含大量数据,直接修改它显然要比复制和创建新的实例来的更快。而数据量不大的结构,创建新的实例,并用类似函数化的结构来编程,可以使程序更易理解,通过一些小小的性能开销来获得更明晰健壮的程序,也是值得的。

Differences Between Variables and Constants变量与常量的区别

让变量的值无法修改,可能会让你想起一个其它编程语言的概念:constants(常量)。就像不可修改的变量一样,常量也是指那种当某个值绑定到一个名字上后就不允许再做修改了。但是在Rust中,常量和变量是有区别的。

首先,你不能在常量上使用mut,常量并不是缺省不可修改,而是始终无法修改。

你通过const关键字声明一个常量而不是let,并且在给常量赋值前必须明确标注值的类型。我们将在Data Types这一节中介绍类型和类型标注,这里你只须要了解到,要创建一个常量必须要标注值是哪种类型。

常量可以在任何作用域中定义,包括全局域,这使得当你的程序在很多地方须要引用到这个值时非常有用。

这里有一个常量定义的例子,我们定义了一个常量MAX_POINTS并设置其值为100000(Rust的惯例是将常量的名字都用大写字母表示,并且单词间用下划线分隔来提高可读性)。

#![allow(unused_variables)]
fn main() {
const MAX_POINTS: u32 = 100_000;
}

在整个程序运行时中,常量在声明它的作用域总是有效的。常量非常适合用来保存那些会被程序多个部分频繁访问的值,举例来说游戏中允许玩家达到的最高分数或者光速的值都是很适合被定义为程序常量。

将你程序中那些硬代码写死的值替换为常量,会利于未来接盘的运维人员方便理解和推导程序的逻辑。如果未来这些值须要修改,你也只需要修改代码中常量的值即可。

Shadowing 重影

你在我们第二章猜数字游戏中曾看到过,我们定义了一个和先前变量名字一样的变量,并且新的变量shadow了它前面的变量。当Rustacean常说第一个变量被第二个变量shadow了,这意味着再用到这个变量时,里面的值会是第二个变量的值。我们通过重复使用let和同一个变量名来shadow一个变量:

fn main() {
    let x = 5;

    let x = x + 1;

    let x = x * 2;

    println!("The value of x is: {}", x);
}

程序一开始定义了一个变量 x 并给它赋值 5,但 x 又被 let x = shadow了,原始的值被加上了1,所以现在x的值是6。然后出现了第三个let语句,又一次shadow了x2乘以之前x的值,得到最终结果12,所以我们运行程序会看到如下的结果:

$ cargo run
   Compiling variables v0.1.0 (file:///projects/variables)
    Finished dev [unoptimized + debuginfo] target(s) in 0.31 secs
     Running `target/debug/variables`
The value of x is: 12

重影与mut是非常不同的,当我们试图重新指定一个变量而不使用let关键字时,我们在编译时发生报错。通过使用let,我们可以将值挪到一个变量中,并且在挪动后,变量依旧是不可修改的。

mut与重影的另一个不同在于,我们可以通过let高效创建一个新的变量,我们可以修改变量对应的值类型并且复用这个变量名。举例来说,我们的程序问用户,他想在某段文本前加入多少空格,用户敲了一堆空格而我们实际上只想记录空格数:

#![allow(unused_variables)]
fn main() {
let spaces = "   ";
let spaces = spaces.len();
}

上面的程序是允许的,因为第一个spaces变量是字符串变量,但第二个spaces变量是一个重新绑定的变量并沿用了第一个变量的名字,可它却是数字型变量。因此重影帮助我们脱离了想一堆变量名的苦恼,譬如spaces_strspaces_num,我们可以直接复用spaces这个简单的名字。然而,如果我们想用mut来这样做,那我们就会得到一个编译时报错:

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

现在我们已经探索了变量是如何工作的,下面我们来看下更多变量可以使用的类型吧。

Data Types 数据类型

在Rust中,每一个值都有一个明确的数据类型,只有知道了数据具体的类型,Rust才能懂得如何处理它。我们先来看两个数据类型的子集:scalar(标量)和compound(复合类型)。

请牢记,Rust是静态类型语言,它必须在编译时知道所有变量的类型。Rust的编译器能够根据我们赋给变量的值,或是我们使用变量的方式来推断出变量的类型。如果存在有多种数据类型的情况,就像我们在第二章猜数字游戏中将String通过parse转化为一个数字类型,我们必须添加一个类型注释:

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

如果我们在这里不使用类型注释,Rust会显示下面的报错信息,说明编译器须要我们提供更多信息来知道我们想要什么类型的数据:

error[E0282]: type annotations needed
 --> src/main.rs:2:9
  |
2 |     let guess = "42".parse().expect("Not a number!");
  |         ^^^^^
  |         |
  |         cannot infer type for `_`
  |         consider giving `guess` a type

你将会在下面看到许多不同数据类型的类型注释。

Scalar Types 标量类型

标量类型代表单独一个值,Rust有四大主要标量类型:整型、浮点数、波尔值和字符。你肯定在其它编程语言中也见过它们,那话不多说,我们来看下它们在Rust中是如何使用的。

Integer Types 整数类型

整型是指那种没有小数位的数字,我们在第二章中使用过一个整型 u32,表示其对应的值是一个无符号整数(有符合整数请用i而不是u)且占据32bits的空间。下面的表中展示了Rust中内建的整数类型,在Signed和Unsigned列中的整型都能用来定义一个整数值:

LengthSignedUnsigned
8-biti8u8
16-biti16u16
32-biti32u32
64-biti64u64
128-biti128u128
archisizeusize

每一个变量可以是无符号,也可以是有符号,并且有明确的尺寸大小。无符号和有符号主要取决于这个数字能否是正数或是负数,换句话说,如果一个数字有可能会是负数,那它就得使用有符号整型,反之如果它总是正值,那就可以使用无符号整型。就像你画在纸上的那样,有些数字前面可以有加号或者减号。但当你认为数字很安全且总是为正值时,还是请用无符号整型,因为带符号的数字在计算机中是以补码的方式存储的。

每一个有符号变量能存储-(2n - 1)到2n-1之间的数字,n是这个变量使用的比特数。所以,i8可以存储-(27 - 1)到27-1之间的数字,即-128到127。相对比的,无符号数u8可以存储0到28-1直接的数字即0到255。

此外,isizeusize这两个整数类型比较特殊,它们取决于你的程序跑在多少位的电脑上。当你的电脑是64位架构的时候,它们就占64bits,如果是32位架构,那就是32bits。

你可以像下表一样在Rust中直接输入整数字面量,但须要注意一点,除了字节字面量,其它所有方式都须要一个类型前缀,就像57u8。此外,示例中的_仅是用作视觉分隔符,是Rust用来方便阅读的,譬如1_000

Number literalsExample
Decimal98_222
Hex0xff
Octal0o77
Binary0b1111_0000
Byte(u8 only)b’A’

译者注:表中示例98_222 代表98222,而非98.222

那么问题来了,你怎么知道我们该用哪种整数类型?如果你不确定的话,没关系,Rust会缺省帮你做合适的选择——整型缺省是i32,这种类型创建最为快速,即便是在64位的系统中。至于isizeusize,在对一些类型的collection(集)索引时会频繁使用到它们。

整数溢出
我们已经说过,如果你有一个u8类型的变量,它只能存储0到255之间的值。如果你试图修改这个变量,给它一个超出这个范围的值,譬如256,这时就会发生整数溢出。对于这种行为,Rust有一些有趣的规则。当你在debug模式下编译程序时,Rust会有针对整数溢出的检查机制,当发现存在整数溢出的情况时,Rust会在你程序运行时panic(惊慌)。panic这个词是Rust用来表示程序由于错误而退出的情况,我们会在第九章中对panic做深入介绍。
当你在release模式下编译,即在编译时使用--release这个标识,Rust就不会去检查整数溢出。取而代之的,Rust会做补码换行,简短的说,就是当给的值超过类型所能允许的最大值时,做类似取余的操作。譬如对于u8类型,256会变成0,257则变成1。程序不会惊慌,但变量就会包含你没想到的值。依赖标准的整数溢出换行机制被认为是一种错误的方法,如果你明确想要做换行,请使用标准库中另一个类型Wrapping

Floating-Point Types 浮点类型

Rust有两个主要的浮点数类型,即包含小数部分的数字,f32f64,分别代表32位大小和64位大小。缺省的类型是f64,因为现代的CPU在处理速度上,对于f32f64已经没有多大出入了,但f64能提供更高的精度。

下面是Rust中使用浮点数的例子:

fn main() {
    let x = 2.0; // f64

    let y: f32 = 3.0; // f32
}

Rust中的浮点数是依据IEEE-754标准设计的,f32代表单精度浮点数,f64代表双精度浮点数。

Numeric Operations 数字操作

Rust提供基础的数字操作:加法、减法、乘法、除法和求余。下面的示例向你展示了如何在let语句中使用它们:

fn main() {
    // addition
    let sum = 5 + 10;

    // subtraction
    let difference = 95.5 - 4.3;

    // multiplication
    let product = 4 * 30;

    // division
    let quotient = 56.7 / 32.2;

    // remainder
    let remainder = 43 % 5;
}

所有在这些语句中的表达式都有一个数学符号并产生了一个值,这些值又被绑定到对应的变量上。你可以在附录B中找到所有Rust提供的操作符清单。

The Boolean Type 波尔类型

就像其它大部分编程语言,Rust中波尔类型也有两个值:truefalse,波尔类型只占一个字节。在Rust中可以通过bool来指明波尔类型:

fn main() {
    let t = true;

    let f: bool = false; // with explicit type annotation
}

波尔类型主要是在条件判断中使用,譬如if表达式,我们会在控制流一节中介绍if表达式。

The Character Type 字符类型

目前我们只介绍了数字,Rust当然也支持字母。Rust中的char类型是最基础的字符类型,下面的代码展示了某些使用字符的方法。(这里请注意,char中的字母是通过单引号指定的,而字符串是通过双引号)

fn main() {
    let c = 'z';
    let z = 'ℤ';
    let heart_eyed_cat = '😻';
}

Rust中的char类型有4字节长度,可以表示Unicode标量值,这代表了它可以比ASCII展现更多的字符。带注音的字母、中文、日文、韩文、emoji和零宽空格,它们在Rust中都是有效的char值。Unicode标量值的范围涵盖了U+0000U+D7FFU+E000U+10FFFF中的所有编码。然而,通常的“字符”与Unicode中的字符却不是一个概念,这句话可能有点让人费解,但人类直观印象中的“字符”并不一定是Rust中的char类型,我们会在第八章的通过字符串存储UTF-8编码文本再讨论这个话题。

Compound Types 复合类型

复合类型可以将多个值组织到一个类型中,Rust有两个基础复合类型:tuples(元组)和arrays(数组)。

The Tuple Type 元组

元组常用来将一些不同类型的数据组合到一起,元组有一个固定的长度,一旦元组定义好,你就无法再修改它的长度。

你可以通过一组括号中的逗号来创建一个元组,元组中的每个位置都有一个对应的类型,这些类型并不需要一样。我们在下面的例子中使用了一些可选的类型:

fn main() {
    let tup: (i32, f64, u8) = (500, 6.4, 1);
}

例子中的变量tup绑定到了一个元组,元组是一个复合元素。想要获取到元组中的某个值,我们可以通过模式匹配去解构元组的值,就像下面这样:

fn main() {
    let tup = (500, 6.4, 1);

    let (x, y, z) = tup;

    println!("The value of y is: {}", y);
}

在上面,我们创建了一个元组并将它绑定到了变量tup,然后let语句通过一个模式,将tup中的值分到了三个变量xyz,这种方式被称作destructuring解构,因为它把一个单元组分解为三部分,最终程序打印出了y的值,即6.4

作为模式匹配方式解构元组的补充,我们还可以直接通过.点号加索引的方式直接获取元组中对应的值:

fn main() {
    let x: (i32, f64, u8) = (500, 6.4, 1);

    let five_hundred = x.0;

    let six_point_four = x.1;

    let one = x.2;
}

这个程序创建了一个元组x,然后通过索引为元组中的每个元素创建了一个新的变量,在几乎所有的编程语言中,元组的索引都是从0开始的。

The Array Type 数组类型

另一个存储多个值的方法是使用数组。不同于元组,数组中的每个元素都必须有一样的类型,不同于其它编程语言中的数组,Rust中的数组就像元组一样,有固定的长度。

在Rust中,数组里的值都是通过逗号分隔并包在一个方括号中:

fn main() {
    let a = [1, 2, 3, 4, 5];
}

数组是非常有用的,尤其是当你想让你的数据存储到栈而非堆上时(我们会在第四章中讨论栈和堆的内容),或者当你想确保你总是有固定数量的某些元素,尽管数组并不像向量类型那样灵活。vcator向量是标准库提供的一种类似的集类型,并提供了伸缩性,如果你不确定该用一个数组还是向量,你或许应该使用向量,我们会在第八章中介绍它。

这有一个你该用数组而非向量的例子——在程序中我们须要知道每个月份的名字。因为我们不大可能回去添加或删除月份,所以你可以定义一个数组,因为你知道它里面总是包含12个一样的元素:

#![allow(unused_variables)]
fn main() {
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];,但显然效率更高。

Accessing Array Elements 访问数组元素

一个数组是栈上一块连续的内存空间,你可以通过索引方式来访问每一个元素,就像这样:

fn main() {
    let a = [1, 2, 3, 4, 5];

    let first = a[0];
    let second = a[1];
}

在这个例子中,变量first会获得值1,因为它是数组索引0对应的值。而second会获取值2,它是数组索引1对应的值。

Invalid Array Element Access 无效的数组元素访问

那当我们尝试访问一个不在数组索引内的元素,这是会发生什么呢?你可以尝试下下面的代码,它可以通过编译,但当运行时却会发生报错:

fn main() {
    let a = [1, 2, 3, 4, 5];
    let index = 10;

    let element = a[index];

    println!("The value of element is: {}", element);
}

通过cargo run指令执行时,会看到这样的结果:

$ cargo run
   Compiling arrays v0.1.0 (file:///projects/arrays)
    Finished dev [unoptimized + debuginfo] target(s) in 0.31 secs
     Running `target/debug/arrays`
thread 'main' panicked at 'index out of bounds: the len is 5 but the index is
 10', src/main.rs:5:19
note: Run with `RUST_BACKTRACE=1` for a backtrace.

编译过程没有发生任何报错,但程序最后产生了一个运行时错误并没能成功推出。当你试图通过索引访问一个元素,Rust会检查你指定的索引是否小于数组的长度,如果这个索引值大于等于数组的长度,Rust就会panic。

这是第一个Rust安全原则的例子,在很多低级语言中,是没有这种检查机制的,当你提供了一个错误的索引时,你就能获取一个错误的内存内容。Rust中会保护你的程序,当遇到这种情况立即退出,而不是允许它继续访问内存。在第九章中,我们将讨论更多关于Rust错误处理的知识。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
《The Rust Programming Language》(Rust编程语言)是一本由Rust开发社区编写的权威指南和教程,用于学习和开发Rust编程语言Rust编程语言是一种开源、现代化的系统级编程语言,具有强大的内存安全性、并发性和性能。它最初由Mozilla开发,并于2010年首次发布。Rust的设计目标是实现安全、并发和快速的系统级编程,适用于像操作系统、浏览器引擎和嵌入式设备这样的低级应用程序。 《The Rust Programming Language》提供了对Rust编程语言的全面介绍。它从基本的语法和数据类型开始,然后涵盖了Rust的所有关键概念和特性,如所有权系统、借用检查器、模块化和并发编程等。这本书不仅适合初学者,还可以作为更有经验的开发者的参考手册。 书中详细介绍了Rust的主要特性,例如所有权系统,它可以避免常见的内存错误,如空指针和数据竞争。同时,该书还着重介绍了Rust的错误处理机制和泛型编程。读者将学习如何使用Rust编写高效、安全和易于维护的代码。 《The Rust Programming Language》还包含许多实用的示例代码和练习,帮助读者通过实践加深对Rust的理解。此外,书中还介绍了一系列构建工具和库,以及有用的开发工作流程。 总之,《The Rust Programming Language》为学习和开发Rust编程语言的人们提供了清晰、全面的指南。无论您是初学者还是有经验的开发者,都可以从中受益,提高Rust编程的技能和效率。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值