一、简介
Rust是Mozilla公司推出的一门全新的编程语言,1.0版本于2015年5月15日正式对外发布。作为多年来少有的新一代系统及编程语言,它的设计准则是“安全、并发、实用”。他的设计者是这样定位这门语言的:Rust的设计目标是要做一门系统编程语言,运行性能高、避免几乎所有的段(Segmentation Fault)和保证线程安全(Thread Safety)。这意味着Rust可以用于建造高效可靠的系统。
1、安全
Rust最重要的特点就是能提供内存安全保证,而且没有额外的性能损失。在传统的系统及编程语言(C/C++)的开过程中,经常出现因各种内存错误引起的崩溃或BUG,比如空指针、野指针、内存泄漏、内存越界、段错误、数据竞争、迭代器失效等。鉴于手动内存管理非常容易出问题,因而发明了垃圾回收机制(Garbage collection),但是不管使用哪种算法的GC系统,都会在性能上付出较大的代价。要么需要较大的运行时占用较大内存,要么需要暂停整个程序,要么具备不确定性的时延。Rust对自己的定位是接近芯片硬件的系统级编程语言。因此,他不可能选择使用自动垃圾回收的机制来解决问题。事实证明,要想解决内存安全的问题,小修小补是不够的,必须搞清楚导致内存错误的根本原因。从源头上解决。Rust就是为此而生的。
2、并发
在计算机单核性能越来越接近瓶颈的今天,多核并行成了提高软件执行效率的发展趋势。一些编程语言已经开始从语言层面支持并发编程,把“并发”的概念植入到了编程语言的血液中。然而,在传统的系统级编程语言中,并行代码很容易出错,而且有些问题很难复现,难以发现和解决问题,debug的成本非常高。线程安全问题一直以来都是非常令人头痛的问题。Rust当然也不会在这一重要领域落伍,它也非常好地支持了并发编程。更重要的是,在强大的内存安全特性的支持下,Rust一举解决了并发条件下的数据竞争(Data Race)问题。它从编译阶段就将数据竞争解决在了萌芽状态,保障了线程安全。Rust在并发方面还具有相当不错的可扩展性。所有跟线程安全相关的特性,都不是在编译器中写死的。用户可以用库的形式实现各种高效且安全的并发编程模型,进而充分利用多核时代的硬件性能。
3、实用
Rust并不只是实验室中的研究型产品,它的目标是解决目前软件行业中实实在在的各种问题。它的实用性体现在方方面面。Rust编译器的后端是基于著名的LLVM完成机器码生成和优化的,它只需要一个非常小巧的运行时即可工作,执行效率上可与C语言相媲美,具备很好的跨平台特性。Rust摈弃了手动内存管理带来的各种不安全的弊端,同时也避免了自动垃圾回收带来的效率损失和不可控性。在绝大部分情况下,保持了“无额外性能损失”的抽象能力。Rust具备比较强大的类型系统,借鉴了许多现代编程语言的历史经验,包含了众多方便“的语法特性。其中包括代数类型系统、模式匹配、闭包、生成器、类型推断、泛型、与C库ABI兼容、宏、模块管理机制、内置开源库发布和管理机制、支持多种编程范式等。它吸收了许多其他语言中优秀的抽象能力,海纳百川,兼容并蓄。在不影响安全和效率的情况下,拥有不俗的抽象表达力。
二、Rust安装
可以根据不同平台(Linux、macOS、Windows )参照Rust官方文档的安装方式,进行安装和设置环境变量。
Rust官方文档链接:https://doc.rust-lang.org/1.30.0/book/2018-edition/ch01-01-installation.html
中文版安装教程:https://rustlang-cn.org/users/book-exp/01%20Getting%20Started/01%20Getting%20Started.html
安装完毕,可采用以下命令验证,能显示出Rust版本即为安装成功:
$ rustc --version
或
$ rustup show
第一个Rust程序——Hello world
首先创建一个存放 Rust 代码的目录。并将你的所有项目存放在这里。
打开终端并输入如下命令创建 projects 目录,并在 projects 目录中为 Hello, world! 项目创建一个目录。
对于 Linux 和 macOS,输入:
$ mkdir ~/projects
$ cd ~/projects
$ mkdir hello_world
$ cd hello_world
编写并运行 Rust 程序
接下来,新建一个源文件,命名为 main.rs。Rust 源文件总是以 .rs 扩展名结尾。如果文件名包含多个单词,使用下划线分隔它们。例如命名为 hello_world.rs,而不是 helloworld.rs。
现在打开刚创建的 main.rs 文件,输入示例 1-1 中的代码。
fn main() {
println!("Hello, world!");
}
保存,进入终端输入命令rustc hello_world.rs编译,编译完成后输入./main运行程序
终端将会打印出 Hello,world!
三、编辑器
Rust IDE可以根据个人喜好采用vim、emacs、vscode、atom、sublime、Visual Studio等,我个人比较喜欢IntelliJ IDEA和Visual Studio.
四、Rust语言基础
(一)通用编程概念 Common Programming Concepts
1、关键字keywords
Rust 语言有一组保留的 关键字(keywords),就像大部分语言一样,它们只能由语言本身使用。记住,你不能使用这些关键字作为变量或函数的名称。大部分关键字有特殊的意义,你将在 Rust 程序中使用它们完成各种任务;一些关键字目前没有相应的功能,是为将来可能添加的功能保留的。可以在附录 A 中找到关键字的列表。
2、标识符identifier
这里我们将对本书中的一些概念做一些解释:变量、函数、结构体等等。所有这些都需要名称。Rust 中的名称被称为 “标识符”(“identifier”),它们可以是任意非空的 ASCII 字符串,不过有如下限制:
要么是:
第一个字符是字母。
其它字符是字母数字或者 _。
或者是:
第一个字符是 _。
标识符需多于一个字符。单独的 _ 不是标识符。
其它字符是字母数字或者 _。
*3、原始标识符raw identifier
有时出于某种原因你可能需要将关键字作为名称。比如你需要调用 C 语言库中名为 match 的函数,在 C 语言中 match 不是关键字。为此你可以使用 “原始标识符”(“raw identifier”)。原始标识符以 r# 开头:
let r#fn = "this variable is named 'fn' even though that's a keyword";
// 调用名为 'match' 的函数
r#match();
你无需经常用到原始标识符,但是当你 真正 需要它们时可以这么做。
(二)变量与可变性 Variables and Mutability
变量默认是不可改变的(immutable)。这是推动你以充分利用 Rust 提供的安全性和简单并发性来编写代码的众多方式之一。不过,你仍然可以使用可变变量。让我们探讨一下 Rust 拥抱不可变性的原因及方法,以及何时你不想使用不可变性。当变量不可变时,一旦值被绑定一个名称上,你就不能改变这个值。
1、变量和常量的区别
不允许改变值的变量,可能会使你想起另一个大部分编程语言都有的概念:常量(constants)。类似于不可变变量,常量是绑定到一个名称的不允许改变的值,不过常量与变量还是有一些区别。
首先,不允许对常量使用 mut。常量不光默认不能变,它总是不能变。声明常量使用 const 关键字而不是 let,并且必须注明值的类型。常量可以在任何作用域中声明,包括全局作用域,这在一个值需要被很多部分的代码用到时很有用。最后一个区别是,常量只能被设置为常量表达式,而不能是函数调用的结果,或任何其他只能在运行时计算出的值。
2、隐藏(Shadowing)
我们可以定义一个与之前变量同名的新变量,而新变量会 隐藏 之前的变量。Rustacean 们称之为第一个变量被第二个 隐藏 了,这意味着使用这个变量时会看到第二个值。可以用相同变量名称来隐藏一个变量,以及重复使用 let 关键字来多次隐藏。
隐藏与将变量标记为 mut 是有区别的。当不小心尝试对变量重新赋值时,如果没有使用 let 关键字,就会导致编译时错误。通过使用 let,我们可以用这个值进行一些计算,不过计算完之后变量仍然是不变的。
mut 与隐藏的另一个区别是,当再次使用 let 时,实际上创建了一个新变量,我们可以改变值的类型,但复用这个名字。例如,假设程序请求用户输入空格字符来说明希望在文本之间显示多少个空格,然而我们真正需要的是将输入存储成数字(多少个空格):
let spaces = " ";
let spaces = spaces.len();
(三)数据类型 Data Types
在 Rust 中,每一个值都属于某一个 数据类型(data type),这告诉 Rust 它被指定为何种数据,以便明确数据处理方式。我们将看到两类数据类型子集:标量(scalar)和 复合(compound)。记住,Rust 是 静态类型(statically typed)语言,也就是说在编译时就必须知道所有变量的类型。根据值及其使用方式,编译器通常可以推断出我们想要用的类型。当多种类型均有可能时,比如第二章的 “比较猜测的数字和秘密数字” 使用 parse 将 String 转换为数字时,必须增加类型注解
1、标量类型
标量(scalar)类型代表一个单独的值。Rust 有四种基本的标量类型:整型、浮点型、布尔类型和字符类型。你可能在其他语言中见过它们。让我们深入了解它们在 Rust 中是如何工作的。
1.1整型
整数 是一个没有小数部分的数字。我们在第二章使用过 u32 整数类型。该类型声明表明,它关联的值应该是一个占据 32 比特位的无符号整数(有符号整数类型以 i 开头而不是 u)。表格 3-1 展示了 Rust 内建的整数类型。在有符号列和无符号列中的每一个变体(例如,i16)都可以用来声明整数值的类型。
表格 3-1: Rust 中的整型
长度 有符号 无符号
8-bit i8 u8
16-bit i16 u16
32-bit i32 u32
64-bit i64 u64
每一个变体都可以是有符号或无符号的,并有一个明确的大小。有符号 和 无符号 代表数字能否为负值,换句话说,数字是否需要有一个符号(有符号数),或者永远为正而不需要符号(无符号数)。这有点像在纸上书写数字:当需要考虑符号的时候,数字以加号或减号作为前缀;然而,可以安全地假设为正数时,加号前缀通常省略。有符号数以补码形式(two’s complement representation)存储(如果你不清楚这是什么,可以在网上搜索;对其的解释超出了本书的范畴)。
每一个有符号的变体可以储存包含从 -(2n - 1) 到 2n - 1 - 1 在内的数字,这里 n 是变体使用的位数。所以 i8 可以储存从 -(27) 到 27 - 1 在内的数字,也就是从 -128 到 127。无符号的变体可以储存从 0 到 2n - 1 的数字,所以 u8 可以储存从 0 到 28 - 1 的数字,也就是从 0 到 255。
另外,isize 和 usize 类型依赖运行程序的计算机架构:64 位架构上它们是 64 位的, 32 位架构上它们是 32 位的。
可以使用表格 3-2 中的任何一种形式编写数字字面值。注意除 byte 以外的所有数字字面值允许使用类型后缀,例如 57u8,同时也允许使用 _ 做为分隔符以方便读数,例如1_000。
表格 3-2: Rust 中的整型字面值
数字 字面值例子
Decimal 98_222
Hex 0xff
Octal 0o77
Binary 0b1111_0000
Byte (u8 only) b'A'
那么该使用哪种类型的数字呢?如果拿不定主意,Rust 的默认类型通常就很好,数字类型默认是 i32:它通常是最快的,甚至在 64 位系统上也是。isize 或 usize 主要作为某些集合的索引。
整型溢出
比方说有一个 u8 ,它可以存放从零到 255 的值。那么当你将其修改为 256 时会发生什么呢?这被称为 “整型溢出”(“integer overflow” ),关于这一行为 Rust 有一些有趣的规则。当在 debug 模式编译时,Rust 检查这类问题并使程序 panic,这个术语被 Rust 用来表明程序因错误而退出。第九章会详细介绍 panic。
在 release 构建中,Rust 不检测溢出,相反会进行一种被称为 “two’s complement wrapping” 的操作。简而言之,256 变成 0,257 变成 1,依此类推。依赖溢出被认为是一种错误,即便可能出现这种行为。如果你确实需要这种行为,标准库中有一个类型显式提供此功能,Wrapping。
1.2 浮点型
Rust 也有两个原生的 浮点数(floating-point numbers)类型,它们是带小数点的数字。Rust 的浮点数类型是 f32 和 f64,分别占 32 位和 64 位。默认类型是 f64,因为在现代 CPU 中,它与 f32 速度几乎一样,不过精度更高。
这是一个展示浮点数的实例:
文件名: src/main.rs
fn main() {
let x = 2.0;
// f64
let y:f32 = 3.0;
// f32
}
浮点数采用 IEEE-754 标准表示。f32 是单精度浮点数,f64 是双精度浮点数。
1.3 数值运算
Rust 中的所有数字类型都支持基本数学运算:加法、减法、乘法、除法和取余。下面的代码展示了如何在 let 语句中使用它们:
文件名: src/main.rs
fn main() {
// 加法
let sum = 5+10;
// 减法
let difference = 95.5-4.3;
// 乘法
let product = 4*30;
// 除法
let quotient = 56.7/32.2;
// 取余
let remainder = 43%5;
}
这些语句中的每个表达式使用了一个数学运算符并计算出了一个值,然后绑定给一个变量。附录 B 包含 Rust 提供的所有运算符的列表。
1.4 布尔型
正如其他大部分编程语言一样,Rust 中的布尔类型有两个可能的值:true 和 false。Rust 中的布尔类型使用 bool 表示。例如:
文件名: src/main.rs
fn main() {
let t = true;
let f:bool = false;// 显式指定类型注解
}
使用布尔值的主要场景是条件表达式,例如 if 表达式。在 “控制流”(“Control Flow”)部分将介绍 if 表达式在 Rust 中如何工作。
1.5 字符类型
目前为止只使用到了数字,不过 Rust 也支持字母。Rust 的 char 类型是语言中最原生的字母类型,如下代码展示了如何使用它。(注意 char 由单引号指定,不同于字符串使用双引号。)
文件名: src/main.rs
fn main() {
let c = 'z';
let z = 'ℤ';
let heart_eyed_cat = '😻';}
Rust 的 char 类型代表了一个 Unicode 标量值(Unicode Scalar Value),这意味着它可以比 ASCII 表示更多内容。在 Rust 中,拼音字母(Accented letters),中文、日文、韩文等字符,emoji(绘文字)以及零长度的空白字符都是有效的 char 值。Unicode 标量值包含从 U+0000 到 U+D7FF 和 U+E000到 U+10FFFF 在内的值。不过,“字符” 并不是一个 Unicode 中的概念,所以人直觉上的 “字符” 可能与 Rust 中的 char 并不符合。第八章的 “字符串” 中将详细讨论这个主题。
2、复合类型
复合类型(Compound types)可以将多个值组合成一个类型。Rust 有两个原生的复合类型:元组(tuple)和数组(array)。
2.1 元组类型
元组是一个将多个其他类型的值组合进一个复合类型的主要方式。
我们使用包含在圆括号中的逗号分隔的值列表来创建一个元组。元组中的每一个位置都有一个类型,而且这些不同值的类型也不必是相同的。这个例子中使用了可选的类型注解:
文件名: src/main.rs
fn main() {
let tup:(i32,f64,u8) = (500,6.4,1);
}
tup 变量绑定到整个元组上,因为元组是一个单独的复合元素。为了从元组中获取单个值,可以使用模式匹配(pattern matching)来解构(destructure)元组值,像这样:
文件名: src/main.rs
fn main() {
let tup = (500,6.4,1);
let (x,y,z) = tup;
println!("The value of y is: {}",y);
}
程序首先创建了一个元组并绑定到 tup 变量上。接着使用了 let 和一个模式将 tup 分成了三个不同的变量,x、y 和 z。这叫做 解构(destructuring),因为它将一个元组拆成了三个部分。最后,程序打印出了 y 的值,也就是 6.4。
除了使用模式匹配解构外,也可以使用点号(.)后跟值的索引来直接访问它们。例如:
文件名: src/main.rs
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。
2.2 数组类型
另一个包含多个值的方式是 数组(array)。与元组不同,数组中的每个元素的类型必须相同。Rust 中的数组与一些其他语言中的数组不同,因为 Rust 中的数组是固定长度的:一旦声明,它们的长度不能增长或缩小。
Rust 中,数组中的值位于中括号内的逗号分隔的列表中:
文件名: src/main.rs
fn main() {
let a = [1,2,3,4,5];
}
当你想要在栈(stack)而不是在堆(heap)上为数据分配空间(第四章将讨论栈与堆的更多内容),或者是想要确保总是有固定数量的元素时,数组非常有用。但是数组并不如 vector 类型灵活。vector 类型是标准库提供的一个 允许 增长和缩小长度的类似数组的集合类型。当不确定是应该使用数组还是 vector 的时候,你可能应该使用 vector。第八章会详细讨论 vector。
一个你可能想要使用数组而不是 vector 的例子是,当程序需要知道一年中月份的名字时。程序不大可能会去增加或减少月份。这时你可以使用数组,因为我们知道它总是包含 12 个元素:
let months = ["January","February","March","April","May","June","July","August","September","October","November","December"];
数组的类型比较有趣;它看起来像 [type; number]。例如:
let a:[i32;5] = [1,2,3,4,5];
首先是方括号;这看起来像创建数组的语法。其中有两部分由分号分割的信息。第一部分是数组中每个元素的类型。因为所有元素都是相同类型的,所以只需列出一次。分号之后,是一个表示数组长度的数字。因为数组是固定长度的,该数字也一直保持不变,即便数组的元素被修改,它也不会增长或缩小。
2.3 访问数组元素
数组是一整块分配在栈上的内存。可以使用索引来访问数组的元素,像这样:
文件名: src/main.rs
fn main() {
let a = [1,2,3,4,5];
let first = a[0];
let second = a[1];
}
在这个例子中,叫做 first 的变量的值是 1,因为它是数组索引 [0] 的值。变量 second 将会是数组索引 [1] 的值 2。
2.4 无效的数组元素访问
如果我们访问数组结尾之后的元素会发生什么呢?比如你将上面的例子改成下面这样,这可以编译不过在运行时会因错误而退出:
文件名: src/main.rs
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:6
note: Run with `RUST_BACKTRACE=1` for a backtrace.
编译并没有产生任何错误,不过程序会出现一个 运行时(runtime)错误并且不会成功退出。当尝试用索引访问一个元素时,Rust 会检查指定的索引是否小于数组的长度。如果索引超出了数组长度,Rust 会 panic,这是 Rust 术语,它用于程序因为错误而退出的情况。
这是第一个在实战中遇到的 Rust 安全原则的例子。在很多底层语言中,并没有进行这类检查,这样当提供了一个不正确的索引时,就会访问无效的内存。通过立即退出而不是允许内存访问并继续执行,Rust 让你避开此类错误。
(四)函数 Function
函数遍布于 Rust 代码中。你已经见过语言中最重要的函数之一:main 函数,它是很多程序的入口点。你也见过 fn 关键字,它用来声明新函数。
Rust 代码中的函数和变量名使用 snake case 规范风格。在 snake case 中,所有字母都是小写并使用下划线分隔单词。
1、 函数参数
函数也可以被定义为拥有 参数(parameters),参数是特殊变量,是函数签名的一部分。当函数拥有参数(形参)时,可以为这些参数提供具体的值(实参)。技术上讲,这些具体值被称为参数(arguments),但是在日常交流中,人们倾向于不区分使用 parameter 和 argument 来表示函数定义中的变量或调用函数时传入的具体值。
2、 包含语句和表达式的函数体
函数体由一系列的语句和一个可选的结尾表达式构成。目前为止,我们只介绍了没有结尾表达式的函数,不过你已经见过作为语句一部分的表达式。因为 Rust 是一门基于表达式(expression-based)的语言,这是一个需要理解的(不同于其他语言)重要区别。其他语言并没有这样的区别,所以让我们看看语句与表达式有什么区别以及这些区别是如何影响函数体的。实际上,我们已经使用过语句和表达式。语句(Statements)是执行一些操作但不返回值的指令。表达式(Expressions)计算并产生一个值。
3、具有返回值的函数
函数可以向调用它的代码返回值。我们并不对返回值命名,但要在箭头(->)后声明它的类型。在 Rust 中,函数的返回值等同于函数体最后一个表达式的值。使用 return 关键字和指定值,可从函数中提前返回;但大部分函数隐式的返回最后的表达式。
(五)注释 Comments
所有程序员都力求使其代码易于理解,不过有时还需要提供额外的解释。在这种情况下,程序员在源码中留下记录,或者 注释(comments),编译器会忽略它们,不过阅读代码的人可能觉得有用。
(六)控制流 Control Flow
根据条件是否为真来决定是否执行某些代码,以及根据条件是否为真来重复运行一段代码是大部分编程语言的基本组成部分。Rust 代码中最常见的用来控制执行流的结构是 if 表达式和循环。
1、if 表达式
if 表达式允许根据条件执行不同的代码分支。你提供一个条件并表示 “如果条件满足,运行这段代码;如果条件不满足,不运行这段代码。
2、使用循环重复执行
多次执行同一段代码是很常用的,Rust 为此提供了多种 循环(loops)。一个循环执行循环体中的代码直到结尾并紧接着回到开头继续执行。为了实验一下循环,让我们新建一个叫做 loops 的项目。
Rust 有三种循环:loop、while 和 for。
小结:
通过以上的简要介绍,我们了解了变量、标量和复合数据类型、函数、注释、 if 表达式和循环!如果想要对以上概念有更深入的了解,最好动手多敲敲代码,从实践和错误中学习,能更好地加深记忆,对Rust语法也会有更好的理解。