Ice 的博客

用心记录思想

结构体数据的所有权(Ownership of Struct Data)

在Listing 5-1中的User结构体定义中,我们使用了自身拥有所有权的String类型而不是&str字符串slice类型。这是一个有意为之的选择,因为我们想要这个结构体拥有它所有的数据,为此只要整个结构体是有效的话数据也是有效的。可以使结构体存储被其他对象拥有的数据的引用,不过这么做的话需要用上生命周期(lifetimes),这是一个第十章会讨论的Rust功能。生命周期确保结构体引用的数据有效性跟结构体本身保持一致。如果你尝试在结构体中存储一个引用而不是指定生命周期将是无效的,比如这样:

filename: src/main.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct User {
active: bool,
username: &str,
email: &str,
sign_in_count: u64,
}

fn main() {
let user1 = User {
email: "someone@example.com",
username: "someusername123",
active: true,
sign_in_count: 1,
};
}

编译器会控诉它需要生命周期标识符:

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
30
31
$ cargo run
Compiling structs v0.1.0 (file:///projects/structs)
error[E0106]: missing lifetime specifier
--> src/main.rs:3:15
|
3 | username: &str,
| ^ expected named lifetime parameter
|
help: consider introducing a named lifetime parameter
|
1 ~ struct User<'a> {
2 | active: bool,
3 ~ username: &'a str,
|

error[E0106]: missing lifetime specifier
--> src/main.rs:4:12
|
4 | email: &str,
| ^ expected named lifetime parameter
|
help: consider introducing a named lifetime parameter
|
1 ~ struct User<'a> {
2 | active: bool,
3 | username: &str,
4 ~ email: &'a str,
|

For more information about this error, try `rustc --explain E0106`.
error: could not compile `structs` due to 2 previous errors=

第十章会讲到如何修复这个问题以便在结构体中存储引用,不过现在,我们使用像String这类拥有所有权的类型来替代&str这样的引用以修正这个错误。

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的错误处理。

0%