Ice 的博客

用心记录思想

struct or structure 是一个自定义的数据类型,允许你包装和命名多个相关联的值,从而形成一个有意义的组合。如果你熟悉一门面向对象语言,struct就像对象中的数据属性。在本章中,我们会对元组和结构体进行对比和对照。还将演示如何定义和实现实例化结构体,并讨论如何定义关联函数,特别是被称为方法的那种关联函数,以指定与结构体类型相关的行为。你可以在程序中基于结构体和枚举(enum)(在第六章中介绍)创建新类型,以充分利用Rust的编译时类型检查。

总结

所有权、借用和slice这些概念让Rust程序在编译时保证内存安全。Rust语言提供了跟其他系统编程语言相同的方式来控制你使用内存,但拥有数据所有者在离开作用域后自动清除其数据的功能意味着你无需额外编写和调试相关的控制代码。

所有权系统影响了Rust中很多其他部分的工作方式,所以我们还会继续讲到这些概念,这将贯穿本书余下的内容。让我们开始第五章吧,来看看如何将多份数据组合进一个struct中。

引用的规则(The Rules of References)

让我们来重新概况一下之前对引用的讨论:

  • 在任意给定时间,要么只能有一个可变引用,要么只能有多个不可变引用。
  • 引用必须总是有效的。

接下来,我们来看看另一种不同类型的引用:slice。

返回值与作用域(Return Values and Scope)

返回值也可以转移所有权。Listing 4-4展示了一个示例,与Listing4-3一样带有类似的注释。

Filename: src/main.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
fn main() {
let s1 = gives_ownership(); // gives_ownership moves its return
// value into s1

let s2 = String::from("hello"); // s2 comes into scope

let s3 = takes_and_gives_back(s2); // s2 is moved into
// takes_and_gives_back, which also
// moves its return value into s3
} // Here, s3 goes out of scope and is dropped. s2 was moved, so nothing
// happens. s1 goes out of scope and is dropped.

fn gives_ownership() -> String { // gives_ownership will move its
// return value into the function
// that calls it

let some_string = String::from("yours"); // some_string comes into scope

some_string // some_string is returned and
// moves out to the calling
// function
}

// This function takes a String and returns one
fn takes_and_gives_back(a_string: String) -> String { // a_string comes into
// scope

a_string // a_string is returned and moves out to the calling function
}

Listing 4-4: Transferring ownership of return values

变量的所有权总是遵循相同的模式:将赋值给另一个变量时移动它。当持有堆中数据的变量离开作用域时,其值将通过drop被清理,除非数据被移动为另一个变量所有。

虽然这样是可以的,但是在每一个函数中都获取所有权并接着返回所有权有些啰嗦。如果我们想要函数使用一个值但不获取所有权该怎么办呢?如果我们还要接着使用它的话,每次都穿进去再返回来就有点烦人了,除此之外,我们可能想返回函数中产生的一些数据。

我们可以使用元组来返回多个值,如Listing 4-5。

Filename:src/main.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
fn main() {
let s1 = String::from("hello");

let (s2, len) = calculate_length(s1);

println!("The length of '{}' is {}.", s2, len);
}

fn calculate_length(s: String) -> (String, usize) {
let length = s.len(); // len() returns the length of a String

(s, length)
}

Listing 4-5: Returning ownership of parameters

但是,对于一个应该是普通的概念来说,这太多的仪式和大量的工作。幸运的是,Rust有一个特性,可以在不用转移所有权的情况下使用值,它叫做引用(references)。

接下来将讲”引用和借用(References and Borrowing)”

Understanding Ownership

Ownership 是Rust最独特的特性,并且对剩下的部分有着深刻的意义。它能让Rust在不需要垃圾回收(garbage collection)就能保证内存安全,所以能够很好地理解owership是如何工作的是非常重要的。在本章,我们将讲和ownership关联的一些特性:borrowing, slice, 和Rust如何在内存中布局数据。

总结(Summary)

你做到了!这是相当大的一章:你学习了关于variables, scalar and compound data type, functions, comments, if expressions, and loops! 讨论并练习这些概念。尝试构建如下的程序:

  • 相互转换摄氏与华氏温度(Convert temperatures between Fahrenheit and Celsius.)
  • 生成n阶斐波那契数列(Generate th nth Fibonacci number.)
  • 打印圣诞颂歌词,并利用歌词中的重复部分(编写循环)(Print the lyrics to the Christmas carol “The Twelve Days of Christmas,” taking advantage of the repetition in the song.)

当你准备好继续的时候,让我们讨论一个其他语言中不常见的概念:所有权(ownership)

注释(Comments)

所有的程序设计人员努力使他们的代码更容易理解,但是有时候额外的解释是有必要的。在这种情况下程序设计人员在源代码中留下注释(comments),编译器会忽略这些注释,但是人阅读源码会发现它很有用。

这里有一个简单的注释例子:

1
2
3
4
5
6
#![allow(unused)]
fn main() {
// So we’re doing something complicated here, long enough that we need
// multiple lines of comments to do it! Whew! Hopefully, this comment will
// explain what’s going on.
}

注释可以放在代码行结尾:

src/main.rs

1
2
3
fn main() {
let lucky_number = 7; // I’m feeling lucky today
}

但是你经常看到更多是如下的格式,在代码的上方的单独一行注释:

1
2
3
4
fn main() {
// I’m feeling lucky today
let lucky_number = 7;
}

Rust 也有其他类型的注释,文档注释(documentation comments),详细在第14章的”Publishing a Crate to Crates.io” 部分介绍。

函数(Functions)

函数在Rust代码中是很普遍的。你已经看了在语言中最重要的函数之一:main函数,是很多程序的入口。你也看到了fn关键字,它允许你声明一个新的函数。

Rust代码中的函数和变量使用snake case的代码风格,所有的单词小写并用下划线隔开。这里有一个程序包含了一个函数的定义:

1
2
3
4
#![allow(unused)]
fn main() {
let x = 5;
}

其次,five函数没有参数并定义了返回值类型,不过函数体只有单单一个5,因为它是一个表达式,可以返回我们想要的值。

让我们看看另外一个例子:

src/main.rs

1
2
3
4
5
6
7
8
9
fn main() {
let x = plus_one(5);

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

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

编译这段代码,会产生一个如下的错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
error[E0308]: mismatched types
--> src/main.rs:7:24
|
7 | fn plus_one(x: i32) -> i32 {
| -------- ^^^ expected `i32`, found `()`
| |
| implicitly returns `()` as its body has no tail or `return` expression
8 | x + 1;
| - help: consider removing this semicolon

For more information about this error, try `rustc --explain E0308`.
error: could not compile `functions` due to previous error

主要的错误信息”mismatched types”揭示了这段代码的核心问题所在。定义函数plus_one,然后说要返回一个i32,但是语句不会计算得到一个值,使用单位类型()表示不返回值。因为不返回值与函数返回一个i32类型的值矛盾,从而出现一个错误。在输出中,Rust提供了一条信息,可能有助于纠正这个错误:它建议删除分号,这会修复这个错误。

正确的代码如下:

1
2
3
4
5
6
7
8
9
fn main() {
let x = plus_one(5);

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

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

无效的数组元素访问

让我们看看发生了什么,如果你尝试访问数组的一个元素,这个元素在数组的结尾之后呢。运行如下的代码,类似于第二章猜数字游戏,从用户输入那里获取数组索引:

src/main.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
use std::io;

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

println!("Please enter an array index.");

let mut index = String::new();

io::stdin()
.read_line(&mut index)
.expect("Failed to read line");

let index: usize = index
.trim()
.parse()
.expect("Index entered was not a number");

let element = a[index];

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

这段代码编译成功。如果运行这段代码使用cargo run然后输入0,1,2,3,4,这个程序会打印出在数组内对应索引的值。如果你输入一个超出范围的数字,比如10,你会看到输出如:

1
2
3
thread 'main' panicked at 'index out of bounds: the len is 5 but the index is 10', src/main.rs:19:19
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

当你使用这个无效的值索引的时候,程序的结果是在运行时间的错误。程序退出并返回错误信息,并且没有运行最后的println!语句。当你试图用一个索引访问一个元素,Rust会检查你指定的这个索引是否会超过数组的长度。如果索引与数组长度相同或者更大,Rust就会死给你看。这个检查在运行时间,特别在这个例子中,因为编译器在编译完之后就不可能知道用户会输入什么。

这是一个Rust的内存安全原则示例的表现。在很多底层代码语言中,当你提供一个错误的索引,这种类型的检查它们是不做的,无效内存就会被访问,会导致你不知道你到底访问到了别的什么奇奇怪怪的值。Rust项目中会立即退出,而不是允许你继续访问,从而保护你面授此类错误的影响。第九章将会讨论Rust的错误处理。

隐藏(Shadowing)

如你在第二章猜数字游戏教程所见,你可以声明一个新的变量用和前面声明的变量同样的名字。Rustaceans说第一个变量被第二个隐藏(Shadowing)了,意思是第二个变量的值是在程序使用时才看到的。我们可以shadow一个变量使用相同的变量名,并重复地使用let关键字,如下:

src/main.rs

1
2
3
4
5
6
7
8
9
10
11
12
fn main() {
let x = 5;

let x = x + 1;

{
let x = x * 2;
println!("The value of x in the inner scope is: {}", x);
}

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

程序首先绑定了5给x。然后重新用let x=隐藏了x,使得原来的值加1,所以变量的值变为了6。然后,在内部作用域,第三次let声明x, 将之前的值乘以2得到12。当内部作用结束,内部的隐藏结束,并且x变回6。当我们运行这个程序,它运行结果如下:

1
2
3
4
5
6
$ cargo run
Compiling variables v0.1.0 (file:///projects/variables)
Finished dev [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/variables`
The value of x in the inner scope is: 12
The value of x is: 6

隐藏不同于将变量标记为mut,因为如果我们意外地尝试重新分配这个变量不使用let关键字,我们会得到一个编译错误。通过使用let,我们可以对一个值执行一些转换,但在这些转换完成后,变量是不可变的。

其他的mut和shadowing之间的不同之处是因为当我们再次使用let关键字,我们实际上创建了一个新的变量。例如,假如我们的程序要求用户输入空格来显示文本之间需要多少空格,然后我们希望将输入存储为数字:

1
2
3
4
fn main() {
let spaces = " ";
let spaces = spaces.len();
}

首先spaces变量是一个string类型,然后第二个spaces变量是数字类型。因此shadowing使我们不需使用不同的变量名,如space_str 和 space_num;然而,如果我们尝试使用mut,如下,我们会得到一个编译时错误:

1
2
3
4
fn main() {
let mut spaces = " ";
spaces = spaces.len();
}

错误说我们不被允许转变变量的类型:

1
2
3
4
5
6
7
8
9
10
$ cargo run
Compiling variables v0.1.0 (file:///projects/variables)
error[E0308]: mismatched types
--> src/main.rs:3:14
|
3 | spaces = spaces.len();
| ^^^^^^^^^^^^ expected `&str`, found `usize`

For more information about this error, try `rustc --explain E0308`.
error: could not compile `variables` due to previous error

现在,我们探索了变量是如何工作的,我们还有更多的数据类型等着你呢!

0%