Matrix

The fitrst thing we do,let's kill all the language lawyers.

—— Henry VI, Part II

The "programming language of the future"

Content

  • 万物之始(The beginning)
  • 基础(Basic)
  • 变量(Value)
  • 数据类型(Data type)
  • 注释(Code comments)
  • 函数(Function)
  • 条件语句(Conditional statements)
  • 循环(Loop)
  • 所有权(Ownership)
  • 切片(Slice)
  • 结构体(Struct)
  • 枚举类(Enumerate)
  • 组织管理(Project management)

Appendix

  • 0x01 RustRover相关配置
  • 0x02 Rust迭代器

The beginning

前言

那么……我们开始吧!想要学习一个编程语言,先得有一个趁手的工具。相信你可以自己配置Rust开发环境,我们就先介绍IDE了。

Rust的IDE有很多就像VSCode、RustRover、VIM等等,本书中将使用RustRover作为开发环境。

JetBrains RustRover目前是预览阶段,不收取订阅费

有关于RustRover安装配置已不再介绍,了解更多信息请参阅本书的Appendix部分(0x01)。

Starting With Chaos

Rust 语言是一种高效、可靠的通用高级语言。其高效不仅限于开发效率,它的执行效率也是令人称赞的,是一种少有的兼顾开发效率和执行效率的语言。

Rust 语言由 Mozilla 开发,最早发布于 2014 年 9 月。Rust 的编译器是在 MIT License 和 Apache License 2.0 双重协议声明下的免费开源软件。截至目前(2024 年 3 月)最新的编译器版本是 1.75.0(Stable)。

Rust 官方在线演练场: https://play.rust-lang.org/。

Basic

在C++中,我们的第一个程序往往是这样:

#include <iostream>
int main(){
std::cout << "Hello World!" ;
}

不推荐加上using namespace std,避免出现命名冲突。

这样的程序是C++的基本程序,作用极其简单,输出了Hello World!

而在Rust中,我们的程序就是这么写的:

fn main(){
println!("Hello World!");
} //fn main

之前的 Hello, World 程序中大概已经告诉了大家输出字符串的方式,但并不全面,

大家可能很疑惑为什么 println!("Hello World") 中的 println 后面还有一个!符号,难道 Rust 函数之后都要加一个感叹号?显然并不是这样。println 不是一个函数,而是一个宏规则。这里不需要更深刻的挖掘宏规则是什么,后面的章节中会专门介绍,并不影响接下来的一段学习。

Rust 输出文字的方式主要有两种:println!()print!()。这两个"函数"都是向命令行输出字符串的方法,区别仅在于前者会在输出的最后附加输出一个换行符。当用这两个"函数"输出信息的时候,第一个参数是格式字符串,后面是一串可变参数,对应着格式字符串中的"占位符",这一点与 C 语言中的 printf 函数很相似。但是,Rust 中格式字符串中的占位符不是 "% + 字母" 的形式,而是一对{}

fn main(){
let a = 10;
println!("a is : {}", a);
}

将上面的代码保存为demo.rsWindows + X单击终端(管理员)输入以下命令编译文件

rustc demo.rs

在目录下我们可以找到一个叫做demo.exe的文件以及一个叫demo.pdb的文件,这并不重要;回到终端,输入.\demo.exe,输出了Hello World!

注意:Powershell需要键入.\,cmd是不需要的。

如果我么想把a输出两遍呢?不会要这么写吧:

println!("a is : {}, a is again : {}", a , a);

其实有更好的写法:

println!("a is : {0}, a is again : {0}", a);

{}之间可以放一个数字,它将把之后的可变参数当作一个数组来访问,下标从 0 开始。

如果要输出{}怎么办呢?格式字符串中通过 {{}} 分别转义代表 {}。但是其他常用转义字符与 C 语言里的转义字符一样,都是反斜杠开头的形式。

例如:

// main.c
#include <stdio.h>
int main(){
int a = 10;
printf("a is %d\n", a);
printf("again, a is %d", a);
}
// main.rs
fn main(){
let a = 10;
println!("a is {}\n", a);
println!("again, a is {}", a);
}

在上面的两个例子中,我们体会到了Rust和C的转义字符。我们一定发现了上面代码中的

let a = 10;

接下来,我们就来详细地讨论Rust变量的设计。

Value

变量,基本类型,函数,注释和控制流,这些几乎是每种编程语言都具有的编程概念。

这些基础概念将存在于每个 Rust 程序中,及早学习它们将使你以最快的速度学习 Rust 的使用。

变量

首先必须说明,Rust 是强类型语言,但具有自动判断变量类型的能力。这很容易让人与弱类型语言产生混淆。

如果要声明变量,需要使用 let 关键字。例如:

let a = 5;

只学习过 JavaScript 的开发者对这句话很敏感,只学习过 C 语言的开发者对这句话很不理解。

在这句声明语句之后,以下三行代码都是被禁止的:

a = 1.01;  // a 从 i32 变为 f64 存在精度损失,Rust不允许精度损失的转换
a = "abc"; // a 是整型,此处赋值为字符
a = 10; // a 是不可变变量

前两种错误很容易理解,但第三个是什么意思?难道 a 不是个变量吗?

这就牵扯到了 Rust 语言为了高并发安全而做的设计:在语言层面尽量少的让变量的值可以改变。所以 a 的值不可变。但这不意味着 a 不是"变量"(英文中的 variable),官方文档称这种变量为不可变变量

如果我们编写的程序的一部分在假设值永远不会改变的情况下运行,而我们代码的另一部分在改变该值,那么代码的第一部分可能就不会按照设计的意图去运转。由于这种原因造成的错误很难在事后找到。这是 Rust 语言设计这种机制的原因。

当然,使变量变得"可变"(mutable)只需一个 mut 关键字。

let mut a = 123;
a = 456;

以上程序没有任何问题。

常量 & 不可变变量

既然不可变变量是不可变的,那不就是常量吗?为什么叫变量?

变量和常量还是有区别的。在 Rust 中,以下程序是合法的:

let a = 123;  // 通过编译,但可能出现警告:因为 a 没有使用
let a = 456;

但是我们这样编写代码,编译器会报错:

Rust中const定义的值必须声明类型。

const a = 12; //错误:const必须声明其类型
const b:i32 = 123; // 这段代码没有任何问题
const a:i32 = 12
let a = 14; // Error: 常量不可赋值

另外,我们可以试试这样写:

fn main() {
let a = 123;
println!("Then, a is {}", a); let a = 456;
println!("Now, a is {}", a);
}

输出了如下的结果:

Then, a is 123
Now, a is 456

是不是很惊讶?一个量竟然可以输出两个值!没什么神秘的,这其实就是Rust的“重新绑定”,变量的值可以"重新绑定",但在"重新绑定"以前不能私自被改变,这样可以确保在每一次"绑定"之后的区域里编译器可以充分的推理程序逻辑。 虽然 Rust 有自动判断类型的功能,但有些情况下声明类型更加方便:

let a:u64 = 123;

这里声明了 a 为无符号 64 位整型变量,如果没有声明类型,a 将自动被确定为有符号 32 位整型变量,这对于 a 的取值范围有很大的影响。

重影(Shadowing)

重影的概念与其他面向对象语言里的"重写"(Override)或"重载"(Overload)是不一样的。重影就是刚才讲述的所谓"重新绑定",之所以加引号就是为了在没有介绍这个概念的时候代替一下概念。

重影就是指变量的名称可以被重新使用的机制:

fn main() {
    let x = 5; // x = 5
    let x = x + 1; // x = 6
    let x = x * 2; // x = 12
    println!("The value of x is: {}", x);
}

输出以下的内容:

The value of x is: 12

重影与可变变量的赋值不是一个概念,重影是指用同一个名字重新代表另一个变量实体,其类型、可变属性和值都可以变化。但可变变量赋值仅能发生值的变化。

let mut string = "123";
string = string.len(); // 错误:不能给字符赋值整型

这段程序会出错。

数据类型(Data type)

前言

Brevity is the soul of wit. ——Shakespeare, Hamlet 2.1

简洁是智慧的灵魂,冗长是肤浅的藻饰。

现代编程语言中有一个普遍的特性就是类型后置,比如刚才的那几个例子:

let a:i64 = 123;
let b:u64 = 456;
let c:f64 = 789.01;

类型后置的特点拯救了程序员的双眼,相比于C/C++,这样的设计就会好些。就像是那年数星星和月亮的浪漫生活(指针)。

整数型

整数型简称整型,按照比特位长度和有无符号分为以下种类:

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

isizeusize 两种整数类型是用来衡量数据大小的,它们的位长度取决于所运行的目标平台,如果是 32 位架构的处理器将使用 32-bit 位长度整型。

很显然,有的整数中间存在一个下划线,这种设计可以让人们在输入一个很大的数字时更容易判断数字的值大概是多少。

实例

fn main() {
let a = 0xFFFF; // hexadecimal
let b = 12_3; // decimal
let c = 0o77; // octal
let d = 0b100; // binary
let e = b'A'; // byte println!("{0} , {1} , {2} , {3} , {4}",a ,b ,c ,d ,e);
}

以上代码输出如下结果:

65535 , 123 , 63 , 4 , 65

可以看到,输出结果自动化为了十进制 (以及ASCII码);以上的变量(a ~ d)都自动判断为i32类型,但是变量e被确定为u8类型。

浮点数型(Floating-Point)

Rust 与其它语言一样支持 32 位浮点数(f32)和 64 位浮点数(f64)。默认情况下,64.0 将表示 64 位浮点数,因为现代计算机处理器对两种浮点数计算的速度几乎相同,但 64 位浮点数精度更高。

fn main(){
let x = 2.0; // f64
let y = 3.0; // f32
}

数学运算

用一段程序反映数学运算:

fn main(){
let sum = 5 + 10; // 加法
let difference = 95.5 - 5.5; // 减法
let product = 4 * 30; // 乘法
let qiotient = 56 / 8; // 除法
let remainder = 43 % 5; // 求余
}

许多运算符号之后加上=号是自运算的意思,例如:

sum += 1 等同于 sum = sum + 1

注意:Rust 不支持 ++--,因为这两个运算符出现在变量的前后会影响代码可读性,减弱了开发者对变量改变的意识能力。这个之后会说(见附录0x02)

布尔值(bool)

布尔型用bool表示,值只能为 truefalse

例如:

fn main(){
let is_open = ture;
}

字符型(char)

在Rust中,字符型用 char 表示。

Rust的 char 类型大小为 4 个字节,代表 Unicode 标量值,这意味着它可以支持中文,日文和韩文字符等非英文字符甚至表情符号和零宽度空格在 Rust 中都是有效的 char 值。

Unicode 值的范围从 U+0000U+D7FFU+E000U+10FFFF (包括两端)。 但是,"字符"这个概念并不存在于 Unicode 中,因此您对"字符"是什么的直觉可能与Rust中的字符概念不匹配。所以一般推荐使用字符串储存 UTF-8 文字(非英文字符尽可能地出现在字符串中)。

注意:由于中文文字编码有两种(GBKUTF-8),所以编程中使用中文字符串有可能导致乱码的出现,这是因为源程序与命令行的文字编码不一致,所以在 Rust 中字符串和字符都必须使用 UTF-8 编码,否则编译器会报错。

符合类型(Composite type)

1.元组

元组是用一对 ( ) 包括的一组数据,可以包含不同种类的数据:

fn main(){
let tup:(i64,f64,u16) = (12, 1.05, 8);
//tup.0 -> 12 [i64]
//tup.1 -> 1.0 [f64]
//tup.2 -> 8 [u16]
let (x, y, z) = tup;
println!("x is {0}, y is {1}, z is {2}",x,y,z);
}

似乎不能将tup的第二项设为1.0,最后输出会略去0

以上代码输出:

x is 12, y is 1.05, z is 8

2.数组

数组是用一对[ ]包括的相同数据类型。

fn main() {
let a = [1,2,3,4,5,6,7,8,9,10];
// a 是一个长度为 10 的整型数组 let b = ["January","February","March","April","May","June"];
// b 是一个长度为 6 的 &str 数组 let c:[i32; 5] = [11,12,13,14,15];
// c 是一个长度为 5 的 i32 数组 let d = [5; 10];
// 等于 let d = [5 , 5 , 5 , 5 , 5 , 5 , 5 , 5 , 5 , 5];
}

数组a,d会被确定为i32类型

数组b会被确定为&str类型

数组包含了多个类型相同的项,而元组包含的是多个类型不同的项。

3.数组访问

数组是可以访问的:

fn main() {
let a = [1,2,3,4,5,6,7,8,9,10];
let b = ["January","February","March","April","May","June"];
let c:[i32; 5] = [11,12,13,14,15];
let d = [5; 10]; // 数组访问
let first = a[0];
let second = a[1];
}

4.常见数组错误

let a = [1, 2, 3];
a[0] = 123; // Error:数组 a 不可变
let mut a = [1, 2, 3];
a[0] = 4; // 这段代码没有任何问题,数组 a 的第 0 项会被赋值为 4

注释(Code comments)

Rust 中的注释方式与其它语言(C、Java)一样,支持两种注释方式

// 这是一个注释

/* 这也是注释 */

/*
* 这又是注释
*/

用于说明文档的注释

在 Rust 中使用 // 可以使其之后到第一个换行符的内容变成注释。

在这种规则下,三个正斜杠 /// 依然是合法的注释开始。所以 Rust 可以用 /// 作为说明文档注释的开头:

/// # fn main
/// 函数功能:输出"Hello World!"
/// 返回值:无
fn main() {
println!("Hello World!");
return; // 此处的return没有任何意义
}

Cargo 具有 cargo doc 功能,开发者可以通过这个命令将工程中的说明注释转换成 HTML 格式的说明文档。

函数(Function)

函数在 Rust 语言中是普遍存在的。

通过之前的章节已经可以了解到 Rust 函数的基本形式:

fn <函数名> ( <参数> ) <函数体>

其中 Rust 函数名称的命名风格是小写字母以下划线分割 (snake_case) :

fn main() {
    println!("Hello, World!");
    sub_function();
} fn sub_function() {
    println!("Hello, Rust!");
}

输出以下的结果:

Hello, World!
Hello, Rust!

注意,我们在源代码中的 main 函数之后定义了sub_function。 Rust不在乎函数在哪里被定义,只需在某个地方定义它们即可。

函数参数

Rust 中定义函数如果需要具备参数必须声明参数名称和类型:

fn main() {
    sub_function(5, 10);
} fn sub_function(x: i32, y: i32) {
    println!("x is : {}", x);
    println!("y is : {}", y);
}

以上函数会输出:

x is 5
y is 10

学习了这些,我们不妨来编写一个加法函数吧!

fn main() {
let result = _add(12, 13);
println!("结果是:{}", result);
}
fn _add(a:i64, b:i64) -> i64 {
return a + b;
}

有两个函数,main_add。前者执行后会输出结果,后者将两个参数相加后并返回。在main里面,我们声明了result变量,println!输出了这个变量的值。

我们并不需要在乎_add的函数表达式,这样写可以告诉编译器这个函数会返回i64的值。

语句和表达式

Rust 函数体由一系列可以以表达式(Expression)结尾的语句(Statement)组成。到目前为止,我们仅见到了没有以表达式结尾的函数,但已经将表达式用作语句的一部分。

语句是执行某些操作且没有返回值的步骤。例如:

let a = 10;

上面的代码没有返回值,是语句

下面的语句是错误的,这个步骤没有返回值。

let a = (let b = 2);

有些操作是有返回的,比如:

a = 5
b = 10
c = 15

这些步骤有返回值,所以它们是表达式

表达式有计算步骤且有返回值。以下是表达式(假设出现的标识符已经被定义):

a = 7
b + 2
c * (a + b)

Rust 中可以在一个用 {} 包括的块里编写一个较为复杂的表达式:

fn main() {
let x = 5; let y = {
let x = 3;
x + 1
}; println!("x 为 : {}", x);
println!("y 为 : {}", y);
}

这个函数包含了一个表达式块:

{
let x = 3;
x + 1
}

而且在块中可以使用函数语句,最后一个步骤是表达式,此表达式的结果值是整个表达式块所代表的值。这种表达式块叫做函数体表达式

注意:x + 1 之后没有分号,否则它将为不可变变量a赋值。

这种表达式块是一个合法的函数体。而且在 Rust 中,函数定义可以嵌套:

fn main() {
fn ten() -> i32 {
10
}
println!("ten() 为: {}", ten());
}

函数返回值

在之前,我们已经遇到过函数返回值了, Rust 函数声明返回值类型的方式:在参数声明之后用->来声明函数返回值的类型。

在函数体中,随时都可以以return关键字结束函数运行并返回一个类型合适的值。

比如:

fn _add(a:i64, b:i64) -> i64{
return a + b;
}

但是 Rust 不支持自动返回值类型判断(C++ 11 auto),如果没有明确声明函数返回值的类型,函数将被认为是"纯过程",不允许产生返回值,return 后面不能有返回值表达式。这样做的目的是为了让公开的函数能够形成可见的公报。

条件语句(Conditional statements)

在 Rust 语言中的条件语句是这样的:

fn main() {
let number = 3;
if number < 5 {
println!("条件为 true");
} else {
println!("条件为 false");
}
}

在上述程序中有条件 if 语句,这个语法在很多其它语言中很常见,但也有一些区别:首先,条件表达式 number < 5 不需要用小括号包括(不需要不是不允许);但是 Rust 中的 if 不存在单语句不用加 {} 的规则,不允许使用一个语句代替一个块。尽管如此,Rust 还是支持传统 else-if 语法的:

fn main() {
let a = 12;
let b;
if a > 0 {
b = 1;
}
else if a < 0 {
b = -1;
}
else {
b = 0;
}
println!("b is {}", b);
}

输出以下文字:

b 为 1

Rust 中的条件表达式必须是 bool 类型,例如下面的程序是错误的:

fn main() {
let number = 3;
if number {
println!("Yes");
}
}

虽然 C/C++ 语言中的条件表达式用整数表示,非 0 即真,但这个规则在很多注重代码安全性的语言中是被禁止的。

结合之前,我们加以联想:

if <condition> { block 1 } else { block 2 }

这种语法中的 { block 1 }{ block 2 } 可不可以是函数体表达式呢?

答案是肯定的!也就是说,在 Rust 中我们可以使用 if-else 结构实现类似于三元条件运算表达式 (A ? B : C) 的效果:

fn main() {
let a = 3;
let number = if a > 0 { 1 } else { -1 };
println!("number 为 {}", number);
}

输出结果:

number 为 1

注意:两个函数体表达式的类型必须一样,且必须有一个 else 及其后的表达式块。

新势力语言中,往往都没有支持三元运算符,比如09年的Go,10年的Rust,它们都没有支持三元运算符。Go甚至没有相应的写法!Rust的 if-else 与 C/C++ 的 if-else区别在于:前者是表达式,后者是语句。语句是没有返回值的,所以在C++里编写:

int a = 10;
int b = if (a > 10){ b = 10 } else { b = 0 }

是错误的,语句不能为变量赋值,因为其无返回值。

现在,让我们来试试if-else吧!

fn main() {
let a = 0;
println!("Then a is : {}", a); let now = if a > 0 { 0 } else { 1 };
println!("Now a is : {}", now);
} // fn main

通过观察上面的代码,我们不难看出。

第一步,不变量a被声明为0println!输出a的值

第二步,我们声明了now的值为表达式if a > 0 { 0 } else { 1 }的值。

该表达式判断a的值是否大于0,大于将其设为0,反之设为1。

最后,println!输出了重影(Shadowing)后的a的值。

如果没有编译出错,该程序将会输出以下结果:

Then a is : 0
Now a is : 1

Tips:

如果cargo报出了warning: crate "xxxx" should have a snake case name 的警告,可以不理会,这个只是cargo警告该项目/包的命名不符合Rust的命名风格。冰不是编译错误。

  • "snake-case": 蛇形命名。比如fetch_datamainget_data
  • "camel-case/Camel-case": 大/小驼峰命名。比如getUsernameGetProcessInit

    过多的命名方式不再赘述。

    参考 程序变量命名的几种叫法(知乎)

循环(Loop)

Rust 除了灵活的条件语句以外,循环结构的设计也很成熟。

while循环

while 循环是最典型的条件语句循环:

fn main() {
let mut number = 0;
while number != 10{
println!("Now, number is {}", number);
number += 1;
}
println!("进程退出");
}

如果没有出什么岔子的话,这段程序会输出:

Now, number is 0
Now, number is 1
Now, number is 2
Now, number is 3
Now, number is 4
Now, number is 5
Now, number is 6
Now, number is 7
Now, number is 8
Now, number is 9
进程退出

Rust 现在(2024年2月4日)还没有 do-while 的用法,但是 do 被规定为保留字,也许以后的版本中会用到。

就像是这样:

let do = 120; //Error: do 是保留关键字

在 C 语言中 for 循环使用三元语句控制循环,但是 Rust 中没有这种用法,需要用 while 循环来代替:

int i;
for (i = 0; i < 10; i++) {
    /* 循环体 */
}
let mut i = 0;
while i < 10 {
    /* 循环体 */
    i += 1;
}

for循环

for 循环是最常用的循环结构,常用来遍历一个线性数据结构(比如数组)。for 循环遍历数组:

fn main() {
let a = [1,2,3,4,5];
for number in a.iter() {
println!("{}",number);
}
}

以上程序输出以下结果:

1
2
3
4
5

这个程序中的for循环完成了对数组a的遍历。a.iter() 代表 a 的迭代器(iterator),在学习有关于对象的章节以前不做赘述。

当然,for 循环其实是可以通过下标来访问数组的:

fn main() {
let a = [1,2,3,4,5,6,7,8,9,10]; let mut item = 10;
for number in 1..item {
println!("{}",number);
}
}

与这段代码等效:

fn main() {
let a = [1,2,3,4,5,6,7,8,9,10]; for number in 1..10 {
println!("{}",number);
}
}

区别在于for循环的次数表达方式。

loop循环

有些时候我们可能遇到过几次这样的情况:某个循环无法在开头和结尾判断是否继续进行循环,必须在循环体中间某处控制循环的进行(break)。如果遇到这种情况,我们经常会在一个 while (true) 循环体里实现中途退出循环的操作。

Rust 特有的原生无限循环结构 —— loop

fn main() {
let string = ['T','s','u','n','a','m','i','.']; // Tsunami let mut iter = 0;
loop {
let char = string[iter];
println!("'{}'", char);
if char == '.' {
break //跳出循环
}
iter += 1
}
}

以上代码输出了Tsunami这个英语单词。

loop 循环可以通过 break 关键字类似于 return 一样使整个循环退出并给予外部一个返回值。一个精妙的设计!所以 loop 循环常被用来当作查找工具使用,如果找到了某个东西当然要将这个结果交出去,所以:

fn main() {
let string = ['T','s','u','n','a','m','i','.']; // Tsunami let mut iter = 0;
let index_n = loop {
let char = string[iter];
iter += 1; /* 查找字符'n'的位置*/
if char == 'n'{
break iter;
}
};
println!("字符'n'的索引是 {}", index_n);
}

或者这样,也能输出相同的结果:

fn main() {
let string = ['T','s','u','n','a','m','i','.']; // Tsunami let mut iter = 0;
loop {
let char = string[iter];
iter += 1;
/* 查找字符'n'的位置*/
if char == 'n'{
break
}
};
println!("字符'n'的索引是 {}", iter);
}

不能将let mut iter = 0;放到loop里面去,否则每次开始循环都会重新初始化iter,导致输出错误的结果。

另外,这样做也会导致iter没法被后面的println!获取到……以至于报错。

更多有关与声明的问题,我们放到 所有权(ownership) 里面讲述

所有权(ownership)

前言

What's past is prologue.

—— Shakespeare

简介

计算机程序必须在运行时管理它们所使用的内存资源。

大多数的编程语言都有管理内存的功能

C/C++

主要通过手动方式管理内存,开发者需要手动的申请和释放内存资源。

但为了提高开发效率,只要不影响程序功能的实现,许多开发者没有及时释放内存的习惯。

所以手动管理内存的方式常常造成资源浪费。

Java

程序在虚拟机(JVM)中运行,JVM 具备自动回收内存资源的功能。

但这种方式常常会降低运行时效率,所以 JVM 会尽可能少的回收资源,这样也会使程序占用较大的内存资源。

所有权对大多数开发者而言是一个新颖的概念,它是 Rust 语言为高效使用内存而设计的语法机制。

所有权概念是为了让 Rust 在编译阶段更有效地分析内存资源的有用性以实现内存管理而诞生的概念。

所有权规则

所有权有以下三条规则:

  • Rust 中的每个值都有一个变量,称为其所有者。
  • 一次只能有一个所有者。
  • 当所有者不在程序运行范围时,该值将被删除。

接下来,就让我们开始学习吧!首先是变量范围

变量范围

我们用下面这段程序描述变量范围的概念:

{   // 此处不能使用"value"

    // 此处使用变量"value"无效

    let value = "Tsunami";

    // 此处可以使用变量"value"

}   // 此处已不再可以使用变量"value"

变量范围是变量的一个属性,其代表变量的可行域,默认从声明变量开始有效直到变量所在域结束。

说到这个,我们不妨看看我们之前查找字符索引的实例:

fn main() {
let string = ['T','s','u','n','a','m','i','.']; // Tsunami let mut iter = 0;
loop {
let char = string[iter];
iter += 1;
/* 查找字符'n'的位置*/
if char == 'n'{
break
}
};
println!("字符'n'的索引是 {}", iter);
}

将它变为:

fn main() {
let string = ['T','s','u','n','a','m','i','.']; // Tsunami loop {
let mut iter = 0;
let char = string[iter];
iter += 1;
/* 查找字符'n'的位置*/
if char == 'n'{
break
}
};
println!("字符'n'的索引是 {}", iter);
}

编译器报错了,说明程序发生了错误。这里就涉及到所有权,可变变量iterloop中声明,当在loop外面访问iter时,不在iter的可访问区域,所以编译器报出了"cannot find value "iter" in this scope"的错误。

内存和内存分配

如果我们定义了一个变量并给它赋予一个值,这个变量的值存在于内存中。这种情况很普遍。但如果我们需要储存的数据长度不确定(比如用户输入的一串字符串),我们就无法在定义时明确数据长度,也就无法在编译阶段令程序分配固定长度的内存空间供数据储存使用。(有人说分配尽可能大的空间可以解决问题,但这个方法很不文明)。这就需要提供一种在程序运行时程序自己申请使用内存的机制——堆。本章所讲的所有"内存资源"都指的是堆所占用的内存空间。

有分配就有释放,程序不能一直占用某个内存资源。因此决定资源是否浪费的关键因素就是资源有没有及时的释放。

{
char *str = strdup("tsunami");
free(str); // 释放 s 资源
}

很显然,Rust 中没有调用 free 函数来释放字符串 str 的资源(在 C 中是不正确的写法,因为 "tsunami" 不在堆中)。Rust 之所以没有明示释放的步骤是因为在变量范围结束的时候,Rust 编译器自动添加了调用释放资源函数的步骤。

这种机制看似很简单了:它不过是帮助程序员在适当的地方添加了一个释放资源的函数调用而已。但这种简单的机制可以有效地解决一个史上最令程序员头疼的编程问题。

变量与数据交互的方式

变量与数据交互方式主要有移动(Move)和克隆(Clone)两种:

Move

多个变量可以在 Rust 中以不同的方式与相同的数据交互:

let x = 10;
let y = x;

这个程序将值 10 绑定到变量 x,然后将 x 的值复制并赋值给变量 y。现在栈中将有两个值 10。此情况中的数据是"基本数据"类型的数据,不需要存储到堆中,仅在栈中的数据的"移动"方式是直接复制,这不会花费更长的时间或更多的存储空间。"基本数据"类型有这些:

  • 所有整数类型(如 i32u32i64)
  • 布尔类型 bool(值为 truefalse )
  • 所有浮点类型(f32f64)
  • 字符类型 char
  • 仅包含以上类型数据的元组(Tuples

但如果发生交互的数据在堆中就是另外一种情况:

let str1 = String::from("Hello");
let str2 = str1;

第一步产生一个 String 对象,值为 "hello"。其中 "hello" 可以认为是类似于长度不确定的数据,需要在堆中存储。

第二步的情况略有不同(这张图仅供参考,真实情况可能比这复杂的多):



图所示:两个 String 对象在栈中,每个 String 对象都有一个指针指向堆中的 "Hello" 字符串。在给 str2 赋值时,只有栈中的数据被复制了,堆中的字符串依然还是原来的字符串。

前面我们说过,当变量超出范围时,Rust 自动调用释放资源函数并清理该变量的堆内存。但是 str1str2 都被释放的话堆区中的 "Hello" 被释放两次,这是不被系统允许的。为了确保安全,在给 str2 赋值时 str1 已经无效了。

没错,在把 str1 的值赋给 str2 以后 str1 将不可以再被使用。就像是一次性的,所以下面这段程序是错的:

fn main() {
let str1 = String::from("Hello");
let str2 = str1; println!("{}", str1); // Error: borrow of moved value: `str1`
}

我们上面提到,str1在给str2赋值的时候就已经不可用了,当我们试图访问它时,编译器丢出了错误。

这个程序就是正确的,因为它没有使用不可用的str1

fn main() {
let str1 = String::from("Hello");
let str2 = str1; println!("{}", str2);
}

此时的情况是:

消失的它 (指str1)

Clone

Rust会尽可能地降低程序的运行成本,所以默认情况下,长度较大的数据存放在堆中,且采用移动的方式进行数据交互。但如果需要将数据单纯的复制一份以供他用,可以使用数据的第二种交互方式 —— 克隆。

回到刚才的例子中,我们不希望str1消失在茫茫数据中,我们可以使用clone。就像这样:

fn main() {
let str1 = String::from("Hello");
let str2 = str1.clone(); println!("str 1 : {0}, str 2 : {1}",str1 , str2);
}

现在,这个程序就会输出正确的结果。

这里是真的将堆中的 "Hello" 复制了一份,所以 str1str2 都分别绑定了一个值,释放的时候也会被当作两个资源。

当然,克隆仅在需要复制的情况下使用,毕竟复制数据会花费更多的时间。

涉及函数的所有权机制

对于变量来说这是最复杂的情况了。

如果将一个变量当作函数的参数传给其他函数,怎样安全的处理所有权呢?

下面这段程序描述了这种情况下所有权机制的运行原理:

fn main() {
    let s = String::from("Hello");
    // s 被声明有效     takes_ownership(s);
    // s 的值被当作参数传入函数
    // 所以可以当作 s 已经被移动,从这里开始已经无效     let x = 5;
    // x 被声明有效     makes_copy(x);
    // x 的值被当作参数传入函数
    // 但 x 是基本类型,依然有效
    // 在这里依然可以使用 x 却不能使用 s } // 函数结束, x 无效, 然后是 s. 但 s 已被移动, 所以不用被释放 fn takes_ownership(_string: String) {
    // 一个 String 参数 _string 传入,有效
    println!("{}", _string);
} // 函数结束, 参数 _string 在这里释放 fn makes_copy(_integer: i32) {
    // 一个 i32 参数 _integer 传入,有效
    println!("{}", _integer);
} // 函数结束, 参数 _integer 是基本类型, 无需释放

其实就是这样:

如果将变量当作参数传入函数,那么它和移动的效果是一样的。

函数返回值的所有权机制

fn main() {
let str1 = gives_owership();
// str1有效,值来自函数的返回值 let str2 = String::from("Hello");
// str2被声明,有效 let str3 = takes_and_give_back(str2);
// str2被移动,无效;str3有效;str1有效 } // str3被释放;str1被释放。 fn takes_and_give_back(a_string:String) -> String {
a_string
// a_string被移出本函数
} fn gives_owership() -> String {
let _string = String::from("Hello");
// _string被声明且有效 return _string;
// _string被当作返回值移出本函数 }

被当作函数返回值的变量所有权将会被移动出函数并返回到调用函数的地方,而不会直接被无效释放。

引用与租借

引用(Reference)是 C++ 开发者较为熟悉的概念。

如果你熟悉指针的概念,你可以把它看作一种指针。

实质上"引用"是变量的间接访问方式。

fn main() {
let str1 = String::from("Hello");
let str2 = &str1; println!("{},{}",str1 ,str2);
}

这与clone很相似,但是它又和clone有一点区别。

我们用&表示引用。

当一个变量的值被引用时,变量本身不会被认定无效。因为"引用"并没有在栈中复制变量的值:

fn main() {
let str1 = String::from("Hello");
let str2 = &str1; println!("{},{}",str1 ,str2);
}

就像是这样:



它们的区别在于:

  • clone的两个量都指向堆(Heap)中的量。
  • 引用只有被引用量指向堆(Heap)中的量,引用量指向被引用的量。

函数参数传递的道理一样:

fn main() {
let str1 = String::from("Hello");
let len = calc_len(&str1); println!("{}'s length is {}", str1, len);
} fn calc_len(string: &String) -> usize {
string.len()
}

输出:

Hello's length is 5

引用不会获得值的所有权。

引用只能租借(Borrow)值的所有权。

引用本身也是一个类型并具有一个值,这个值记录的是别的值所在的位置,但引用不具有所指值的所有权:

fn main() {
let str1 = String::from("Hello");
let str2 = &str1;
let str3 = str1; println!("{}",str2);
}

这段程序不正确:因为 str2 租借的 str1 已经将所有权移动到 str3,所以 str2 将无法继续租借使用 str1 的所有权。如果需要使用 str2 使用该值,必须重新租借:

fn main() {
let str1 = String::from("Hello");
let mut str2 = &str1;
let str3 = str1; str2 = &str3;
println!("{}",str2);
}

此程序没有任何问题。

刚才我们遇到的问题就是这样的:



我们重新让str2获得了所有权,谋权篡位不是吗?

既然引用不具有所有权,即使它租借了所有权,它也只享有使用权(这跟租房子是一个道理)。

如果尝试利用租借来的权利来修改数据会被阻止:

fn main() {
let str1 = String::from("Hello");
let mut str2 = &str1;
println!("{}",str2); str2.push_str("Tsunami"); // Error: 租借的值不可变,除非声明可变(mut)
println!("{}",str2);
}

这段程序中 s2 尝试修改 s1 的值被阻止,租借的所有权不能修改所有者的值。

当然,也存在一种可变的租借方式,就像你租一个房子,如果物业规定房主可以修改房子结构,房主在租借时也在合同中声明赋予你这种权利,你是可以重新装修房子的:

fn main() {
let mut str1 = String::from("Hello");
// str1可变 let mut str2 = & mut str1;
// 声明引用可变
println!("{}",str2); // 这段是正确的
str2.push_str(" ,Tsunami");
println!("{}",str2);
}

这段程序就没有问题了。我们用 &mut 修饰可变的引用类型。

可变引用与不可变引用相比除了权限不同以外,可变引用不允许多重引用,但不可变引用可以:

fn main() {
let mut str = String::from("Tsunami"); let str2 = &mut str;
let str3 = &mut str; println!("{},{}",str2,str3); // Error: 可变引用不可以多重引用
}

这段程序不正确,因为多重可变引用了 str

Rust 对可变引用的这种设计主要出于对并发状态下发生数据访问碰撞的考虑,在编译阶段就避免了这种事情的发生。

由于发生数据访问碰撞的必要条件之一是数据被至少一个使用者写且同时被至少一个其他使用者读或写,所以在一个值被可变引用时不允许再次被任何引用。

垂悬引用 (Darling References)

这是一个换了个名字的概念,如果放在有指针概念的编程语言里它就指的是那种没有实际指向一个真正能访问的数据的指针(注意,不一定是空指针,还有可能是已经释放的资源)。它们就像失去悬挂物体的绳子,所以叫"垂悬引用"。

"垂悬引用"在 Rust 里不允许出现,如果有,编译器会发现它。

fn main() {
let reference_to_nothing = dangle();
} // nightly的Rust要求声明返回值的声明周期,所以加上了static;一般的stable就不需要了
fn dangle() -> &'static String {
let s = String::from("Tsunami");
&s
}

很显然,伴随着dangle函数的结束,其局部变量的值本身没有被当作返回值,被释放了。但它的引用却被返回,这个引用所指向的值已经不能确定的存在,故不允许其出现。

切片(Slice)

Who wants a stylus?

I do.

Who wants SLICEs?

Nobody wants.

Okay,that's a tasteless joke.

切片(Slice)是对数据值的部分引用。

切片这个名字往往出现在生物课上,我们做样本玻片的时候要从生物体上获取切片,以供在显微镜上观察。在 Rust 中,切片的意思大致也是这样,只不过它从数据取材引用。

字符串切片

最简单、最常用的数据切片类型是字符串切片(String Slice)。

fn main() {
let str = String::from("Tsunami"); let part1 = &str[0..2];
let part2 = &str[3..7]; println!("{} = {} + {}",str, part1, part2);
}

输出:

Tsunami = Ts + nami



上图解释了字符串切片的原理(注:Rust 中的字符串类型实质上记录了字符在内存中的起始位置和其长度,我们暂时了解到这一点)。

使用..表示范围的语法在循环章节中出现过。x..y 表示[x, y)的数学含义。.. 两边可以没有运算数:

..y 等价于 0..y
x.. 等价于位置 x 到数据结束
.. 等价于位置 0 到结束

注意:尽量不要在字符串中使用非英文字符,因为编码的问题。具体原因会在别处叙述。

被切片引用的字符串禁止更改其值:

fn main() {
    let mut s = String::from("Lun");
    let slice = &s[0..3];
    s.push_str("Tsunami"); // Error: 切片值不可变
    println!("slice = {}", slice);
}

这段程序不正确。

s 被部分引用,禁止更改其值。

实际上,到目前为止你一定疑惑为什么每一次使用字符串都要这样写String::from("Lun") ,直接写"Lun"不行吗?

事已至此我们必须分辨这两者概念的区别了。在 Rust 中有两种常用的字符串类型:strStringstr 是 Rust 核心语言类型,就是本章一直在讲的字符串切片(String Slice),常常以引用的形式出现(&str)。

凡是用双引号包括的字符串常量整体的类型性质都是 &str

let a = "Tsunami";

这里的 a 就是一个 &str 类型的变量。

String 类型是 Rust 标准公共库提供的一种数据类型,它的功能更完善——它支持字符串的追加、清空等实用的操作。Stringstr 除了同样拥有一个字符开始位置属性和一个字符串长度属性以外还有一个容量(capacity)属性。

Stringstr 都支持切片,切片的结果是 &str 类型的数据。

注意:切片结果必须是引用类型,但开发者必须自己明示这一点:

let b = &a[..]

即便不注意是否为引用类型,编辑器也会报出错误(需要验证)

有一个快速的办法可以将 String 转换成 &str:

fn main() {
let a = String::from("Tsunami"); // String
let b = &a[..]; // &str
}

非字符串切片

除了字符串以外,其他一些线性数据结构也支持切片操作,例如数组:

fn main() {
let array = [1,2,3,4,5,6,7,8,9,10];
let part = &array[..5]; // 遍历切片
for i in part {
println!("{}",i);
}
}

结构体(Struct)

Rust 中的结构体(Struct)与元组(Tuple)都可以将若干个类型不一定相同的数据捆绑在一起形成整体,

但结构体的每个成员和其本身都有一个名字,这样访问它成员的时候就不用记住下标了。

元组常用于非定义的多值传递,而结构体用于规范常用的数据结构。结构体的每个成员叫做"字段"。

结构体定义

这是一个结构体定义:

struct WebSite {
domain: String,
top: String,
public_time: String,
nation: String,
webtype: String,
time: i64
}

Rust结构体声明方式:

struct <struct_name> { <item>:<type>, xxxx:xxx, ········ }

注意:如果你常用 C/C++,请记住在 Rust 里 struct 语句仅用来定义,不能声明实例,结尾不需要 ; 符号,而且每个字段定义之后用 , 分隔。

结构体实例

Rust 很多地方受 JavaScript 影响,在实例化结构体的时候用 JSON 对象的key: value语法来实现定义:

fn main() {
struct WebSite {
domain: String,
top: String,
public_time: String,
nation: String,
webtype: String,
time_style: i64
} let my_blog = WebSite {
domain : String::from("www.karlablog.net"),
top : String::from(".net"),
public_time : String::from("2011-12-25"),
nation : String::from("China"),
webtype : String::from("blog"),
time_style : 24
};
}

如果你不了解 JSON 对象,你可以不用管它,记住格式就可以了:

name : value(,)

这样的好处是不仅使程序更加直观,还不需要按照定义的顺序来输入成员的值。

如果正在实例化的结构体有字段名称和现存变量名称一样的,可以简化书写:

fn main() {
struct WebSite {
domain: String,
top: String,
public_time: String,
nation: String,
webtype: String,
time_style: i64
} let domain = String::from("www.karlablog.net");
let top = String::from(".net");
let my_blog = WebSite {
domain, // domain : domain,
top, // top : top,
public_time : String::from("2011-12-25"),
nation : String::from("China"),
webtype : String::from("blog"),
time_style : 24
};
}

有这样一种情况:你想要新建一个结构体的实例,其中大部分属性需要被设置成与现存的一个结构体属性一样,仅需更改其中的一两个字段的值,可以使用结构体更新语法:

fn main() {
struct WebSite {
domain: String,
top: String,
public_time: String,
nation: String,
webtype: String,
time_style: i64
} let my_blog = WebSite {
domain : String::from("www.karlablog.net"),
top : String::from(".net"),
public_time : String::from("2011-12-25"),
nation : String::from("China"),
webtype : String::from("blog"),
time_style : 24
};
let my_blog2 = WebSite {
domain : String::from("KarlaBlog.com"),
top : String::from(".com"),
..my_blog
};
}

注意:..my_blog 后面不可以有逗号。这种语法不允许一成不变的复制另一个结构体实例,意思就是说至少重新设定一个字段的值才能引用其他实例的值。

元组结构体

有一种更简单的定义和使用结构体的方式:元组结构体

元组结构体是一种形式是元组的结构体。

与元组的区别是它有名字和固定的类型格式。它存在的意义是为了处理那些需要定义类型(经常使用)又不想太复杂的简单数据:

fn main() {
struct RGB(i32,u8,i64);
struct Origin(f64, f64); let black = RGB(0,0,0);
let origin = Origin(0.0,0.0);
}

"颜色"和"点坐标"是常用的两种数据类型,但如果实例化时写个大括号再写上两个名字就为了可读性,牺牲了便捷性,Rust 不会遗留这个问题。元组结构体对象的使用方式和元组一样,通过.和下标来进行访问:

fn main() {
struct Colour(i32,u8,i64);
struct Origin(f64, f64); let black = Colour(0,0,0);
let origin = Origin(0.0,0.0); println!("black RGB: {} {} {}", black.0, black.1, black.2);
println!("black oringin: {} {}", origin.0, origin.1);
}

输出:

black RGB: 0 0 0
black oringin: 0 0

结构体所有权

结构体必须掌握字段值所有权,因为结构体失效的时候会释放所有字段。

这就是为什么本章的案例中使用了String类型而不使用&str的原因。

但这不意味着结构体中不定义引用型字段,这需要通过"生命周期"机制来实现。

但现在还难以说明"生命周期"概念,所以只能在后面章节说明。

输出结构体

调试中,完整地显示出一个结构体实例是非常有用的。但如果我们手动的书写一个格式会非常的不方便。所以 Rust 提供了一个方便地输出一整个结构体的方法:

#[derive(Debug)]
struct Rectangle {
width: u32,
height : u32
} fn main() {
let rect1 = Rectangle { width : 30, height : 50}; println!("rect1 is {:#?}", rect1);
}

如第一行所示:一定要导入调试库 #[derive(Debug)] ,之后在 println!print 宏中就可以用{:?}占位符输出一整个结构体,输出:

rect1 is Rectangle {
width: 30,
height: 50,
}

结构体方法

方法(Method)和函数(Function)类似,只不过它是用来操作结构体实例的。

如果你学习过一些面向对象的语言,那你一定很清楚函数一般放在类定义里并在函数中用 this 表示所操作的实例。

Rust 语言不是面向对象的,从它所有权机制的创新可以看出这一点。但是面向对象的珍贵思想可以在 Rust 实现。

结构体方法的第一个参数必须是 &self,不需声明类型,因为 self 不是一种风格而是关键字。

计算一个矩形的面积:

struct Rectangle {
width: u32,
height : u32
} // impl声明了结构体中的方法
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
} fn main() {
let rect1 = Rectangle { width : 30, height : 50}; println!("rect1's area is {}", rect1.area());
}

以上程序将会输出:

rect1 is 1500

请注意,在调用结构体方法的时候不需要填写self,这是出于对使用方便性的考虑。

补充:

原教程并未说明self的具体情况,故补充。

self是一个关键字,方法的接受者,或当前模块

useself充当所引用的当前模块。

use std::io::{self, Read};

就相当于:

use std::io;
use std::io::Read;

使用它访问当前文件的元素:

fn foo(){}
fn bar() {
self::foo()
}

self作为方法的当前接收者,允许在大多数情况下省略参数类型。 除了这种特殊性,self 的用法与任何其他参数非常相似:

一个多参数的例子:

struct Rectangle { width : u64, height : u64 }
impl Rectangle {
fn area(&self) -> u64 {
self.width * self.height
} fn wider(&self, rect : &Rectangle) -> bool {
self.width > self.height
}
} fn main() {
let rect1 = Rectangle { width : 10, height : 25};
let rect2 = Rectangle { width : 75, height : 150}; if rect1.wider(&rect2) == true {
println!("matrix 1 is more width than matrix 2.")
} else {
println!("matrix 2 is more width than matrix 1.")
} println!("area of matrix 1 is {}",rect1.area());
println!("area of matrix 2 is {}",rect2.area());
}

这段代码输出:

matrix 2 is more width than matrix 1.
area of matrix 1 is 250
area of matrix 2 is 11250

这段代码有些长,让我们来分析一下吧!

代码的第一行声明了一个结构体,包含长和宽的数据,类型为u64

第二行定义了几个方法,分别是area(计算面积)和wider(比较宽的长度)。

前者返回u64,后者返回bool

接下是main函数,分别为rect1rect2初始化定义结构体,判断、并输出结果。

结构体关联函数

之所以"结构体方法"不叫"结构体函数"是因为"函数"这个名字留给了这种函数:它在impl块中却没有&self参数。

这种函数不依赖实例,但是使用它需要声明是在哪个impl块中的。

一直使用的String::from函数就是一个"关联函数"。

也就是结构体函数不需要self参数,与正常的定义函数相同。

#[derive(Debug)]
struct Matrix { width : u64, length : u64 } impl Matrix {
fn creat(width:u64,length:u64) -> Matrix {
Matrix { width, length }
}
}
fn main() {
println!("{:#?}",Matrix::creat(12,24));
}

以上函数输出:

Matrix {
width: 12,
length: 24,
}

其实impl模块时可以写很多次的,但是正常的开发应该不会写很多,也不排除特殊情况

单元结构体

结构体可以只作为一种象征而无需任何成员:

struct UnitStruct;

我们称这种没有结构体定义的结构体为单元结构体(Unit Struct)

注:原教程里的“身体”有点恐怖,本教材换为“结构体定义”

枚举类(Enumerate)

枚举

枚举类在Rust中略显复杂,但是依然可以很轻松地使用:

#[derive(Debug)]
enum ProgrammingLanguage {
Cpp , Rust , Java , Csharp
}
fn main() {
let learnt_lang = ProgrammingLanguage::Rust;
println!("{:#?}", learnt_lang);
}

输出:

Rust

书分为纸质书(Papery book)和电子书(Electronic book)。

如果你现在正在开发一个图书管理系统,你需要描述两种书的不同属性(纸质书有索书号,电子书只有 URL),你可以为枚举类成员添加元组属性描述:

enum Books {
papery(u64),
Electronic(String)
} fn main() {
let book = Books::papery(1145); // index
let e_book = Books::Electronic(String::from("url://......")); // url
}

如果你想为属性命名,可以用结构体语法:

enum Books {
papery { index : u64 },
Electronic { url : String }
} fn main() {
let book = Books::papery{ index : 1145 };
let e_book = Books::Electronic { url : String::from("url://......")};
}

虽然可以如此命名,但请注意,并不能像访问结构体字段一样访问枚举类绑定的属性。访问的方法在match语法中。

匹配(match)

枚举的目的是对某一类事物的分类,分类的目的是为了对不同的情况进行描述。基于这个原理,往往枚举类最终都会被分支结构处理(许多语言中的 switch )。

switch 语法很经典,但在 Rust 中并不支持,很多语言摒弃 switch 的原因都是因为 switch 容易存在因忘记添加 break 而产生的串接运行问题,Java 和 C# 这类语言通过安全检查杜绝这种情况出现。

Rust 通过 match 语句来实现分支结构。先认识一下如何用match处理枚举类:

enum Books {
Papery { index : u64 },
Electronic { url : String }
} fn main() {
let book = Books::Papery { index : 1145 };
let e_book = Books::Electronic { url : String::from("url://......")}; match book {
Books::Papery {index} => {
println!("Papery book {}", index);
}
Books::Electronic {url} => {
println!("E-book {}", url);
}
}
}

输出:

Papery book 1145

match 块也可以当作函数表达式来对待,它也是可以有返回值的:

match 枚举类实例 {
分类1 => 返回值表达式,
分类2 => 返回值表达式,
...
}

就像这样:

enum Books {
Papery { index : u64 },
Electronic { url : String }
} fn main() {
let book = Books::Papery { index : 1145 };
let e_book = Books::Electronic { url : String::from("url://......")}; let is_book = match book {
Books::Papery {index} => { true }
Books::Electronic {url} => { false }
}; if is_book == true {
println!("This is a book.")
} else {
println!("This is an e-book")
}
}

但是所有返回值表达式的类型必须一致

如果把枚举类附加属性定义成元组,在 match 块中需要临时指定一个名字:

enum Book {
    Papery(u32),
    Electronic {url: String},
}
let book = Book::Papery(1001); match book {
    Book::Papery(i) => {
        println!("{}", i);
    },
    Book::Electronic { url } => {
        println!("{}", url);
    }
}

match 除了能够对枚举类进行分支选择以外,还可以对整数、浮点数、字符和字符串切片引用(&str)类型的数据进行分支选择。其中,浮点数类型被分支选择虽然合法,但不推荐这样使用,因为精度问题可能会导致分支错误。

对非枚举类进行分支选择时必须注意处理例外情况,例外情况用下划线_表示:

fn main() {
    let t = "abc";
    match t {
        "abc" => println!("Yes"),
        _ => {},
    }
}

或:

fn main() {
let str = "abc";
match str {
"acd" => { println!("not match") } // 输出“不匹配”
_ => {} // 什么都不做
}
}

Option 枚举类

Option 是 Rust 标准库中的枚举类,这个类用于填补 Rust 不支持null引用的空白。

许多语言支持null的存在(C/C++、Java),这样很方便,但也制造了极大的问题,null 的发明者也承认这一点,"一个方便的想法造成累计 10 亿美元的损失"。

null 经常在开发者把一切都当作不是 null 的时候给予程序致命一击:毕竟只要出现一个这样的错误,程序的运行就要彻底终止。

为了解决这个问题,很多语言默认不允许 null,但在语言层面支持 null 的出现(常在类型前面用 ? 符号修饰,比如C#)。

Java 默认支持 null,但可以通过@NotNull注解限制出现 null,这是一种应对的办法。

Rust 在语言层面彻底不允许空值 null 的存在,但无奈null 可以高效地解决少量的问题,所以 Rust 引入了 Option 枚举类:

enum Option<T> {
Some(T),
None,
}

如果你想定义一个可以为空值的类,你可以这样:

fn main() {
let opt = Option::Some("Tsunami");
match opt {
Option::Some(something) => {
println!("{}", something)
}
Option::None => {
println!("warning: opt is null")
}
}
}

如果你的变量刚开始是空值,你体谅一下编译器,它怎么知道值不为空的时候变量是什么类型的呢?

所以初始值为空的 Option 必须明确类型:

fn main() {
let opt:Option<&str> = Option::None;
match opt {
Option::Some(something) => {
println!("{}",something)
}
Option::None => {
println!("warning: opt is null")
}
}
}

随后,程序输出:

warning: opt is null

这种设计会让空值编程变得不容易,但这正是构建一个稳定高效的系统所需要的。由于 Option 是 Rust 编译器默认引入的,在使用时可以省略Option::直接写None或者 Some()

Option 是一种特殊的枚举类,它可以含值分支选择:

fn main() {
let a = None;
match a {
Some(128) => println!("a不为空"),
_ => println!("a为空")
}
}

程序输出:

a为空

if-let语法

fn main() {
let a = 0;
match a {
0 => {
println!("zero")
}
_ => {}
}
}

放入主函数运行结果:

zero

这段程序的目的是判断 i 是否是数字 0,如果是就打印 zero。

现在用 if-let 语法缩短这段代码:

fn main() {
let a = 0;
if let 0 = a {
println!("zero")
}
}

if let 语法格式如下:

if let 匹配值 = 源变量 {
语句块
}

可以在之后添加一个 else 块来处理例外情况。

if let 语法可以认为是只区分两种情况的 match 语句的语法糖

语法糖指的是某种语法的原理相同的便捷替代品

对于枚举类依然适用:

fn main() {
enum Book {
Papery(u32),
Electronic(String)
}
let book = Book::Electronic(String::from("url"));
if let Book::Papery(index) = book {
println!("Papery {}", index);
} else {
println!("Not papery book");
}
}

组织管理(Project management)

任何一门编程语言如果不能组织代码都是难以深入的,几乎没有一个软件产品是由一个源文件编译而成的。

本教程到目前为止所有的程序都是在一个文件中编写的,主要是为了方便学习 Rust 语言的语法和概念。

对于一个工程来讲,组织代码是十分重要的。

Rust 中有三个重要的组织概念:箱、包、模块。

箱(Crate)

"箱"是二进制程序文件或者库文件,存在于"包"中。

"箱"是树状结构的,它的树根是编译器开始运行时编译的源文件所编译的程序。

注意:"二进制程序文件"不一定是"二进制可执行文件",只能确定是是包含目标机器语言的文件,文件格式随编译环境的不同而不同。

包(Package)

当我们使用 Cargo 执行 new 命令创建 Rust 工程时,工程目录下会建立一个 Cargo.toml 文件。工程的实质就是一个包,包必须由一个 Cargo.toml 文件来管理,该文件描述了包的基本信息以及依赖项。

一个包最多包含一个库"箱",可以包含任意数量的二进制"箱",但是至少包含一个"箱"(不管是库还是二进制"箱")。

当使用 cargo new 命令创建完包之后,src 目录下会生成一个 main.rs 源文件,Cargo 默认这个文件为二进制箱的根,编译之后的二进制箱将与包名相同。

模块(Module)

对于一个软件工程来说,我们往往按照所使用的编程语言的组织规范来进行组织,组织模块的主要结构往往是树。Java 组织功能模块的主要单位是类,而 JavaScript 组织模块的主要方式是 function。

这些先进的语言的组织单位可以层层包含,就像文件系统的目录结构一样。Rust 中的组织单位是模块(Module)。

(未完成)

声明

部分图片/文本来自网络,未商用。

来源:

特别感谢

  • LunTsunami(修订部分文字)
  • RuLega(为本教程绘图[部分])
  • 菜鸟教程(为我们提供资料)

Matrix【未完成】的更多相关文章

  1. [占位-未完成]scikit-learn一般实例之十一:异构数据源的特征联合

    [占位-未完成]scikit-learn一般实例之十一:异构数据源的特征联合 Datasets can often contain components of that require differe ...

  2. angular2系列教程(十一)路由嵌套、路由生命周期、matrix URL notation

    今天我们要讲的是ng2的路由的第二部分,包括路由嵌套.路由生命周期等知识点. 例子 例子仍然是上节课的例子:

  3. Pramp mock interview (4th practice): Matrix Spiral Print

    March 16, 2016 Problem statement:Given a 2D array (matrix) named M, print all items of M in a spiral ...

  4. Atitit Data Matrix dm码的原理与特点

    Atitit Data Matrix dm码的原理与特点 Datamatrix原名Datacode,由美国国际资料公司(International Data Matrix, 简称ID Matrix)于 ...

  5. Android笔记——Matrix

    转自:http://www.cnblogs.com/qiengo/archive/2012/06/30/2570874.html#translate Matrix的数学原理 在Android中,如果你 ...

  6. 通过Matrix进行二维图形仿射变换

    Affine Transformation是一种二维坐标到二维坐标之间的线性变换,保持二维图形的"平直性"和"平行性".仿射变换可以通过一系列的原子变换的复合来 ...

  7. [占位-未完成]scikit-learn一般实例之十二:用于RBF核的显式特征映射逼近

    It shows how to use RBFSampler and Nystroem to approximate the feature map of an RBF kernel for clas ...

  8. [占位-未完成]scikit-learn一般实例之十:核岭回归和SVR的比较

    [占位-未完成]scikit-learn一般实例之十:核岭回归和SVR的比较

  9. [LeetCode] Kth Smallest Element in a Sorted Matrix 有序矩阵中第K小的元素

    Given a n x n matrix where each of the rows and columns are sorted in ascending order, find the kth ...

  10. [LeetCode] Longest Increasing Path in a Matrix 矩阵中的最长递增路径

    Given an integer matrix, find the length of the longest increasing path. From each cell, you can eit ...

随机推荐

  1. 聚焦业务价值:分众传媒在 Serverless 上的探索和实践

    作者 | 吴松(分众传媒研发总监) **关注 Serverless 公众号后台回复 分众 即可获得云原生峰会 PPT! ** 本文总结于分众传媒研发总监吴松在阿里云云原生实战峰会上的分享,从三个方面详 ...

  2. Django 对实体的增删改查样例

    class UserInfo(models.Model): """ 人员信息 """ user_id = models.CharField( ...

  3. [Vue] Computed property "XXX" was assigned to but it has no setter.

    阅读这篇文章:https://blog.csdn.net/weixin_34090562/article/details/91369638 全选,通过计算属性计算得来.结果报错Computed pro ...

  4. CommonJS 和 ES6 Module 究竟有什么区别?

    https://juejin.im/post/5e5f10176fb9a07cd443c1e2

  5. 【日常踩坑】解决 kex_exchange_identification 报错

    目录 踩坑 原因分析 解决办法 1. 临时关闭代理 2. 修改代理软件配置,22 端口走直连 3. 改用 HTTPS 协议,走 443 端口 参考资料 踩坑 最近在使用 git 时,发现 git pu ...

  6. Go-单元测试-Test

    单元测试 文件名以 _test.go 结尾 函数名以 Test 开头 函数参数固定 t *testing.T 运行单元测试 go test Demo 源文件 package unit import & ...

  7. springboot入参下划线转驼峰出参驼峰转下划线

    springboot入参出参下划线转驼峰 前言 因为历史原因前端入参和出参都为下划线,下划线对有亿点强迫症的我来说是不可接受的.因此就有了下面这篇. 本篇基于之前的一篇springboot封装统一返回 ...

  8. 鲲鹏920上面 Docker 部署 clickhouse 的方式方法

    鲲鹏920上面 Docker 部署 clickhouse 的方式方法 背景 最近有一套鲲鹏920的测试环境, 研发同事想纯Dcoker部署一套环境. 其中就包括了 Clickhouse 之前发现Cli ...

  9. [转帖]使用Rclone实现minio数据的迁移

    使用Rclone实现minio数据的迁移 一.准备 1.1 使用工具 rclone:开源的对象存储在线迁移工具,用于文件和目录的同步,支持阿里云的oss.minio .亚马逊S3 等. 1.2 注意事 ...

  10. [转帖]Redash -- Redash部署安装docker版

    向导 官网 1.环境准备 1.1 安装docker和docker-compose 1.2 安装nodejs和npm 2.安装Redash 官网 主页 Developer Guide github 讨论 ...