The Rust Programming Language
想要更互动的学习体验?测验,高亮,可视化
Rust 程序设计语言
Source Code
Rust编程语言从根本上讲是关于授权的(empowerment)。
处理内存管理、数据表示和并发性的底层细节的“系统级”工作。传统上,这一编程领域被视为神秘莫测的,只有少数经过多年学习以避免其臭名昭著的陷阱的人才能进入。即使是那些实践它的人也要谨慎,以免他们的代码被利用、崩溃或损坏。
需要“深入”到较低级别控制的程序员可以使用Rust做到这一点,而不必承担崩溃或安全漏洞的常见风险,也不必学习变化莫测的工具链的细微之处。更好的是,该语言的设计初衷是引导您自然地编写出在速度和内存使用方面高效的可靠代码。
在Rust中引入并行是一种风险相对较低的操作:编译器将为您捕获典型错误。而且,可以在代码中处理更积极的优化,并确信不会意外地引入崩溃或漏洞。
但是Rust并不局限于低级的系统编程。它的表达能力和人体工程学足以使CLI应用程序、web服务器和许多其他类型的代码编写起来相当愉快
Rust语言有一组仅供该语言使用的关键字,与其他语言一样。记住,不能用这些词作为变量或函数的名称。大多数关键字都有特殊的含义。关键字列表。
默认情况下变量是不可变的。这是Rust为您提供的许多注意点之一,可以利用Rust提供的安全性和简单并发性来编写代码。但是,仍然可以选择使变量可变。
当变量是不可变的时,一旦值绑定到名称,就不能更改该值。
fn main() {
let x = 5; //let mut x = 5;
println!("The value of x is: {x}");
x = 6; // 报错:error[E0384]: cannot assign twice to immutable variable `x`
println!("The value of x is: {x}");
}
如果代码的一部分基于一个值永远不会改变的假设进行操作,而代码的另一部分改变了该值,那么代码的第一部分就有可能无法实现其设计目的。这类错误的原因很难在事后找到,特别是当第二段代码只在偶尔更改值时。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}");
}
与不可变变量一样,常量是绑定到名称且不允许更改的值,但常量和变量之间有一些区别。
首先,不允许将mut
与常量一起使用。常量在默认情况下不只是不可变的——它总是不可变的。使用const
关键字来声明常量,并且必须对值的类型进行注释。
常量可以在任何作用域中声明,包括全局作用域中,这使得它对于代码的很多部分都需要知道的值非常有用。
最后一个区别是,常量只能设置为常量表达式,而不是在运行时计算的值。
#![allow(unused)]
fn main() {
const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3;
}
Rust对常量的命名约定是全部使用大写字母和在单词之间使用下划线。
在声明它们的作用域内,常量在程序运行的整个过程中都是有效的。
将整个程序中使用的硬编码值命名为常量,可以将该值的含义传递给将来的代码维护者。如果硬编码的值将来需要更新,那么在代码中只有一个地方需要更改,这也会有所帮助。
常量求值是在编译过程中计算表达式结果的过程。只有所有表达式的一个子集可以在编译时求值。
可以声明一个与前一个变量同名的新变量。
Rustaceans会说,第一个变量被第二个变量遮蔽,这意味着当使用变量名时,编译器将看到第二个变量。
实际上,第二个变量掩盖了第一个变量,对于变量名自己的任何用法,直到它本身被掩盖或作用域结束。
我们可以通过使用相同的变量名并重复使用let
关键字来隐藏一个变量,如下所示:
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}");
}
//The value of x in the inner scope is: 12
//The value of x is: 6
遮蔽与将一个变量标记为mut
不同,因为如果我们不小心尝试重新赋值给这个变量而不使用let
关键字,我们将得到一个编译时错误。通过使用let
,我们可以对值执行一些转换,但在这些转换完成后,该变量是不可变的。
mut
和遮蔽之间的另一个区别是,因为我们在再次使用let
关键字时有效地创建了一个新变量,所以可以更改值的类型,但重用相同的名称。
let spaces = " ";
let spaces = spaces.len();
第一个spaces
变量是字符串类型,第二个spaces
变量是数字类型。因此,遮蔽使我们不必想出不同的名称,如spaces_str
和spaces_num
;相反,我们可以重用更简单的空间名称。然而,如果我们尝试使用mut来实现此功能,如下面所示,我们将得到一个编译时错误:
let mut spaces = " ";
spaces = spaces.len();
// error[E0308]: mismatched types
本节所有的数据类型的大小都是已知的。
Rust中的每个值都具有特定的数据类型。我们将研究两个数据类型子集: 标量( scalar)和复合(compound)。
请记住Rust是一种静态类型语言,这意味着它在编译时必须知道所有变量的类型。
编译器通常可以根据值和使用方式推断出我们想要使用的类型
。
在有多种可能类型的情况下,比如使用parse
将字符串转换为数字类型时,我们必须添加一个类型注释,
let guess: u32 = "42".parse().expect("Not a number!");
: u32
这个为类型注释(type annotation)
您将看到针对其他数据类型的不同类型注释。
标量类型表示单个值。Rust有四种主要标量类型:整数、浮点数、布尔值和字符。
u32
: 这个类型声明表明与它相关联的值应该是一个无符号整数(有符号整数类型以i
开头,而不是u
),它占用32位空间。
Rust中的内置整数类型如表所示。
每个变体可以是有符号的,也可以是无符号的,并且具有显式的大小。
有符号数使用二进制的补码来存储。
每个有signed 变量可以存储从-2^(n - 1)
到2^(n - 1) - 1
(含)的数字,其中n是该变量使用的比特数。
i8
-128 to 127.
Unsigned 变量可以存储从0
到2^n - 1
的数字,
i8
0 to 255
此外,isize
和usize
类型取决于运行程序的计算机的体系结构,在表中表示为“arch”:如果是64位体系结构,则为64位,如果是32位体系结构,则为32位。
注意,可以是多个数字类型的数字字面量允许使用类型后缀(如57u8
)来指定类型。数字字面量还可以使用_
作为可视分隔符,以使数字更易于阅读,例如1_000
,它的值将与指定1000
的值相同。
Rust中的整数字面量
那么如何知道使用哪种类型的整数呢?如果不确定,Rust的默认值通常是很好的起点:整数类型默认为i32
。使用isize或usize的主要情况是为某种集合建立索引。
要显式处理溢出的可能性,可以使用标准库为基本数字类型提供的方法族:
wrapping_*
方法在所有模式下进行包装,例如wrapping_add
checked_*
方法溢出,则返回None
值overflowing_*
方法 返回值和一个布尔值(指示是否发生溢出)saturating_*
方法对值的最小或最大值进行饱和Rust还有两种浮点数的基本类型,即带有小数点的数字。
Rust的浮点类型是f32
和f64
,大小分别为32位和64位。默认类型是f64
,因为在现代的cpu上,它的速度与f32大致相同,但能够实现更高的精度。所有浮点类型都是有符号的。
fn main() {
let x = 2.0; // f64
let y: f32 = 3.0; // f32
}
浮点数是根据IEEE-754标准表示的。f32
型为单精度(single-precision)浮点数,f64
为双精度(double precision)浮点数。
Rust支持所有数字类型的基本数学运算:加减乘除和取余(addition, subtraction, multiplication, division, and remainder)。
整数除法舍入(rounds down)到最接近的整数。
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;
let floored = 2 / 3; // Results in 0
// remainder
let remainder = 43 % 5;
}
附录B包含Rust提供的所有操作符的列表。
表B-1包含Rust中的操作符,一个操作符在上下文中如何出现的示例,一个简短的解释,以及该操作符是否可重载。如果操作符是可重载的,则列出用于重载该操作符的相关trait。
Operator | Example | Explanation | Overloadable? |
---|---|---|---|
! | ident!(…), ident!{…}, ident![…] | Macro expansion | |
! | !expr | Bitwise or logical complement 按位或逻辑取反 | Not |
!= | expr != expr | Nonequality comparison | PartialEq |
% | expr % expr | Arithmetic remainder | Rem |
%= | var %= expr | Arithmetic remainder and assignment | RemAssign |
& | &expr, &mut expr | Borrow | |
& | &type, &mut type, &'a type, &'a mut type | Borrowed pointer type | |
& | expr & expr | Bitwise AND | BitAnd |
&= | var &= expr | Bitwise AND and assignment | BitAndAssign |
&& | expr && expr | Short-circuiting logical AND | |
* | expr * expr | Arithmetic multiplication | Mul |
*= | var *= expr | Arithmetic multiplication and assignment | MulAssign |
* | *expr | Dereference | Deref |
* | *const type, *mut type | Raw pointer | |
+ | trait + trait, 'a + trait | Compound type constraint | |
+ | expr + expr | Arithmetic addition | Add |
+= | var += expr | Arithmetic addition and assignment 算术加法和赋值 | AddAssign |
, | expr, expr | Argument and element separator | |
- | - expr | Arithmetic negation | Neg |
- | expr - expr | Arithmetic subtraction | Sub |
-= | var -= expr | Arithmetic subtraction and assignment | SubAssign |
-> | fn(…) -> type, | … | -> type |
. | expr.ident | Member access | |
… | …, expr…, …expr, expr…expr | Right-exclusive range literal | PartialOrd |
…= | …=expr, expr…=expr | Right-inclusive range literal | PartialOrd |
… | …expr | Struct literal update syntax | |
… | variant(x, …), struct_type { x, … } | “And the rest” pattern binding | |
… | expr…expr | (Deprecated, use …= instead) In a pattern: inclusive range pattern | |
/ | expr / expr | Arithmetic division | Div |
/= | var /= expr | Arithmetic division and assignment | DivAssign |
: | pat: type, ident: type | Constraints | |
: | ident: expr | Struct field initializer | |
: | 'a: loop {…} | Loop label | |
; expr; | Statement and item terminator | ||
; | […; len] | Part of fixed-size array syntax | |
<< | expr << expr | Left-shift | Shl |
<<= | var <<= expr | Left-shift and assignment | ShlAssign |
< | expr < expr | Less than comparison | PartialOrd |
<= | expr <= expr | Less than or equal to comparison | PartialOrd |
= | var = expr, ident = type | Assignment/equivalence | |
== | expr == expr | Equality comparison | PartialEq |
=> | pat => expr | Part of match arm syntax | |
> | expr > expr | Greater than comparison | PartialOrd |
>= | expr >= expr | Greater than or equal to comparison | PartialOrd |
>> | expr >> expr | Right-shift | Shr |
>>= | var >>= expr | Right-shift and assignment ShrAssign | |
@ | ident @ pat | Pattern binding | |
^ | expr ^ expr | Bitwise exclusive OR | BitXor |
^= | var ^= expr | Bitwise exclusive OR and assignment | BitXorAssign |
| | pat | pat | Pattern alternatives | |
| | expr | expr | Bitwise OR | BitOr |
|= | var |= expr | Bitwise OR and assignment | BitOrAssign |
|| | expr || expr | Short-circuiting logical OR | |
? | expr? | Error propagation |
与大多数其他编程语言一样,Rust中的布尔类型有两个可能的值:true
和false
。布尔值的大小为一个字节。Rust中的布尔类型使用bool
指定。
fn main() {
let t = true;
let f: bool = false; // with explicit type annotation
}
使用布尔值的主要方法是通过条件语句,例如if表达式。“控制流”
Rust的char
类型是该语言中最原始的字母类型。
fn main() {
let c = 'z';
let z: char = 'ℤ'; // with explicit type annotation
let heart_eyed_cat = '';
}
注意,我们用单引号指定字符字面量,相对的用双引号指定字符串字面量。
Rust的char类型大小为4个字节,表示Unicode标量值,这意味着它可以表示比ASCII多得多的内容。重音字母;中文、日文、韩文;emoji;和零宽度空间都是Rust中的有效字符值。Unicode标量值的范围从U+0000
到U+D7FF
和U+E000
到U+10FFFF
(含)。然而,“character”在Unicode中并不是一个真正的概念,因此您对“字符”的直觉可能与Rust中的字符不匹配。使用字符串存储UTF-8编码文本
复合类型可以将多个值分组为一种类型。
Rust有两种基本复合类型: 元组
和数组
。
元组是一种将许多具有各种类型的值分组为一种复合类型的通用方法。元组有固定的长度:一旦声明,它们的大小就不能增加或缩小。
通过在圆括号内写入逗号分隔的值列表来创建元组。元组中的每个位置都有一个类型,并且元组中不同值的类型不必相同。
fn main() {
let tup: (i32, f64, u8) = (500, 6.4, 1);
}
变量tup
绑定到整个元组,因为元组被认为是单个复合元素。为了从元组中获取单个值,我们可以使用模式匹配( pattern matching)来解构元组值,像这样:
fn main() {
let tup = (500, 6.4, 1);
let (x, y, z) = tup; // 这被称为解构,因为它将单个元组分解为三个部分。
println!("The value of y is: {y}");
}
我们还可以直接访问tuple元素,方法是使用句点(.
)后跟我们想要访问的值的索引。
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。
没有任何值的元组有一个特殊的名称(unit
)。这个值及其对应的类型都是()
,表示空值或空返回类型。表达式如果不返回任何其他值,则隐式返回unit
值。
另一种拥有多个值集合的方法是使用数组。与元组不同,数组的每个元素必须具有相同的类型。与其他一些语言中的数组不同,Rust中的数组有固定的长度。
我们将数组中的值写成方括号内逗号分隔的列表:
fn main() {
let a = [1, 2, 3, 4, 5];
}
当想把数据分配到栈上而不是堆上时(栈和堆),或者当希望确保总是有固定数量的元素时,数组是很有用的。
不过,数组不像向量(vector )类型那样灵活。vector
是标准库提供的一种类似的集合类型,可以在大小上增加或缩小。如果不确定是使用数组还是vector
,那么很可能应该使用vector
(vector)。
但是,当您知道不需要更改元素的数量时,数组更有用。例如,如果你在程序中使用月份的名称,你可能会使用数组而不是vector,因为你知道它总是包含12个元素:
let months = ["January", "February", "March", "April", "May", "June", "July",
"August", "September", "October", "November", "December"];
使用方括号和每个元素的类型,一个分号,然后是数组中元素的数量来编写数组的类型,像这样:
// i32 是每个元素的类型。分号后面的数字5表示数组包含五个元素。
let a: [i32; 5] = [1, 2, 3, 4, 5];
也可以初始化一个数组来包含每个元素相同的值,方法是指定初始值,后面是分号,然后是方括号中的数组长度,如下所示:
// 等价 let a = [3, 3, 3, 3, 3]; 但更简洁
let a = [3; 5];
数组是一个已知的固定大小的内存块,可以分配到栈上。可以使用索引访问数组的元素,像这样:
fn main() {
let a = [1, 2, 3, 4, 5];
// 索引从 0 开始
let first = a[0];
let second = a[1];
}
如果试图访问超出数组末端的数组元素会发生什么。
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 {index} is: {element}");
}
# 如果输入的是数组结束之后的数字,例如10,则报错信息
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
这是Rust的内存安全原则的一个实例。当您提供不正确的索引时,就会访问无效的内存。Rust通过立即退出(而不是允许内存访问并继续)来防止这种错误。后续会更多地讨论了Rust的错误处理,以及如何编写可读的、安全的代码,既不会引起恐慌,也不会允许无效的内存访问。
该语言中最重要的函数之一:main
函数,它是许多程序的入口点。您还看到了fn
关键字,它允许您声明新函数。
Rust代码使用snake case
作为函数和变量名的常规格式,其中所有字母都是小写的,下划线是单独的单词。
fn main() {
println!("Hello, world!");
another_function();
}
fn another_function() {
println!("Another function.");
}
在Rust中,通过输入fn
、函数名和一对括号来定义函数。花括号告诉编译器函数体的开始和结束位置。
只要输入函数名后接一组括号,就可以调用我们定义的任何函数。
注意,我们在源代码中的main
函数之后定义了another_function
,之前也可以定义它。
Rust并不关心在哪里定义函数,只关心函数定义在调用者可以看到的作用域中的某个地方。
// 执行结果
Hello, world!
Another function.
这些行按照它们在main函数中出现的顺序执行。
可以定义具有参数的函数,这些参数是作为函数签名一部分的特殊变量。使用形参
和实参
这两个词来表示函数定义中的变量或调用函数时传入的具体值。
fn main() {
another_function(5);
}
fn another_function(x: i32) {
println!("The value of x is: {x}");
}
在函数签名中,必须声明每个参数的类型。(使用类型注释)
当定义多个形参时,用逗号分隔形参声明,像这样:
fn main() {
print_labeled_measurement(5, 'h');
}
fn print_labeled_measurement(value: i32, unit_label: char) {
println!("The measurement is: {value}{unit_label}");
}
函数体由一系列语句组成,可选地以表达式结尾。
到目前为止,所介绍的函数还没有包含结束表达式,但是您已经看到了表达式作为语句的一部分。因为Rust是一种基于表达式的语言,这是需要理解的一个重要区别。
语句
(Statements )是执行某些操作但不返回值的指令。表达式
(Expressions)求值得到结果值。使用let
关键字创建变量并为其赋值是一条语句。
fn main() {
let y = 6;
}
函数定义也是语句
语句不返回值。因此,您不能将let语句赋值给另一个变量,正如下面的代码尝试做的那样;你会得到一个错误:
fn main() {
let x = (let y = 6);
}
let y = 6
语句没有返回值,因此x
没有任何可以绑定的对象。这与C和Ruby等其他语言中的情况不同,在这些语言中,赋值操作返回赋值的值。在这些语言中,你可以写x = y = 6
,让x和y的值都为6;但在Rust 中却不是这样。
表达式(Expressions )求值为一个值,并构成将在Rust中编写的其余大部分代码。考虑一个数学操作,例如5 + 6
,它是一个求值为11的表达式。表达式可以是语句的一部分:在语句let y = 6;
中的6是求值为6的表达式。调用函数是一个表达式。调用宏是一个表达式。用花括号创建的新作用域块是一个表达式,例如:
fn main() {
let y = {
let x = 3;
x + 1
};
println!("The value of y is: {y}");
// The value of y is: 4
}
这个表达式:
{
let x = 3;
x + 1
}
是一个求值为4的块。该值作为let
语句的一部分被绑定到y
。注意,x + 1
行末尾没有分号,这与您目前看到的大多数行不同。表达式不包括结束分号
。如果在表达式末尾添加分号,则将其转换为语句,然后它将不返回值。在接下来探索函数返回值和表达式时,请记住这一点。
函数可以向调用它们的代码返回值。
我们不指定返回值的名称,但必须在箭头(->
)后声明其类型。
在Rust中,函数的返回值与函数体中最后的表达式的值相同。通过使用
return关键字并指定一个值,可以提前从函数返回,但大多数函数都隐式返回最后一个表达式。
fn five() -> i32 {
5
}
fn main() {
let x = five(); // 5
println!("The value of x is: {x}");
}
five
函数中没有函数调用、宏,甚至没有let语句——只有数字5本身。这在Rust中是一个完全有效的函数。注意,函数的返回类型也被指定为-> i32
。
这里有两个重要的点:
首先,这行let x = five();
显示我们正在使用函数的返回值来初始化变量。
其次,five
函数没有形参并定义了返回值的类型,但是函数体是一个没有分号的单独的5,因为它是我们想要返回的表达式。
另一个例子:
fn main() {
let x = plus_one(5);
println!("The value of x is: {x}");
}
fn plus_one(x: i32) -> i32 {
x + 1
}
如果在包含x + 1
的行末尾加上分号,将其从表达式更改为语句,则会出现如下错误:
主要的错误消息“类型不匹配”揭示了这段代码的核心问题。函数plus_one
的定义说它将返回一个i32
,但是语句不会求值,该值由unit类型()
表示。因此,没有返回任何东西,这与函数定义相矛盾,并导致错误。在这个输出中,Rust提供了一条消息,可能有助于纠正这个问题:它建议删除分号,这将修复错误。
所有的程序员都努力使他们的代码容易理解,但有时额外的解释是必要的。在这些情况下,程序员在源代码中留下注释,编译器将忽略这些注释,但阅读源代码的人可能会发现这些注释很有用。
简单的注释:
// hello, world
在Rust中,惯用的注释样式以两个斜杠开始注释,并且注释一直持续到行尾。对于超出一行的注释,你需要在每一行包含//,像这样:
// 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.
注释也可以放在包含代码的行的末尾:
fn main() {
let lucky_number = 7; // I’m feeling lucky today
}
注释在代码上面的单独一行上:
fn main() {
// I’m feeling lucky today
let lucky_number = 7;
}
Rust还有另一种注释:文档注释,参考 Publishing a Crate to Crates.io
根据条件是否为真来运行代码的能力,或者在条件为真时重复运行代码的能力,是大多数编程语言的基本构建模块。控制Rust代码执行流的最常见构造是if
表达式和循环。
if
表达式if
表达式允许根据条件对代码进行分支。
fn main() {
let number = 3;
if number < 5 {
println!("condition was true");
} else {
println!("condition was false");
}
}
所有if
表达式都以关键字if
开头,后面跟着一个条件。
如果条件为真,我们就把要执行的代码块放在条件后面的花括号内。与if
表达式中的条件相关联的代码块有时被称为arms
。
else
表达式还可以选择包含一个else
表达式,以便在条件求值为false
时为程序提供一个可选的代码块来执行。如果不提供else
表达式且条件为false
,则程序将跳过If块,继续执行下一段代码。
值得注意的是,这段代码中的条件必须是bool类型。如果条件不是bool类型,将得到一个错误。
fn main() {
let number = 3;
if number {
println!("number was three");
}
}
if条件这次的计算结果为3,Rust抛出一个错误
该错误表明Rust期望的是bool
值,但得到的是整数。与Ruby和JavaScript等语言不同,Rust不会自动尝试将非布尔类型转换为布尔类型。必须是显式的,并且总是用一个布尔值作为if的条件。
else if
处理多个条件fn main() {
let number = 6;
if number % 4 == 0 {
println!("number is divisible by 4");
} else if number % 3 == 0 {
println!("number is divisible by 3");
} else if number % 2 == 0 {
println!("number is divisible by 2");
} else {
println!("number is not divisible by 4, 3, or 2");
}
}
当这个程序执行时,它依次检查每个if
表达式,并执行条件为真的第一个主体。
注意,尽管6能被2整除,但我们看不到 number is divisible by 2
, 也看不到 number is not divisible by 4, 3, or 2
这是因为Rust只对第一个真条件执行块,一旦找到一个,它甚至不会检查其他的。
使用太多的else if表达式会使代码变得混乱,因此,如果有多个else if表达式,可能需要重构代码。针对这些情况描述了一个名为match的强大Rust分支结构。
let
语句中使用if
因为if
是一个表达式,可以在let语句的右侧使用它来将结果分配给一个变量,
fn main() {
let condition = true;
let number = if condition { 5 } else { 6 };
println!("The value of number is: {number}");
}
// 结果:The value of number is: 5
number变量将根据if
表达式的结果被绑定到一个值。
记住,代码块的计算结果是其中的最后一个表达式,数字本身也是表达式。
在本例中,整个if表达式的值取决于执行的代码块。这意味着if语句的每个分支都有可能是结果的值,所有必须是相同的类型
loop
关键字告诉Rust一遍又一遍地执行一个代码块,直到显式地告诉它停止为止。例如,
fn main() {
loop {
println!("again!");
}
}
可以在循环中放置break
关键字,以告诉程序何时停止执行循环。
continue
关键字,它在循环中告诉程序跳过这个循环迭代中所有剩余的代码,进入下一个迭代。
loop
的用途之一是重试知道可能失败的操作,例如检查线程是否已完成其工作。可能还需要将该操作的结果传递到循环之外的代码。为此,可以在用于停止循环的break
表达式之后添加希望返回的值,该值将从循环中返回,以便您可以使用它,如下所示:
fn main() {
let mut counter = 0;
let result = loop {
counter += 1;
if counter == 10 {
break counter * 2;
}
};
println!("The result is {result}"); // 20
}
如果循环中有循环,break
和 continue
应用在最里面的循环。可以选择在循环上指定一个循环标签( loop label
),然后可以使用break
和 continue
指定关键字应用于带标签的循环,而不是最内层的循环。循环标签必须以单引号开始。
fn main() {
let mut count = 0;
'counting_up: loop {
println!("count = {count}");
let mut remaining = 10;
loop {
println!("remaining = {remaining}");
if remaining == 9 {
break;
}
if count == 2 {
break 'counting_up;
}
remaining -= 1;
}
count += 1;
}
println!("End count = {count}");
}
结果:
count = 0
remaining = 10
remaining = 9
count = 1
remaining = 10
remaining = 9
count = 2
remaining = 10
End count = 2
程序通常需要在循环中计算条件。当条件为真时,循环运行。当条件不再为真时,程序调用break
,停止循环。但是,这种模式非常常见,Rust为此有一个内置的语言构造,称为while
循环。
fn main() {
let mut number = 3;
while number != 0 {
println!("{number}!");
number -= 1;
}
println!("LIFTOFF!!!");
}
这种构造消除了大量的嵌套,而如果使用loop
、if
、else
和break
,则需要嵌套,而且更清晰。当条件为真时,代码运行;否则,它退出循环。
for ... in
可以选择使用while结构来循环遍历集合(例如数组)的元素。
fn main() {
let a = [10, 20, 30, 40, 50];
let mut index = 0;
while index < 5 {
println!("the value is: {}", a[index]);
index += 1;
}
}
然而,这种方法容易出错;如果索引值或测试条件不正确,可能会导致程序恐慌。例如,如果将a数组的定义更改为有4个元素,但忘记将条件更新为当index < 4时,代码将陷入混乱。它还很慢,因为编译器添加运行时代码来执行条件检查,在循环的每次迭代中,索引是否在数组的边界内。
作为一种更简洁的替代方法,可以使用for
循环并为集合中的每一项执行一些代码。
fn main() {
let a = [10, 20, 30, 40, 50];
for element in a {
println!("the value is: {element}");
}
}
现在增加了代码的安全性,并消除了由于超出数组末尾或不够远而丢失某些项而导致的bug的可能性。
使用for循环,如果更改数组中的值的数量,就不需要记得更改任何其他代码。
for循环的安全性和简洁性使其成为Rust中最常用的循环结构。即使在需要运行某些代码一定次数的情况下(如清单3-3中使用while循环的倒计时示例),大多数Rustaceans也会使用for循环。实现这一点的方法是使用标准库提供的Range
,它按顺序生成所有数字,从一个数字开始,到另一个数字之前结束。
下面是使用for循环和另一个我们还没有谈到的方法rev来反转范围的倒计时效果:
fn main() {
for number in (1..4).rev() {
println!("{number}!");
}
println!("LIFTOFF!!!");
}
结果:
3!
2!
1!
lifeoff
Ownership
)所有权是Rust最独特的特性,对该语言的其他部分具有深远的影响。它使Rust能够在不需要垃圾收集器的情况下保证内存安全,因此理解所有权是如何工作的非常重要。在本章中,我们将讨论所有权以及几个相关特性:借用(borrowing)、切片(slice)以及Rust如何在内存中布局数据。
所有权是一组控制Rust程序如何管理内存的规则。
所有程序在运行时都必须管理它们使用计算机内存的方式。有些语言具有垃圾收集功能,在程序运行时定期查找不再使用的内存;在其他语言中,程序员必须显式地分配和释放内存。
Rust使用了第三种方法:通过一个拥有一组由编译器检查的规则的所有权系统来管理内存。如果违反任何规则,程序将无法编译。所有权的任何特性都不会在程序运行时减慢它的速度。
对于Rust和所有权系统的规则的经验越丰富,就越容易自然而然地开发出安全和高效的代码。坚持下去!
当你理解了所有权,你就有了一个坚实的基础来理解使Rust独一无二的特性。
许多编程语言不要求经常考虑栈和堆。但是在像Rust这样的系统编程语言中,值是在栈上还是在堆上都会影响该语言的行为以及为什么必须做出某些决定。
栈和堆都是代码可以在运行时使用的内存部分,但是它们以不同的方式构造。栈按获取的顺序存储值,并以相反的顺序删除值。这被称为后进先出(last in, first out
)。
添加数据称为入栈(pushing
),删除数据称为出栈(popping
)。
存储在栈上的所有数据必须具有已知的固定大小。编译时大小未知或大小可能发生变化的数据必须存储在堆中。
堆的组织性较差:当将数据放到堆上时,需要一个确定大小的空间。内存分配器在堆中找到一个足够大的空间,将其标记为正在使用,并返回一个指针,这是该位置的地址。这个过程称为堆上的分配,有时简写为分配(allocating
)(将值压到栈上并不认为是分配)。因为指向堆的指针是已知的固定大小,所以可以将指针存储在栈上,但是当需要实际数据时,必须依靠指针。
入栈比在堆上分配要快,因为分配器从来不需要寻找存储新数据的位置;这个位置总是在栈的顶部。相比之下,在堆上分配空间需要更多的工作,因为分配器必须首先找到足够大的空间来保存数据,然后执行记账(bookkeeping),为下一次分配做准备。
访问堆中的数据要比访问栈中的数据慢,因为必须通过指针才能到达那里。当前的处理器如果在内存中跳来跳去的更少,就会更快。出于同样的原因,如果处理与其他数据很接近的数据(比如栈上的数据),而不是处理距离较远的数据(比如堆上的数据),处理器可以更好地完成工作。
当代码调用函数时,传递给函数的值(包括指向堆上数据的指针)和函数的局部变量被推入栈。当函数结束时,这些值从栈中弹出。
跟踪代码的哪些部分使用了堆上的哪些数据,最小化堆上的重复数据量,清理堆上未使用的数据,以免耗尽空间,这些都是所有权所解决的问题。一旦理解了所有权,就不需要经常考虑栈和堆,但是知道所有权的主要目的是管理堆数据,可以帮助解释它为什么以这种方式工作。
作为所有权的第一个例子,将研究一些变量的作用域。作用域是程序中对于某项(item)有效的范围。
fn main() {
{ // s is not valid here, it’s not yet declared
let s = "hello"; // s is valid from this point forward
// do stuff with s
} // this scope is now over, and s is no longer valid
}
变量从声明它的时刻起一直有效直到当前作用域结束。
换句话说,这里有两个重要的点:
此时,作用域和变量何时有效之间的关系与其他编程语言中的关系类似。现在,将在此基础上引入String
类型。
前面介绍的类型都是已知的大小,可以存储在栈中,并在作用域结束时弹出栈,如果代码的另一部分需要在不同的作用域中使用相同的值,则可以快速而简单地复制它们,以创建一个新的独立实例。但我们希望查看存储在堆上的数据,并探索Rust如何知道何时清理这些数据,String
类型就是一个很好的例子。
将集中讨论String
中与所有权相关的部分。这些方面也适用于其他复杂的数据类型,无论它们是由标准库提供的还是自己创建的。字符串
已经看到了字符串字面量,其中字符串值被硬编码到程序中。字符串字面值很方便,但并不适用于想要使用文本的所有情况。一个原因是它们是不可变的。另一个原因是,在编写代码时,并不是每个字符串值都是已知的:例如,如果我们想获取用户输入并存储它,该怎么办?对于这些情况,Rust有第二个字符串类型String
。这种类型管理分配在堆上的数据,因此能够存储在编译时我们不知道的大量文本。可以使用from
函数从字符串字面量创建一个String
,像这样:
let s = String::from("hello");
双冒号::
操作符允许我们在String
类型下命名这个特定的from
函数,而不是使用诸如string_from
之类的名称。参考“方法语法(“Method Syntax” )”和“Paths for Referring to an Item in the Module Tree”
这类字符串可以被改变:
fn main() {
let mut s = String::from("hello");
s.push_str(", world!"); // push_str() appends a literal to a String
println!("{}", s); // This will print `hello, world!`
}
那么,这里有什么不同呢?为什么String
可以改变而字面量不能?不同之处在于这两种类型如何处理内存。
对于String
类型,为了支持可变的、可增长的文本段,需要在堆上分配一定数量的内存量(在编译时未知)来保存内容。这意味着:
第一部分是由我们完成的:当我们调用String::from
时,它的实现请求所需的内存。这在编程语言中非常普遍。
然而,第二部分是不同的。在具有垃圾收集器(GC)的语言中,GC会跟踪并清理不再使用的内存,我们不需要考虑它。在大多数没有GC的语言中,我们负责识别何时不再使用内存并调用代码显式地释放内存,就像我们请求内存一样。正确地做到这一点一直是一个困难的编程问题。如果我们忘记了,就会浪费内存。如果我们做得太早,就会得到一个无效变量。如果我们做两次,那也是个bug。我们需要对一个allocate
函数和一个free
函数进行配对。
Rust采取了不同的路径:一旦拥有内存的变量超出作用域,内存就会自动返回。
{
let s = String::from("hello"); // s is valid from this point forward
// do stuff with s
} // this scope is now over, and s is no
// longer valid
有一个自然的点可以将String
需要的内存返回给分配器:当s超出作用域时。当变量超出作用域时,Rust为我们调用一个特殊的函数。这个函数称为drop
,它是String
的作者可以放置代码以返回内存的地方。Rust在右花括号处自动调用drop
。
注意:在c++中,这种在项目生命周期结束时释放资源的模式有时被称为资源获取即初始化(Resource Acquisition is Initialization, RAII)。如果你使用过RAII模式,你就会熟悉Rust中的drop函数。
这种模式对Rust代码的编写方式有着深远的影响。现在看起来可能很简单,但是在更复杂的情况下,当我们想让多个变量使用堆上分配的数据时,代码的行为可能会出乎意料。
Move
)在Rust中,多个变量可以以不同的方式与相同的数据交互。
let x = 5;
let y = x;
大概可以猜到这是在做什么:“将值5绑定到x;然后复制x中的值,并将其绑定到y上。”现在有了两个变量,x和y,它们都等于5。因为整数是具有已知固定大小的简单值,而这两个5
值被入栈。
现在看看String版本:
let s1 = String::from("hello");
let s2 = s1;
这看起来非常相似,所以我们可以假设它的工作方式是相同的:即,第二行将生成s1
中的值的副本,并将其绑定到s2
。但事实并非如此。
字符串由三个部分组成,如左侧所示:一个指向存储字符串内容的内存的指针、一个长度和一个容量。这组数据存储在栈上。右边是堆上保存内容的内存。
长度是String
内容当前使用的内存大小(以字节为单位)。容量是String
从分配器接收到的内存总量,以字节为单位。长度和容量之间的差异很重要,但在这个上下文中不是这样,所以现在可以忽略容量。
当将s1
赋值给s2
时,将复制String
数据,这意味着复制栈上的指针、长度和容量。我们不复制指针所指向的堆上的数据(如果Rust也复制堆数据。那么如果堆上的数据很大,那么从运行时性能来看,操作s2 = s1
可能非常昂贵。)。换句话说,数据在内存中的表示如下图所示。
为了确保内存安全,在let s2 = s1
之后,Rust认为s1
不再有效。因此,当s1超出作用域时,Rust不需要释放任何东西。
let s1 = String::from("hello");
let s2 = s1;
println!("{}, world!", s1);
如果在使用其他语言时听说过术语浅拷贝( shallow copy
)和深拷贝(deep copy
),那么在不复制数据的情况下复制指针、长度和容量的概念可能听起来像进行浅拷贝。但是因为Rust也使第一个变量无效,而不是称之为浅拷贝,它被称为移动(move)
。在这个例子中,我们说s1
被移到了s2
。实际情况如下图所示。
这就解决了我们的问题!只有s2
有效,当它超出作用域时,它将单独释放内存,我们就完成了。
此外,这暗示了一个设计选择:Rust永远不会自动创建数据的“深度”副本。因此,就运行时性能而言,任何自动复制都可以被认为是廉价的。
Clone
)如果确实想要深度复制String
的堆数据,而不仅仅是栈数据,可以使用一个名为clone
的常见方法
let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1 = {}, s2 = {}", s1, s2);
当看到clone
调用时,就知道正在执行一些任意的代码,而且这些代码可能非常昂贵。这是一种视觉信号,表明发生了一些不同的事情。
Copy
)还有一个问题还没说到。这段使用整数的代码是有效的:
let x = 5;
let y = x;
println!("x = {}, y = {}", x, y);
但这段代码似乎与我们刚刚学到的内容相矛盾:我们没有调用Clone
,但x
仍然有效,并且没有移动到y
中。
原因是,在编译时具有已知大小的整数等类型完全存储在栈中,因此可以快速复制实际值。这意味着在创建变量y
后,没有理由阻止x
有效。换句话说,这里的深度复制和浅复制没有区别,因此调用clone与通常的浅复制没有任何不同,我们可以忽略它。
Rust有一个叫做Copy
trait的特殊注解,可以把它放在在栈中存储的类型上,比如整数(更多关于trait的内容)。如果一个类型实现了Copy
特性,使用它的变量不会移动,而是简单地复制,使它们在赋值给另一个变量后仍然有效。
如果该类型或其任何一部分实现了Drop
特性,Rust不允许用Copy来注释一个类型。要了解如何将Copy
注释添加到类型中以实现trait
,参见“可派生trait”(Derivable Traits)。
那么什么类型实现了Copy特性呢?可以检查给定类型的文档以确定是否正确,但作为一般规则,任何简单标量值组都可以实现Copy
,任何需要分配或某种形式的资源的都不能实现Copy
。下面是实现Copy
的一些类型:
u32
。bool
,值为true
和false
。f64
。char
。Copy
的类型。例如,(i32, i32)
实现Copy
,但(i32, String)
不实现Copy
。将值传递给函数的机制类似于将值赋给变量的机制。将变量传递给函数将会移动或复制,就像赋值一样。
fn main() {
let s = String::from("hello"); // s comes into scope
takes_ownership(s); // s's value moves into the function...
// ... and so is no longer valid here
let x = 5; // x comes into scope
makes_copy(x); // x would move into the function,
// but i32 is Copy, so it's okay to still
// use x afterward
} // Here, x goes out of scope, then s. But because s's value was moved, nothing
// special happens.
fn takes_ownership(some_string: String) { // some_string comes into scope
println!("{}", some_string);
} // Here, some_string goes out of scope and `drop` is called. The backing
// memory is freed.
fn makes_copy(some_integer: i32) { // some_integer comes into scope
println!("{}", some_integer);
} // Here, some_integer goes out of scope. Nothing special happens.
如果尝试在调用takes_ownership
之后使用s
, Rust将抛出编译时错误。这些静态检查避免错误。
返回值还可以转移所有权
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
}
变量的所有权每次都遵循相同的模式:将值赋给另一个变量会移动该变量
。当包含堆上数据的变量超出作用域时,除非数据的所有权已移动到另一个变量,否则该值将被drop
清除。
虽然这是可行的,但每个函数获得所有权,然后又返回所有权,多少有点乏味。如果我们想让函数使用值但不拥有所有权,该怎么办?
如果我们想要再次使用它,除了我们可能想要返回的函数体产生的任何数据之外,我们传入的任何内容也需要返回,这是非常烦人的。
Rust允许我们使用一个元组返回多个值
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) { // s is a reference to a String
let length = s.len(); // len() returns the length of a String
(s, length)
}// Here, s goes out of scope. But because it does not have ownership of what
// it refers to, it is not dropped.
但对于一个本应普遍的概念来说,这太过繁文缛节,工作量也太大了。幸运的是,Rust有一个使用值而不转移所有权的特性,称为引用(reference
)。
上例的元组代码的问题是,我们必须将String
返回给调用函数,以便在调用calculate_length
之后仍然可以使用String
,因为String
已移动到calculate_length
中。相反,我们可以提供String
值的引用。引用就像一个指针,它是一个地址,我们可以通过它访问存储在该地址的数据;该数据属于其他变量。
与指针不同,引用保证在其生命周期内指向特定类型的有效值。
下面是如何定义和使用calculate_length函数,它引用一个对象作为参数,而不是获得值的所有权:
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("The length of '{}' is {}.", s1, len);
}
fn calculate_length(s: &String) -> usize {
s.len()
}
首先,注意变量声明和函数返回值中的所有元组代码都消失了。
其次,注意我们将&s1
传递给calculate_length
,并且在它的定义中,我们使用&String
而不是String
。&
表示引用,它们允许您引用某个值,而不必拥有该值的所有权。如下图:
注意:与使用
&
的引用相反的是解引用,它是通过解引用操作符*
完成的。
仔细看看这里的函数调用:
let s1 = String::from("hello");
let len = calculate_length(&s1);
使用&s1
语法,我们可以创建引用s1
的值,但不拥有它。因为它不拥有它,所以当引用停止使用时,它所指向的值不会被删除。
同样,函数的签名使用&
表示形参s
的类型是引用。
把创建引用(reference )的动作称为借用(borrowing)。就像在现实生活中,如果一个人拥有某物,你可以向他借。用完之后,你得把它还给我。你不拥有它。
如果我们试图修改我们借用的东西会发生什么呢?剧透:它不管用!
正如变量默认是不可变的一样,
引用也是不可变的
。
可以修改代码,允许通过一些小的调整(使用可变引用)来修改借用值,:
fn main() {
let mut s = String::from("hello");
change(&mut s);
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
}
首先,把s
变成mut
。我们用&mut s
创建一个可变引用,在这里调用change
函数,并更新函数签名以接受一个带有some_string: &mut String
的可变引用。这非常清楚地表明,change
函数将改变它所借用的值。
可变引用有一个很大的限制:如果对一个值的有一个可变引用,那么就不能有对该值的其他引用
。
这段代码试图创建两个对s的可变引用将失败:
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s;
println!("{}, {}", r1, r2);
禁止同时对同一数据进行多个可变引用的限制允许以一种非常可控的方式进行改变。这是新russtacans难以应对的问题,因为大多数语言允许随时改变。有这个限制的好处是Rust可以防止编译时的数据竞争(data race
)。数据竞争类似于竞争条件,当出现以下三种行为时发生:
数据竞争会导致未定义的行为,当试图在运行时追踪它们时,可能很难诊断和修复;Rust通过拒绝编译带有数据竞争的代码来防止这个问题!
和往常一样,可以使用花括号来创建一个新的作用域,允许多个可变引用,但不能同时使用:
let mut s = String::from("hello");
{
let r1 = &mut s;
} // r1 goes out of scope here, so we can make a new reference with no problems.
let r2 = &mut s;
Rust对同时有可变和不可变引用实施了类似的规则。这段代码会导致一个错误:
let mut s = String::from("hello");
let r1 = &s; // no problem
let r2 = &s; // no problem
let r3 = &mut s; // BIG PROBLEM
println!("{}, {}, and {}", r1, r2, r3);
注意,引用的作用域从它被引入的地方开始,一直延续到该引用最后一次被使用的时候。例如,这段代码将会编译,因为不可变引用的最后一次使用println!
,在引入可变引用之前发生:
let mut s = String::from("hello");
let r1 = &s; // no problem
let r2 = &s; // no problem
println!("{} and {}", r1, r2);
// variables r1 and r2 will not be used after this point
let r3 = &mut s; // no problem
println!("{}", r3);
不可变引用r1
和r2
的作用域在最后被使用的println!
的地方结束,也就是在可变引用r3
创建之前。这些作用域不重叠,因此此代码是允许的。编译器判断引用在作用域结束之前不再被使用的能力被称为非词法生命期(Non-Lexical lifetime,简称NLL),您可以在Edition Guide中阅读更多关于它的信息。可以在Edition Guide中阅读
尽管有时借用错误可能令人沮丧,但请记住,是Rust编译器在早期(在编译时而不是在运行时)指出了潜在的错误,并准确地指出了问题所在。这样就不必去追踪为什么数据不是想的那样。
在有指针的语言中,很容易错误地创建悬浮指针,这个指针,引用了内存中可能已经给了其他人的位置——通过释放一些内存,同时保留指向该内存的指针。相比之下,在Rust中,编译器保证引用永远不会是悬浮引用:如果对某些数据有引用,在引用数据之前,编译器将确保数据不会超出作用域。
fn main() {
let reference_to_nothing = dangle();
}
fn dangle() -> &String { // dangle returns a reference to a String
let s = String::from("hello"); // s is a new String
&s // we return a reference to the String, s
} // Here, s goes out of scope, and is dropped. Its memory goes away.
// Danger!
这里的解决方案是直接返回String:
fn no_dangle() -> String {
let s = String::from("hello");
s
}
切片允许引用集合中连续的元素序列,而不是整个集合。切片是一种引用,因此它没有所有权。
这里有一个小的编程问题:编写一个函数,它接受一个由空格分隔的单词字符串,并返回它在该字符串中找到的第一个单词。如果函数在字符串中没有找到空格,则整个字符串必须是一个单词,因此应该返回整个字符串。
来看看如何在不使用切片的情况下编写这个函数的签名,来理解切片将解决的问题:
fn first_word(s: &String) -> usize {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return i;
}
}
s.len()
}
因为需要逐个遍历String
元素并检查值是否为空格,所以使用as_bytes
方法将String
转换为一个字节数组
let bytes = s.as_bytes();
接下来,我们使用iter
方法在字节数组上创建一个迭代器:
iterators
for (i, &item) in bytes.iter().enumerate() {
现在,iter
是一个方法,它返回集合中的每个元素,而enumerate
将iter
的结果包装起来,并将每个元素作为元组的一部分返回。
从enumerate
返回的元组的第一个元素是索引,第二个元素是对元素的引用。
因为enumerate
方法返回一个元组,所以可以使用模式
来解构该元组。在for循环中,指定了一个模式,其中i
用于元组的索引,&item
用于元组的单字节。因为从.iter().enumerate()
获得了对元素的引用,所以在模式中使用&
。模式
现在有了一种方法来查找字符串中第一个单词末尾的索引,但是有一个问题。返回的是usize
本身,但它只是在&String
的上下文中有意义的数字。换句话说,因为它是String的一个独立值,所以不能保证它将来仍然有效。
fn main() {
let mut s = String::from("hello world");
let word = first_word(&s); // word will get the value 5
s.clear(); // this empties the String, making it equal to ""
// word still has the value 5 here, but there's no more string that
// we could meaningfully use the value 5 with. word is now totally invalid!
}
这个程序编译时没有任何错误,如果在调用s.clear()
之后使用word
也会可以。因为word
根本没有连接到s
的状态,所以word
仍然包含值5
。我们可以使用值5
和变量s
来尝试提取出第一个单词,但这将是一个错误,因为在word
中保存5
后s
的内容发生了更改。
不得不担心word
中的索引与s
中的数据不同步是乏味和容易出错的!
幸运的是,Rust对这个问题有一个解决方案:字符串切片
字符串切片是对字符串部分的引用。
let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6..11];
通过指定[starting_index..ending_index]
,来创建切片。
其中starting_index
是片中的第一个位置,ending_index
比片中的最后一个位置多一个。
在内部,切片数据结构存储了切片的起始位置(一个指针)和长度,对应于ending_index
减去starting_index
。因此,在let world = &s[6..11];
的情况下,world
将是一个切片,其中包含一个指针,指向s
索引为6
的字节,长度值为5
。
Rust的..
范围语法,如果想从索引0开始,可以删除两个句点之前的值。换句话说,它们是相等的:
let s = String::from("hello");
let slice = &s[0..2];
let slice = &s[..2];
同样,如果切片包含String
的最后一个字节,则可以删除尾随数。这意味着它们相等:
let s = String::from("hello");
let len = s.len();
let slice = &s[3..len];
let slice = &s[3..];
还可以删除这两个值以获取整个字符串的切片。所以它们相等:
let s = String::from("hello");
let len = s.len();
let slice = &s[0..len];
let slice = &s[..];
注意:字符串切片范围索引必须出现在有效的UTF-8字符边界上。
如果试图在多字节字符中间创建字符串切片,程序将退出并显示错误。为了引入字符串切片,我们假设本节只使用ASCII;
关于UTF-8处理的更深入的讨论“ Storing UTF-8 Encoded Text with Strings”
重写first_word
以返回一个切片。表示“字符串切片”的类型被写成&str
:
fn first_word(s: &String) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
现在,当调用first_word
时,返回一个绑定到底层数据的值。
我们现在有了一个简单的API,它很难搞砸,因为编译器将确保对String
的引用保持有效。
使用first_word
的slice
版本会抛出编译时错误:
fn main() {
let mut s = String::from("hello world");
let word = first_word(&s);
s.clear(); // error!
println!("the first word is: {}", word);
}
回想一下借用规则,如果我们有一个对某物的不可变引用,我们也不能采用一个可变引用。因为clear
需要截断String
,所以它需要获得一个可变引用。println!
在调用clear
之后,在word
中使用了引用,因此不可变引用在那时必须仍然是活动的。Rust禁止clear
中的可变引用和word
中的不可变引用同时存在,编译会失败。Rust不仅使我们的API更容易使用,而且还在编译时消除了一整类错误!
现在知道了切片,可以正确地理解字符串字面量:
let s = "Hello, world!";
这里s
的类型是&str
:它是一个指向二进制文件中特定点的切片。这也是为什么字符串字面量是不可变的;&str
是一个不可变引用。
知道取字面量和字符串值的切片,对first_word
可以进行了更多的改进,这是它的签名:
fn first_word(s: &String) -> &str {
更有经验的Rustacean会编写如下的签名,因为它允许在&String
值和&str
值上使用相同的函数。
fn first_word(s: &str) -> &str {
如果我们有一个字符串切片,我们可以直接传递它。如果我们有一个String
,我们可以传递一个String
的切片或对String
的引用。这种灵活性利用了deref coercions。定义一个函数来接受字符串切片,而不是对String
的引用,使我们的API更加通用和有用,而不会丢失任何功能:
fn main() {
let my_string = String::from("hello world");
// `first_word` works on slices of `String`s, whether partial or whole
let word = first_word(&my_string[0..6]);
let word = first_word(&my_string[..]);
// `first_word` also works on references to `String`s, which are equivalent
// to whole slices of `String`s
let word = first_word(&my_string);
let my_string_literal = "hello world";
// `first_word` works on slices of string literals, whether partial or whole
let word = first_word(&my_string_literal[0..6]);
let word = first_word(&my_string_literal[..]);
// Because string literals *are* string slices already,
// this works too, without the slice syntax!
let word = first_word(my_string_literal);
}
字符串切片是特定于字符串的。但还有一种更常见的切片。
正如我们可能要引用字符串的一部分一样,我们也可能要引用数组的一部分。我们会这样做:
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3];
assert_eq!(slice, &[2, 3]);
该切片的类型为&[i32]
。它的工作方式与字符串切片相同,即存储对第一个元素的引用和长度。也会在所有其他的集合中使用这种切片。
所有权、借用和切片的概念确保了Rust程序在编译时的内存安全。
struct
)构造相关数据结构体( struct, or structure
)是一种自定义数据类型,允许将多个相关值打包在一起并命名,这些值组成一个有意义的组 (group)。
下面将比较和对比元组和结构体,并演示何时结构体是更好的数据分组方式。
我们将演示如何定义和实例化结构体。我们将讨论如何定义关联函数(associated functions),特别是称为方法的关联函数,以指定与结构体类型相关的行为。结构体(struct)和枚举(enum)是在程序域中创建新类型的构建块,以充分利用Rust的编译时类型检查。
结构体类似于元组,因为两者都包含多个相关值。与元组一样,结构体的各个部分可以是不同的类型。与元组不同的是,在结构体中,您将为每个数据块命名,这样就可以清楚地了解值的含义。添加这些名称意味着结构体比元组更灵活:您不必依赖于数据的顺序来指定或访问实例的值。
要定义一个结构体,输入关键字struct
并命名(name )整个结构体。结构体的名称应该描述被分组在一起的数据块的意义。然后,在花括号内定义数据块的名称和类型,我们称之为字段 (field
)。例如,如下显示了一个存储用户帐户信息的结构体:
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
要在定义了结构体之后使用它,我们可以通过为每个字段指定具体的值来创建该结构体的实例。我们不必按照与在结构体中声明字段相同的顺序指定字段。
fn main() {
let user1 = User {
email: String::from("[email protected]"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
}
为了从结构体中获得特定的值,我们使用点表示法。
fn main() {
let mut user1 = User {
email: String::from("[email protected]"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
user1.email = String::from("[email protected]");
}
注意,整个实例必须是可变的;Rust不允许只将某些字段标记为可变的。与任何表达式一样,可以构造结构体的新实例作为函数体中的最后一个表达式,隐式地返回该新实例。
fn build_user(email: String, username: String) -> User {
User {
email: email,
username: username,
active: true,
sign_in_count: 1,
}
}
use the field init shorthand
syntax to rewrite build_user
fn build_user(email: String, username: String) -> User {
User {
email,
username,
active: true,
sign_in_count: 1,
}
}
将email
字段的值设置为build_user
函数的email
参数中的值。因为email
字段和email
参数具有相同的名称,所以我们只需要写email
而不是email: email
。
创建包含来自另一个实例的大部分值,但更改一些值的新实例,通常很有用。可以使用结构体更新语法(struct update syntax
)来完成此操作。
首先,展示了如何在user2
中定期创建一个新的User
实例,而不使用更新语法。
fn main() {
// --snip--
let user2 = User {
active: user1.active,
username: user1.username,
email: String::from("[email protected]"),
sign_in_count: user1.sign_in_count,
};
}
使用结构体更新语法,我们可以用更少的代码达到同样的效果
语法..
指定未显式设置的其余字段应具有与给定实例中的字段相同的值。
fn main() {
// --snip--
let user2 = User {
email: String::from("[email protected]"),
..user1
};
}
在user2
中创建了一个实例,该实例对email
具有不同的值,但对user1
中的username
、active
和sign_in_count
字段具有相同的值。. .User1
必须排在最后,以指定任何剩余字段应该从User1
中的相应字段中获取它们的值,但可以选择以任何顺序为任意数量的字段指定值,而不管这些字段在结构体定义中的顺序。
注意,结构体更新语法使用=
类似于赋值;这是因为它移动数据。在这个例子中,在创建user2
之后就不能再使用user1
了,因为user1
的username
字段中的String
被移到了user2
中。如果我们为user2
提供了email
和username
的新String
值,因此只使用user1
中的active
和sign_in_count
值,那么user1
在创建user2
后仍然有效。active
和sign_in_count
的类型是实现Copy
特性的类型,因此在“Stack-Only Data: Copy”一节中讨论的行为将适用。
Rust还支持类似于元组的结构体,称为元组结构体(Tuple Structs
)。
元组结构体具有结构体名提供的附加含义,但没有与其字段相关联的名称;相反,它们只有字段的类型。当想为整个元组命名并使元组的类型与其他元组不同时,以及将每个字段命名为常规结构体中将显得冗长或多余时,元组结构体非常有用。
要定义一个元组结构体,从struct
关键字和结构体名称开始,后面跟着元组中的类型。例如,这里定义并使用了两个名为Color和Point的元组结构体:
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
fn main() {
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
}
注意,black
和origin
是不同的类型,因为它们是不同元组结构体的实例。定义的每个结构体都是自己的类型,即使结构体中的字段可能具有相同的类型。
Unit-Like Structs Without Any Fields
还可以定义没有任何字段的结构体!这些被称为unit-like structs
,因为它们的行为类似于()
,即我们在“元组类型”一节中提到的unit type
。当需要在某些类型上实现trait
,但没有任何想要存储在类型本身中的数据时,类单元结构体可能很有用。trait。下面是一个声明并实例化名为AlwaysEqual的 unit 结构体的例子:
struct AlwaysEqual;
fn main() {
let subject = AlwaysEqual;
}
在User
结构体定义中,我们使用了拥有的String
类型而不是&str
字符串切片类型。这是经过深思熟虑的选择,因为我们希望这个结构体的每个实例都拥有它的所有数据,并且只要整个结构体有效,这些数据就有效。
结构体也可以存储对其他东西拥有的数据的引用,但这样做需要使用生命周期(lifetimes
),这是以后要讨论的Rust特性。生命周期确保结构体引用的数据在该结构体存在的时间内都有效。假设试图在结构体中存储引用而不指定生存期,如下所示;这是行不通的:
struct User {
active: bool,
username: &str,
email: &str,
sign_in_count: u64,
}
fn main() {
let user1 = User {
email: "[email protected]",
username: "someusername123",
active: true,
sign_in_count: 1,
};
}
fn main() {
let width1 = 30;
let height1 = 50;
println!(
"The area of the rectangle is {} square pixels.",
area(width1, height1)
);
}
fn area(width: u32, height: u32) -> u32 {
width * height
}
面积函数应该计算一个矩形的面积,但是我们写的函数有两个参数,在我们的程序中没有明确的地方指明这两个参数是相关的。将宽度和高度组合在一起将更易于阅读和管理。
fn main() {
let rect1 = (30, 50);
println!(
"The area of the rectangle is {} square pixels.",
area(rect1)
);
}
fn area(dimensions: (u32, u32)) -> u32 {
dimensions.0 * dimensions.1
}
在某种程度上,这个项目更好。元组允许添加一些结构体,现在只传递一个参数。但另一方面,这个版本不太清楚:元组不为它们的元素命名,所以必须索引到元组的各个部分,这使得计算不那么明显。
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!(
"The area of the rectangle is {} square pixels.",
area(&rect1)
);
}
fn area(rectangle: &Rectangle) -> u32 {
rectangle.width * rectangle.height
}
Adding Useful Functionality with Derived Traits
当我们调试程序时,如果能够打印Rectangle的实例并查看其所有字段的值,将非常有用。尝试使用println!
宏,正如我们在前几章所使用的。然而,这是行不通的。
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!("rect1 is {}", rect1);
}
error[E0277]: `Rectangle` doesn't implement `std::fmt::Display`
println!
宏可以进行多种格式设置,默认情况下,花括号表示println!
使用称为Display
的格式:用于直接供最终用户使用的输出。
println!("rect1 is {:?}", rect1);
放置说明符:?
在花括号中表示println!
我们希望使用一种名为Debug
的输出格式。Debug
特性使我们能够以一种对开发人员有用的方式打印结构体,以便在调试代码时看到它的价值。
Rust确实包含打印调试信息的功能,但必须显式地选择加入,才能使该功能对结构体可用。为此,在结构体定义之前添加外部属性#[derive(Debug)]
,
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!("rect1 is {:?}", rect1);
}
当我们有较大的结构体时,输出更容易阅读是很有用的;在这些情况下,我们可以在println!
的字符串使用{:#?}
而不是{:?}
。
使用Debug
格式打印值的另一种方法是使用dbg!
宏,它获得表达式的所有权(println!
需要引用),打印文件和dbg!宏调用与该表达式的结果值一起在代码中发生,并返回该值的所有权。
Note: Calling the
dbg!
macro prints to the standard error console stream (stderr), as opposed to println! which prints to the standard output console stream (stdout).
下面是一个例子,我们感兴趣的是分配给width字段的值,以及rect1中整个结构体的值:
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let scale = 2;
let rect1 = Rectangle {
width: dbg!(30 * scale),
height: 50,
};
dbg!(&rect1);
}
我们可以放dbg!
表达式 30 * scale
周围,因为dbg!
返回表达式值的所有权,width字段将得到与没有调用dbg!
一样的值。不需要dbg!
为了获得rect1的所有权,所以在下一个调用中使用对rect1
的引用。
除了Debug
特性之外,Rust还为我们提供了许多与derive
属性一起使用的traits
,这些属性可以向自定义类型添加有用的行为。这些特征及其行为在附录c中列出。我们将在第10章中介绍如何使用自定义行为实现这些traits
,以及如何创建自己的traits
。除了derive
,还有许多属性;更多属性参考
我们的area
函数非常具体:它只计算矩形的面积。将这种行为更紧密地绑定到我们的矩形结构体将会很有帮助,因为它不能与任何其他类型一起工作。让我们看看如何通过将area函数转换为在矩形类型中定义的area
方法来继续重构这段代码。
方法类似于函数:用fn
关键字和名称来声明它们,它们可以有参数和返回值,并且它们包含一些当从其他地方调用方法时运行的代码。与函数不同,方法是在结构体(或枚举或trait对象)的上下文中定义的,它们的第一个形参总是self
,它表示调用方法的结构体的实例。
让我们改变有一个Rectangle
实例作为参数的area
函数,取而代之的是在Rectangle
结构体上定义一个area
方法:
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!(
"The area of the rectangle is {} square pixels.",
rect1.area()
);
}
要在Rectangle
的上下文中定义函数,我们为Rectangle
启动一个impl
(实现)块。这个impl
块中的所有内容都将与Rectangle
类型关联。然后将area
函数移动到impl
花括号内,将签名中的第一个(在本例中是唯一一个)参数更改为self
。在main
中,调用area
函数并传递rect1
作为参数,可以使用方法语法来调用Rectangle
实例的area
方法。
方法语法(method syntax)在实例后面加一个点,然后跟上方法名、圆括号和任何参数
在area
的签名中,我们使用&self
而不是rectangle: &Rectangle
。&self实际上是 self: &Self
的缩写。在impl
块中,Self
类型是impl
块所针对的类型的别名。
方法的第一个参数必须有一个名为Self
的类型为self
的参数
注意,我们仍然需要在self
简写前面使用&
来表示该方法借用了self
实例,就像我们在rectangle: & rectangle
中所做的一样。
如果想要更改实例,我们将使用&mut self
作为第一个参数。
只使用self
作为第一个参数来获得实例的所有权的方法是罕见的;当方法将自身转换为其他东西,并且希望阻止调用者在转换后使用原始实例时,通常使用这种技术。
使用方法而不是函数的主要原因,除了提供方法语法和不必在每个方法的签名中重复self
的类型之外,是为了组织。我们把一个类型实例所能做的所有事情都放在了一个impl
块中,而不是让代码的未来用户在我们提供的库的各个地方搜索Rectangle的功能。
注意,我们可以选择将方法的名称与结构体的某个字段的名称相同。例如,我们可以在Rectangle
上定义一个名为width
的方法:
impl Rectangle {
fn width(&self) -> bool {
self.width > 0
}
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
if rect1.width() {
println!("The rectangle has a nonzero width; it is {}", rect1.width);
}
}
我们可以将同名方法中的字段用于任何目的。
在main
中,当我们执行rect1.width
,带括号时, Rust知道我们指的是方法width
。当我们不使用括号时,Rust知道我们指的是字段width
。
通常,但并非总是,当给出与字段同名的方法时,希望它只返回字段中的值,而不做其他任何事情。这样的方法称为getter
, Rust不像其他一些语言那样为结构体字段自动实现getter
。getter很有用,因为您可以将字段设为private
,而将方法设为public
,从而启用对该字段的只读访问,作为类型的公共API的一部分。我们将在第7章讨论什么是public
和private
,以及如何将一个字段或方法指定为public
或private
。
->
操作符在哪里?在C和c++中,调用方法使用两种不同的操作符:如果直接在对象上调用一个方法,使用 .
。如果在指向对象的指针上调用方法,并且需要先解引用该指针,使用 ->
。
Rust没有与->
相当的操作符,相反,Rust有一个称为自动引用和解引用的功能(automatic referencing and dereferencing
)。调用方法是Rust中少数具有这种行为的地方之一。它是这样工作的:当使用object.something()
调用一个方法时,Rust会自动添加&
、&mut
或*
,以便对象与方法的签名匹配。换句话说,以下是相同的:
p1.distance(&p2);
(&p1).distance(&p2);
这种自动引用行为能够工作是因为方法有一个明确的接收者——self
类型。给定接收方和方法的名称,Rust可以确定该方法是在读取reading (&self
)、改变mutating (&mut self
)还是消费consuming (self
)。Rust将方法接收者的借用隐式化的事实是实践中使所有权符合人体工程学的一个重要部分。
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
方法可以接受多个参数,将这些参数添加到签名的self
参数之后,这些参数的工作原理就像函数中的参数一样。
在impl
块中定义的所有函数都称为关联函数,因为它们与以impl
后面命名的类型相关联。
可以定义不将self
作为第一个形参(因此它们不是方法)的关联函数,因为它们不需要使用该类型的实例。已经使用过一个这样的函数:定义在String
类型上的String::from
函数。
非方法的关联函数通常用于返回结构体的新实例的构造函数。这些通常被称为new
,但new
不是一个特殊的名称,也不是语言内建的。例如,可以选择提供一个名为square
的关联函数,它将具有一个维度参数并将其用作宽度和高度,从而更容易创建一个正方形矩形,而不必两次指定相同的值:
impl Rectangle {
fn square(size: u32) -> Self {
Self {
width: size,
height: size,
}
}
}
返回类型中的Self
关键字和函数体中的Self
关键字是出现在impl
关键字之后的类型的别名,在本例中是Rectangle
。
要调用这个关联函数,我们使用::
语法和结构体名;let sq = Rectangle::square(3);
是一个例子。该函数的名称空间由结构体来划分:::
语法用于关联函数和模块创建的名称空间。我们将在第7章讨论模块。
每个结构体允许有多个impl块。以下同5.3.2
等价
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
impl Rectangle {
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
这里没有理由将这些方法分离到多个impl
块中,但这是有效的语法。将在第10章中看到多个impl
块非常有用的情况,在那里将讨论泛型类型和trait
。
结构体允许创建对域有意义的自定义类型。通过使用结构体,可以保持相关联的数据块相互连接,并为每个数据块命名,以使代码清晰。在impl
块中,可以定义与类型相关联的函数,而方法是一种相关联的函数,它允许指定结构体实例的行为。
但结构体并不是创建自定义类型的唯一方法:让我们转向Rust的enum
特性。