赞
踩
在本章中将会介绍那些几乎在每种编程语言中都出现的概念,并告诉你它们在Rust中如何使用。大多数编程语言其实核心内容是类似的,本章中的概念并不是Rust独有。但是我们今天只介绍它们在Rust上下文环境下的表现,并按照惯例解释下如何使用这些概念。
特别说明,在本章中你会学习有关变量,基本类型,函数,注释和控制流。它们是每一个Rust程序的基础,早早的学习它们可以为你的Rust学习之旅提供强大的核心保障。
关键字
就像其它的编程语言一样,Rust有许多保留用的关键字。请始终牢记,任何情况下都不要将这些关键字用作你变量和函数的名字。大部分关键字都有特殊的意义,你的Rust程序使用它们来完成各种各样的任务;有一些关键字目前还不提供相关的功能,但它们也同样保留了下来,未来或许会被添加进Rust中。你可以在附录A中找到完整的关键字清单。
在第二章中,我们提到过,缺省情况下,变量是不可被修改的。这是众多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
我们被允许修改变量的值了,x
由5
变味了6
,当mut
使用的时候。某些情形下,你希望某个变量可修改,因为相比于不可修改变量,它能使你的代码更加易于书写。
对于可修改还是不可修改,你须要去好好权衡,考量。举个例子,如果你有一个结构,包含大量数据,直接修改它显然要比复制和创建新的实例来的更快。而数据量不大的结构,创建新的实例,并用类似函数化的结构来编程,可以使程序更易理解,通过一些小小的性能开销来获得更明晰健壮的程序,也是值得的。
让变量的值无法修改,可能会让你想起一个其它编程语言的概念:constants(常量)。就像不可修改的变量一样,常量也是指那种当某个值绑定到一个名字上后就不允许再做修改了。但是在Rust中,常量和变量是有区别的。
首先,你不能在常量上使用mut
,常量并不是缺省不可修改,而是始终无法修改。
你通过const
关键字声明一个常量而不是let
,并且在给常量赋值前必须明确标注值的类型。我们将在Data Types这一节中介绍类型和类型标注,这里你只须要了解到,要创建一个常量必须要标注值是哪种类型。
常量可以在任何作用域中定义,包括全局域,这使得当你的程序在很多地方须要引用到这个值时非常有用。
这里有一个常量定义的例子,我们定义了一个常量MAX_POINTS
并设置其值为100000(Rust的惯例是将常量的名字都用大写字母表示,并且单词间用下划线分隔来提高可读性)。
#![allow(unused_variables)]
fn main() {
const MAX_POINTS: u32 = 100_000;
}
在整个程序运行时中,常量在声明它的作用域总是有效的。常量非常适合用来保存那些会被程序多个部分频繁访问的值,举例来说游戏中允许玩家达到的最高分数或者光速的值都是很适合被定义为程序常量。
将你程序中那些硬代码写死的值替换为常量,会利于未来接盘的运维人员方便理解和推导程序的逻辑。如果未来这些值须要修改,你也只需要修改代码中常量的值即可。
你在我们第二章猜数字游戏中曾看到过,我们定义了一个和先前变量名字一样的变量,并且新的变量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了x
,2
乘以之前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_str
和spaces_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`
现在我们已经探索了变量是如何工作的,下面我们来看下更多变量可以使用的类型吧。
在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
你将会在下面看到许多不同数据类型的类型注释。
标量类型代表单独一个值,Rust有四大主要标量类型:整型、浮点数、波尔值和字符。你肯定在其它编程语言中也见过它们,那话不多说,我们来看下它们在Rust中是如何使用的。
整型是指那种没有小数位的数字,我们在第二章中使用过一个整型 u32
,表示其对应的值是一个无符号整数(有符合整数请用i
而不是u
)且占据32bits的空间。下面的表中展示了Rust中内建的整数类型,在Signed和Unsigned列中的整型都能用来定义一个整数值:
Length | Signed | Unsigned |
---|---|---|
8-bit | i8 | u8 |
16-bit | i16 | u16 |
32-bit | i32 | u32 |
64-bit | i64 | u64 |
128-bit | i128 | u128 |
arch | isize | usize |
每一个变量可以是无符号,也可以是有符号,并且有明确的尺寸大小。无符号和有符号主要取决于这个数字能否是正数或是负数,换句话说,如果一个数字有可能会是负数,那它就得使用有符号整型,反之如果它总是正值,那就可以使用无符号整型。就像你画在纸上的那样,有些数字前面可以有加号或者减号。但当你认为数字很安全且总是为正值时,还是请用无符号整型,因为带符号的数字在计算机中是以补码的方式存储的。
每一个有符号变量能存储-(2n - 1)到2n-1之间的数字,n是这个变量使用的比特数。所以,i8
可以存储-(27 - 1)到27-1之间的数字,即-128到127。相对比的,无符号数u8
可以存储0到28-1直接的数字即0到255。
此外,isize
和usize
这两个整数类型比较特殊,它们取决于你的程序跑在多少位的电脑上。当你的电脑是64位架构的时候,它们就占64bits,如果是32位架构,那就是32bits。
你可以像下表一样在Rust中直接输入整数字面量,但须要注意一点,除了字节字面量,其它所有方式都须要一个类型前缀,就像57u8
。此外,示例中的_
仅是用作视觉分隔符,是Rust用来方便阅读的,譬如1_000
:
Number literals | Example |
---|---|
Decimal | 98_222 |
Hex | 0xff |
Octal | 0o77 |
Binary | 0b1111_0000 |
Byte(u8 only) | b’A’ |
译者注:表中示例98_222 代表98222,而非98.222
那么问题来了,你怎么知道我们该用哪种整数类型?如果你不确定的话,没关系,Rust会缺省帮你做合适的选择——整型缺省是i32
,这种类型创建最为快速,即便是在64位的系统中。至于isize
和usize
,在对一些类型的collection(集)索引时会频繁使用到它们。
整数溢出
我们已经说过,如果你有一个u8
类型的变量,它只能存储0到255之间的值。如果你试图修改这个变量,给它一个超出这个范围的值,譬如256,这时就会发生整数溢出。对于这种行为,Rust有一些有趣的规则。当你在debug模式下编译程序时,Rust会有针对整数溢出的检查机制,当发现存在整数溢出的情况时,Rust会在你程序运行时panic(惊慌)。panic这个词是Rust用来表示程序由于错误而退出的情况,我们会在第九章中对panic做深入介绍。
当你在release模式下编译,即在编译时使用--release
这个标识,Rust就不会去检查整数溢出。取而代之的,Rust会做补码换行,简短的说,就是当给的值超过类型所能允许的最大值时,做类似取余的操作。譬如对于u8
类型,256会变成0,257则变成1。程序不会惊慌,但变量就会包含你没想到的值。依赖标准的整数溢出换行机制被认为是一种错误的方法,如果你明确想要做换行,请使用标准库中另一个类型Wrapping。
Rust有两个主要的浮点数类型,即包含小数部分的数字,f32
和f64
,分别代表32位大小和64位大小。缺省的类型是f64
,因为现代的CPU在处理速度上,对于f32
和f64
已经没有多大出入了,但f64
能提供更高的精度。
下面是Rust中使用浮点数的例子:
fn main() {
let x = 2.0; // f64
let y: f32 = 3.0; // f32
}
Rust中的浮点数是依据IEEE-754标准设计的,f32
代表单精度浮点数,f64
代表双精度浮点数。
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提供的操作符清单。
就像其它大部分编程语言,Rust中波尔类型也有两个值:true
和false
,波尔类型只占一个字节。在Rust中可以通过bool
来指明波尔类型:
fn main() {
let t = true;
let f: bool = false; // with explicit type annotation
}
波尔类型主要是在条件判断中使用,譬如if
表达式,我们会在控制流一节中介绍if
表达式。
目前我们只介绍了数字,Rust当然也支持字母。Rust中的char
类型是最基础的字符类型,下面的代码展示了某些使用字符的方法。(这里请注意,char
中的字母是通过单引号指定的,而字符串是通过双引号)
fn main() {
let c = 'z';
let z = 'ℤ';
let heart_eyed_cat = '声明:本文内容由网友自发贡献,转载请注明出处:【wpsshop博客】
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。