笔记的内容主要参考与《Rust 程序设计语言》,一些也参考了《通过例子学 Rust》和《Rust语言圣经》。
Rust学习笔记分为上中下,其它两个地址在Rust学习笔记(上)和Rust学习笔记(下)。
当执行这个宏时,程序会打印出一个错误信息,展开并清理栈数据(也可以不清理数据就退出程序),然后接着退出。
panic!
和和其他语言不一样的地方,像下面的代码,这种情况下其他像 C 这样语言会尝试直接提供所要求的值,即便这可能不是你期望的:你会得到任何对应 vector 中这个元素的内存位置的值,甚至是这些内存并不属于 vector 的情况。这被称为 缓冲区溢出(buffer overread),并可能会导致安全漏洞,比如攻击者可以像这样操作索引来读取储存在数组后面不被允许的数据。为了使程序远离这类漏洞,如果尝试读取一个索引不存在的元素,Rust 会停止执行并拒绝继续。
fn main() {
let v = vec![1, 2, 3];
v[99];
}
遇到错误 Rust 还可以使用 backtrace ,得到一个详细的错误,通过 RUST_BACKTRACE=1 cargo run
启用。
大部分错误并没有严重到需要程序完全停止执行。有时,一个函数会因为一个容易理解并做出反应的原因失败。例如,如果因为打开一个并不存在的文件而失败,此时我们可能想要创建这个文件,而不是终止进程。
enum Result<T, E> {
Ok(T),
Err(E),
}
现在你需要知道的就是 T
代表成功时返回的 Ok
成员中的数据的类型,而 E
代表失败时返回的 Err
成员中的错误的类型。
fn main() {
let f = File::open("hello.txt");
let f = match f {
Ok(file) => file,
Err(error) => {
panic!("Problem opening the file: {:?}", error)
},
};
}
// 还可以匹配不同的错误
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let f = File::open("hello.txt");
let f = match f {
Ok(file) => file,
Err(error) => match error.kind() {
ErrorKind::NotFound => match File::create("hello.txt") {
Ok(fc) => fc,
Err(e) => panic!("Problem creating the file: {:?}", e),
},
other_error => panic!("Problem opening the file: {:?}", other_error),
},
};
}
match
能够胜任它的工作,不过它可能有点冗长并且不总是能很好的表明其意图。Result
类型定义了很多辅助方法来处理各种情况。其中之一叫做 unwrap
,它的实现就类似于示例 9-4 中的 match
语句。如果 Result
值是成员 Ok
,unwrap
会返回 Ok
中的值。如果 Result
是成员 Err
,unwrap
会为我们调用 panic!
。这里是一个实践 unwrap
的例子:
use std::fs::File;
fn main() {
let f = File::open("hello.txt").unwrap();
}
还有另一个类似于 unwrap
的方法叫做 expect
,不过它允许自定义错误。
use std::fs::File;
fn main() {
let f = File::open("hello.txt").expect("Failed to open hello.txt");
}
Result
,这意味着函数返回一个 Result
类型的值,其中泛型参数 T
的具体类型是 String
,而 E
的具体类型是 io::Error
。如果这个函数没有出任何错误成功返回,函数的调用者会收到一个包含 String
的 Ok
值 。如果函数遇到任何错误,函数的调用者会收到一个 Err
值,它储存了一个包含更多这个问题相关信息的 io::Error
实例。
fn read_username_from_file() -> Result<String, io::Error> {
let f = File::open("hello.txt");
let mut f = match f {
Ok(file) => file,
Err(e) => return Err(e),
};
let mut s = String::new();
match f.read_to_string(&mut s) {
Ok(_) => Ok(s),
Err(e) => Err(e),
}
}
// 等同于上面代码,如果是ok会继续执行,Err的话结束程序
fn read_username_from_file() -> Result<String, io::Error> {
// 文章中提到,?会自动匹配你想要的返回错我类型
let mut f = File::open("hello.txt")?;
let mut s = String::new();
f.read_to_string(&mut s)?;
Ok(s)
}
// 更简洁的方式
fn read_username_from_file() -> Result<String, io::Error> {
let mut s = String::new();
File::open("hello.txt")?.read_to_string(&mut s)?;
Ok(s)
}
main
函数是特殊的,其必须返回什么类型是有限制的。main
函数的一个有效的返回值是 ()
,同时出于方便,另一个有效的返回值是 Result
,如下所示。Box
被称为 “trait 对象”(“trait object”),目前可以理解 Box
为使用 ?
时 main
允许返回的 “任何类型的错误”。
use std::error::Error;
use std::fs::File;
fn main() -> Result<(), Box<dyn Error>> {
let f = File::open("hello.txt")?;
Ok(())
}
该如何决定何时应该 panic!
以及何时应该返回 Result
呢?如果代码 panic,就没有恢复的可能。选择返回 Result
值的话,就将选择权交给了调用者,而不是代替他们做出决定。调用者可能会选择以符合他们场景的方式尝试恢复,或者也可能干脆就认为 Err
是不可恢复的,所以他们也可能会调用 panic!
并将可恢复的错误变成了不可恢复的错误。因此返回 Result
是定义可能会失败的函数的一个好的默认选择。
有一些情况 panic 比返回 Result
更为合适,不过他们并不常见。
我们可以使用泛型为像函数签名或结构体这样的项创建定义,这样它们就可以用于多种不同的具体数据类型。如下图,两个只在名称和签名中类型有所不同的函数,可以利用泛型优化它们。
fn largest_i32(list: &[i32]) -> i32 {
let mut largest = list[0];
for &item in list.iter() {
if item > largest {
largest = item;
}
}
largest
}
fn largest_char(list: &[char]) -> char {
let mut largest = list[0];
for &item in list.iter() {
if item > largest {
largest = item;
}
}
largest
}
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let result = largest_i32(&number_list);
println!("The largest number is {}", result);
let char_list = vec!['y', 'm', 'a', 'q'];
let result = largest_char(&char_list);
println!("The largest char is {}", result);
}
fn largest<T>(list: &[T]) -> T {
let mut largest = list[0];
for &item in list.iter() {
// 这里会报错
if item > largest {
largest = item;
}
}
largest
}
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let result = largest(&number_list);
println!("The largest number is {}", result);
let char_list = vec!['y', 'm', 'a', 'q'];
let result = largest(&char_list);
println!("The largest char is {}", result);
}
选择 T
是因为 Rust 的习惯是让变量名尽量短,通常就只有一个字母,同时 Rust 类型命名规范是骆驼命名法(CamelCase)。T
作为 “type” 的缩写是大部分 Rust 程序员的首选。
那里会报错是因为,注释中提到了 std::cmp::PartialOrd
,这是一个 trait,这个错误表明 largest
的函数体不能适用于 T
的所有可能的类型。因为在函数体需要比较 T
类型的值,不过它只能用于我们知道如何排序的类型。为了开启比较功能,标准库中定义的 std::cmp::PartialOrd
trait 可以实现类型的比较功能。
这个定义表明结构体 Point
对于一些类型 T
是泛型的,而且字段 x
和 y
都是相同类型的,无论它具体是何类型。
struct Point<T> {
x: T,
y: T,
}
fn main() {
let integer = Point { x: 5, y: 10 };
let float = Point { x: 1.0, y: 4.0 };
}
不同类型的
struct Point<T, U> {
x: T,
y: U,
}
fn main() {
let both_integer = Point { x: 5, y: 10 };
let both_float = Point { x: 1.0, y: 4.0 };
let integer_and_float = Point { x: 5, y: 4.0 };
}
enum Option<T> {
Some(T),
None,
}
enum Result<T, E> {
Ok(T),
Err(E),
}
在 impl
之后声明泛型 T
,这样 Rust 就知道 Point
的尖括号中的类型是泛型而不是具体类型。(不用纠结这里了,为什么 impl 后要加 T,就按它的理解)
struct Point<T> {
x: T,
y: T,
}
impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
}
fn main() {
let p = Point { x: 5, y: 10 };
println!("p.x = {}", p.x());
}
Rust 实现了泛型,使得使用泛型类型参数的代码相比使用具体类型并没有任何速度上的损失。Rust 通过在编译时进行泛型代码的单态化(monomorphization)来保证效率。单态化是一个通过填充编译时使用的具体类型,将通用代码转换为特定代码的过程。
一个类型的行为由其可供调用的方法构成。如果可以对不同类型调用相同的方法的话,这些类型就可以共享相同的行为了。trait 定义是一种将方法签名组合起来的方法,目的是定义一个实现某些目的所必需的行为的集合。
// 大写
pub trait Summary {
fn summarize(&self) -> String;
}
pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}
impl Summary for NewsArticle {
fn summarize(&self) -> String {
format!("{}, by {} ({})", self.headline, self.author, self.location)
}
}
pub struct Tweet {
pub username: String,
pub content: String,
pub reply: bool,
pub retweet: bool,
}
impl Summary for Tweet {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
let tweet = Tweet {
username: String::from("horse_ebooks"),
content: String::from("of course, as you probably already know, people"),
reply: false,
retweet: false,
};
println!("1 new tweet: {}", tweet.summarize());
如果这个 lib.rs 是对应 aggregator
crate 的,而别人想要利用我们 crate 的功能为其自己的库作用域中的结构体实现 Summary
trait。首先他们需要将 trait 引入作用域。这可以通过指定 use aggregator::Summary;
实现,这样就可以为其类型实现 Summary
trait 了。Summary
还必须是公有 trait 使得其他 crate 可以实现它,
实现 trait 时需要注意的一个限制是,只有当 trait 或者要实现 trait 的类型位于 crate 的本地作用域时,才能为该类型实现 trait。例如,可以为 aggregator
crate 的自定义类型 Tweet
实现如标准库中的 Display
trait,这是因为 Tweet
类型位于 aggregator
crate 本地的作用域中。类似地,也可以在 aggregator
crate 中为 Vec
实现 Summary
,这是因为 Summary
trait 位于 aggregator
crate 本地作用域中。
但是不能为外部类型实现外部 trait。例如,不能在 aggregator
crate 中为 Vec
实现 Display
trait。这是因为 Display
和 Vec
都定义于标准库中,它们并不位于 aggregator
crate 本地作用域中。这个限制是被称为 相干性(coherence) 的程序属性的一部分,或者更具体的说是 孤儿规则(orphan rule),其得名于不存在父类型。这条规则确保了其他人编写的代码不会破坏你代码,反之亦然。没有这条规则的话,两个 crate 可以分别对相同类型实现相同的 trait,而 Rust 将无从得知应该使用哪一个实现。
(这段代码没有违反孤儿规则(orphan rule),因为至少有一方(trait 或类型)是在本地 crate 中定义的。孤儿规则防止你为不在你的 crate 中定义的类型实现不在你的 crate 中定义的 trait。在这个例子中,CommandError
是在你的 crate 中定义的,而fmt::Display
trait 是标准库提供的。这样的实现是被允许的,因为它满足了孤儿规则的条件之一:要实现的类型(CommandError
)是本地定义的。)
pub trait Summary {
fn summarize(&self) -> String {
String::from("(Read more...)")
}
// 可以有多个
fn summarize(&self) -> String {
format!("(Read more from {}...)", self.summarize_author())
}
}
pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}
// 指定一个空的impl,也可只实现个别trait
impl Summary for NewsArticle {}
fn main() {
let article = NewsArticle {
headline: String::from("Penguins win the Stanley Cup Championship!"),
location: String::from("Pittsburgh, PA, USA"),
author: String::from("Iceburgh"),
content: String::from("The Pittsburgh Penguins once again are the best
hockey team in the NHL."),
};
println!("New article available! {}", article.summarize());
}
可以将传递 NewsArticle
或 Tweet
的实例来调用 notify
。
外面可以直接用,为什么要套个函数?方便代码重用,这样限制了只有实现trait的类才能用。
pub fn notify(item: impl Summary) {
println!("Breaking news! {}", item.summarize());
}
上面的代码可以变成这样。
pub fn notify<T: Summary>(item: T) {
println!("Breaking news! {}", item.summarize());
}
pub fn notify(item1: impl Summary, item2: impl Summary) {
pub fn notify<T: Summary>(item1: T, item2: T) {
pub fn notify(item: impl Summary + Display) {
pub fn notify<T: Summary + Display>(item: T) {
fn some_function<T: Display + Clone, U: Clone + Debug>(t: T, u: U) -> i32 {
fn some_function<T, U>(t: T, u: U) -> i32
where T: Display + Clone,
U: Clone + Debug
{
通过使用 impl Summary
作为返回值类型,在不确定其具体的类型的情况下。
fn returns_summarizable() -> impl Summary {
Tweet {
username: String::from("horse_ebooks"),
content: String::from("of course, as you probably already know, people"),
reply: false,
retweet: false,
}
}
// 这样无法运行,不能返回两种
fn returns_summarizable(switch: bool) -> impl Summary {
if switch {
NewsArticle {
headline: String::from("Penguins win the Stanley Cup Championship!"),
location: String::from("Pittsburgh, PA, USA"),
author: String::from("Iceburgh"),
content: String::from("The Pittsburgh Penguins once again are the best
hockey team in the NHL."),
}
} else {
Tweet {
username: String::from("horse_ebooks"),
content: String::from("of course, as you probably already know, people"),
reply: false,
retweet: false,
}
}
}
// 过滤掉没有PartialOrd和Copy trait的T
fn largest<T: PartialOrd + Copy>(list: &[T]) -> T {
let mut largest = list[0];
for &item in list.iter() {
if item > largest {
largest = item;
}
}
largest
}
更高级的那 impl 控制
use std::fmt::Display;
struct Pair<T> {
x: T,
y: T,
}
impl<T> Pair<T> {
fn new(x: T, y: T) -> Self {
Self {
x,
y,
}
}
}
impl<T: Display + PartialOrd> Pair<T> {
fn cmp_display(&self) {
if self.x >= self.y {
println!("The largest member is x = {}", self.x);
} else {
println!("The largest member is y = {}", self.y);
}
}
}
也可以对任何实现了特定 trait 的类型有条件地实现 trait。对任何满足特定 trait bound 的类型实现 trait 被称为 blanket implementations,他们被广泛的用于 Rust 标准库中。例如,标准库为任何实现了 Display
trait 的类型实现了 ToString
trait。这个 impl
块看起来像这样:
impl<T: Display> ToString for T {
// --snip--
}
生命周期避免了悬垂引用(指向已经被释放或无效内存的引用)。
下图为借用检查器
{
let r; // ---------+-- 'a
// |
{ // |
let x = 5; // -+-- 'b |
r = &x; // | |
} // -+ |
// |
println!("r: {}", r); // |
} // ---------+
// 正确的例子
#![allow(unused_variables)]
{
let x = 5; // ----------+-- 'b
// |
let r = &x; // --+-- 'a |
// | |
println!("r: {}", r); // | |
// --+ |
} // ----------+
下面这个会报错,因为编译器不知道到底返回的是x还是y,也就是无法确定生命周期。
fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";
let result = longest(string1.as_str(), string2);
println!("The longest string is {}", result);
}
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() {
x
} else {
y
}
}
借用检查器自身同样也无法确定,因为它不知道 x
和 y
的生命周期是如何与返回值的生命周期相关联的。
要解决这个问题需要用到生命周期注解,生命周期注解并不改变任何引用的生命周期的长短,它用于描述多个引用生命周期相互的关系。
// 这里我们想要告诉Rust关于参数中的引用和返回值之间的限制是他们都必须拥有相同的生命周期
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
// 就算是这样也必须加注解,不过可以只加一个
fn longest<'a>(x: &'a str, y: &str) -> &'a str {
x
}
当具体的引用被传递给 longest
时,被 'a
所替代的具体生命周期是 x
的作用域与 y
的作用域相重叠的那一部分。换一种说法就是泛型生命周期 'a
的具体生命周期等同于 x
和 y
的生命周期中较小的那一个。因为我们用相同的生命周期参数 'a
标注了返回的引用值,所以返回的引用值就能保证在 x
和 y
中较短的那个生命周期结束之前保持有效。
另一个问题,当从函数返回一个引用,返回值的生命周期参数需要与一个参数的生命周期参数相匹配。如果返回的引用 没有 指向任何一个参数,那么唯一的可能就是它指向一个函数内部创建的值,它将会是一个悬垂引用,因为它将会在函数结束时离开作用域。像下面的代码会报错。
这是因为返回值的生命周期与参数完全没有关联。 result
在 longest
函数的结尾将离开作用域并被清理,而我们尝试从函数返回一个 result
的引用。解决方案是返回一个有所有权的数据类型而不是一个引用,这样函数调用者就需要负责清理这个值了。
fn longest<'a>(x: &str, y: &str) -> &'a str {
let result = String::from("really long string");
result.as_str()
}
比如下面的结构体,用到了&str,就需要加声明周期。为什么?因为&str是一个引用,也就是这个struct拿不到它的所有权,所以很有肯能,在这个struct使用的过程中,&str失效了,造成错误。所有必须让这个结构体的生命周期和&str一样。
struct ImportantExcerpt<'a> {
part: &'a str,
}
编译器采用三条规则来判断引用何时不需要明确的注解。
第一条,每一个是引用的参数都有它自己的生命周期参数。
第二条,如果只有一个输入生命周期参数,那么它被赋予所有输出生命周期参数。
第三条,如果方法有多个输入生命周期参数并且其中一个参数是 &self 或 &mut self,说明是个对象的方法, 那么所有输出生命周期参数被赋予 self 的生命周期。为什么这么规定呢?我觉得记住就行,就是这么设计的,那难道声明周期高于这个对象吗?
fn first_word(s: &str) -> &str
// 根据第一条规则变为
fn first_word<'a>(s: &'a str) -> &str
// 根据第二条规则变为
fn first_word<'a>(s: &'a str) -> &'a str
// 根据第一条规则变为
fn longest(x: &str, y: &str) -> &str
// 根据第一条规则变为
fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str
// 根据第二条规则不成立
struct Book<'a> {
name: &'a str,
}
impl<'a> Book<'a> {
// 更具第三规则省略声明周期的标注
fn get_prefix(&self) -> &str {
&self.name[..3]
}
}
(这块我有些困惑,暂时不思考了,有机会读一读关于String和&str的文章)
'static
,其生命周期能够存活于整个程序期间。所有的字符串字面值都拥有 'static
生命周期。这是因为字符串的文本被直接储存在程序的二进制文件中而这个文件总是可用的,因此所有的字符串字面值都是 'static
的。
let s: &'static str = "I have a static lifetime.";
// 这两句是等效的
let s: &str = "I have a static lifetime.";
为什么?既然所有的字符串字面值都拥有 'static
生命周期,那么下面的代码为什么报错?
fn main() {
{
let s: &'static str = "hello world";
}
println!("s={}", s);
}
这里 str 的 lifetime 确实是 'static
,但是它被 scope 所限制,也就是“变小”了。
还有个疑问,既然 &str 默认'static
,呢么这里为什么编译不通过呢?为什么必须加 'static
。
fn get_static_message() -> &str {
"Hello, I have a static lifetime."
}
具体原因不懂,我觉得可能是 'static
是一个上限,是可能被别的值所影响的,所以还是得标出来。
至于为什么 &str 都是 'static
,因为它直接存储在二进制文件内,而不是在运行时动态地存储在堆或栈上。那为什么直接存储在二进制文件内?因为这样可以减少运行时的内存使用,而且字符串字面值是不变的等一些原因吧。
另外,需要注意的,只有引用有声明周期,像下面的例子,都是直接把值 copy 出去,不存在什么声明周期。
fn main() {
let r;
{
let x = 5;
r = x;
}
println!("r: {}", r);
}
fn get_static_message() -> i32 {
1
}
fn longest_with_an_announcement<'a, T>(x: &'a str, y: &'a str, ann: T) -> &'a str
where T: Display
{
println!("Announcement! {}", ann);
if x.len() > y.len() {
x
} else {
y
}
}
闭包(closures)是可以保存进变量或作为参数传递给其他函数的匿名函数,并且可以使用在其所在的作用域的值
// simulated_expensive_calculation(intensity)这个函数是非常耗时的,在下面三个地方出现
// 我们其实只需要它的运行结果,所以第一种方式就是把它提出来,赋给变量
// 但这样带来一个问题是有些地方并不执行,比如else的if,也必须执行
// 所以第二种方式就是用闭包,它只在调用它时运行
// 但是使用了这种,在第一个if里它仍然会执行两次
// 解决办法为可以搞个变量在第一个if里接一下值(那直接在函数呢个办法里,在第一个if接一下不就行了?呵呵,例子不好),另一种用srtuct(后面会写)
fn generate_workout(intensity: u32, random_number: u32) {
let expensive_closure = |num| {
println!("calculating slowly...");
thread::sleep(Duration::from_secs(2));
num
};
if intensity < 25 {
println!(
"Today, do {} pushups!",
// simulated_expensive_calculation(intensity)
expensive_closure(intensity)
);
println!(
"Next, do {} situps!",
// simulated_expensive_calculation(intensity)
expensive_closure(intensity)
);
} else {
if random_number == 3 {
println!("Take a break today! Remember to stay hydrated!");
} else {
println!(
"Today, run for {} minutes!",
// // simulated_expensive_calculation(intensity)
expensive_closure(intensity)
);
}
}
}
闭包不要求像 fn 函数那样在参数和返回值上注明类型。函数中需要类型注解是因为他们是暴露给用户的显式接口的一部分,如果不定义用户无法使用。但是闭包并不用于这样暴露给外面,只供自己使用。当然,你也可以标出来。
像下面,会报错,这是因为每个闭包都有自己的唯一类型,不能像下面呢样。
let example_closure = |x| x;
let s = example_closure(String::from("hello"));
let n = example_closure(5);
可以创建一个存放闭包和调用闭包结果的结构体。该结构体只会在需要结果时执行闭包,并会缓存结果值,这样余下的代码就不必再负责保存结果并可以复用该值。这种模式被称 memoization 或 lazy evaluation。
struct Cacher<T>
where T: Fn(u32) -> u32
{
calculation: T,
value: Option<u32>,
}
impl<T> Cacher<T>
where T: Fn(u32) -> u32
{
fn new(calculation: T) -> Cacher<T> {
Cacher {
// 省略的写法
calculation,
value: None,
}
}
fn value(&mut self, arg: u32) -> u32 {
match self.value {
// 当然这里的逻辑,当arg改变时,还是返回原来的v。可以自行设定逻辑
Some(v) => v,
None => {
let v = (self.calculation)(arg);
self.value = Some(v);
v
},
}
}
}
上面的目标将变为:
fn generate_workout(intensity: u32, random_number: u32) {
let mut expensive_result = Cacher::new(|num| {
println!("calculating slowly...");
thread::sleep(Duration::from_secs(2));
num
});
if intensity < 25 {
println!(
"Today, do {} pushups!",
expensive_result.value(intensity)
);
println!(
"Next, do {} situps!",
expensive_result.value(intensity)
);
} else {
if random_number == 3 {
println!("Take a break today! Remember to stay hydrated!");
} else {
println!(
"Today, run for {} minutes!",
expensive_result.value(intensity)
);
}
}
}
像这样,可以直接拿到 x 的值。
fn main() {
let x = 4;
let equal_to_x = |z| z == x;
let y = 4;
assert!(equal_to_x(y));
}
闭包可以通过三种方式捕获其环境,他们直接对应函数的三种获取参数的方式:获取所有权,可变借用和不可变借用。这三种捕获值的方式被编码为如下三个 Fn
trait:
FnOnce
消费从周围作用域捕获的变量,闭包周围的作用域被称为其环境,environment。为了消费捕获到的变量,闭包必须获取其所有权并在定义闭包时将其移动进闭包。其名称的 Once
部分代表了闭包不能多次获取相同变量的所有权的事实,所以它只能被调用一次。FnMut
获取可变的借用值所以可以改变其环境Fn
从其环境获取不可变的借用值这个要在where里写,如果只是在函数里用,Rust会自动判断。
另外也可以用 move 将所有权移到闭包里。
let equal_to_x = move |z| z == x;
迭代器(iterator)负责遍历序列中的每一项,像下面 iter()
会返回一个迭代器,然后遍历。直接遍历也行,因为 for 循环会帮你调用迭代器 。
let v1 = vec![1, 2, 3];
let v1_iter = v1.iter();
for val in v1_iter {
println!("Got: {}", val);
}
迭代器会实现了一个叫做 Iterator
的定义于标准库的 trait,里面都会有一个 next
方法。比如下面这样,要注意的是声明的迭代器需要为 mut,在迭代器上调用 next
方法改变了迭代器中用来记录序列位置的状态。
#[test]
fn iterator_demonstration() {
let v1 = vec![1, 2, 3];
let mut v1_iter = v1.iter();
assert_eq!(v1_iter.next(), Some(&1));
assert_eq!(v1_iter.next(), Some(&2));
assert_eq!(v1_iter.next(), Some(&3));
assert_eq!(v1_iter.next(), None);
}
另外iter
方法生成一个不可变引用的迭代器。如果我们需要一个获取 v1
所有权并返回拥有所有权的迭代器,则可以调用 into_iter
。类似的,如果我们希望迭代可变引用,则可以调用 iter_mut
。
这些调用 next
方法的方法被称为 消费适配器(consuming adaptors),因为调用他们会消耗迭代器。一个消费适配器的例子是 sum
方法。这个方法获取迭代器的所有权并反复调用 next
来遍历迭代器,因而会消费迭代器。调用 sum
之后不再允许使用 v1_iter
因为调用 sum
时它会获取迭代器的所有权。
fn iterator_sum() {
let v1 = vec![1, 2, 3];
let v1_iter = v1.iter();
let total: i32 = v1_iter.sum();
assert_eq!(total, 6);
}
迭代器适配器(iterator adaptors),他们允许我们将当前迭代器变为不同类型的迭代器,还可以链式调用多个迭代器适配器。
map:将迭代器中的每个元素转换为另一种形式或值。
let v1: Vec<i32> = vec![1, 2, 3];
let v2: Vec<_> = v1.iter().map(|x| x + 1).collect();
filter:用于从迭代器中筛选出满足某个条件的元素。
shoes.into_iter().filter(|s| s.size == shoe_size).collect()
大概就是下面这样,重点就是实现 next 方法。
struct Counter {
count: u32,
}
impl Counter {
fn new() -> Counter {
Counter { count: 0 }
}
}
impl Iterator for Counter {
type Item = u32;
// 只会从 1 数到 5 的迭代器
fn next(&mut self) -> Option<Self::Item> {
self.count += 1;
if self.count < 6 {
Some(self.count)
} else {
None
}
}
}
fn calling_next_directly() {
let mut counter = Counter::new();
assert_eq!(counter.next(), Some(1));
assert_eq!(counter.next(), Some(2));
assert_eq!(counter.next(), Some(3));
assert_eq!(counter.next(), Some(4));
assert_eq!(counter.next(), Some(5));
assert_eq!(counter.next(), None);
}
智能指针是一类数据结构,他们的表现类似指针,但是也拥有额外的元数据和功能。而且引用是一类只借用数据的指针;相反,在大部分情况下,智能指针拥有他们指向的数据,比如String,Vec。智能指针通常使用结构体实现,区别于常规结构体的显著特性在于其实现了 Deref
和 Drop
trait。Deref
trait 允许智能指针结构体实例表现的像引用一样,这样就可以编写既用于引用、又用于智能指针的代码。Drop
trait 允许我们自定义当智能指针离开作用域时运行的代码。
Box
允许你将一个值放在堆上而不是栈上,留在栈上的则是指向堆数据的指针。除了数据被储存在堆上而不是栈上之外,box 没有性能损失。不过也没有很多额外的功能。用于像是编译时未知大小,而又想要在需要确切大小的上下文中使用这个类型值的时候;当有大量数据并希望在确保数据不被拷贝的情况下转移所有权的时候(普通呢些i32都实现了copy trait);当希望拥有一个值并只关心它的类型是否实现了特定 trait 而不是其具体类型的时候。
它的一个应用像是可以存储递归。如果正常写的话Rust会因为不知道这个变量的大小而报错。对于 Box
,因为它是一个指针,我们总是知道它需要多少空间,指针的大小并不会根据其指向的数据量而改变。意味着不同于直接储存一个值,我们将间接的储存一个指向值的指针。
enum List {
Cons(i32, Box<List>),
Nil,
}
use crate::List::{Cons, Nil};
fn main() {
let list = Cons(1,
Box::new(Cons(2,
Box::new(Cons(3,
Box::new(Nil))))));
}
Box
类型是一个智能指针,因为它实现了 Deref
trait,它允许 Box
值被当作引用对待。当 Box
值离开作用域时,由于 Box
类型 Drop
trait 的实现,box 所指向的堆数据也会被清除。
以下是一个简单的解引用:
fn main() {
let x = 5;
let y = &x;
assert_eq!(5, x);
assert_eq!(5, *y);
}
fn main() {
let x = 5;
let y = Box::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);
}
实现 Deref
trait 允许我们重载 解引用运算符(dereference operator)*
(与乘法运算符或通配符相区别)。通过这种方式实现 Deref
trait 的智能指针可以被当作常规引用来对待,可以编写操作引用的代码并用于智能指针。
use std::ops::Deref;
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
// 为什么这么写,不用想呢么多了
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
实现之后,像下面解引用代码就可以运行了,实际输入*y
,运行逻辑为*(y.deref())
(外面还有个*是因为deref里是&self.0)
fn main() {
let x = 5;
let y = MyBox::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);
}
它是 Rust 在函数或方法传参上的一种便利。其将实现了 Deref
的类型的引用转换为原始类型通过 Deref
所能够转换的类型的引用。当这种特定类型的引用作为实参传递给和形参类型不同的函数或方法时,解引用强制多态将自动发生。这时会有一系列的 deref
方法被调用,把我们提供的类型转换成了参数所需的类型。
比如下面这个例子,MyBox
上实现了 Deref
trait,Rust 可以通过 deref
调用将 &MyBox
变为 &String
。标准库中提供了 String
上的 Deref
实现,其会返回字符串 slice,这可以在 Deref
的 API 文档中看到。Rust 再次调用 deref
将 &String
变为 &str
,这就符合 hello
函数的定义了。
解引用强制多态(deref coercions)的加入使得 Rust 程序员编写函数和方法调用时无需增加过多显式使用 &
和 *
的引用和解引用。
fn hello(name: &str) {
println!("Hello, {}!", name);
}
fn main() {
let m = MyBox::new(String::from("Rust"));
hello(&m);
// 如果没有deref coercions
hello(&(*m)[..]);
}
如果是要处理可变引用,会用到DerefMut,具体不说了。
其允许我们在值要离开作用域时执行一些代码,像是这样
struct CustomSmartPointer {
data: String,
}
impl Drop for CustomSmartPointer {
fn drop(&mut self) {
println!("Dropping CustomSmartPointer with data `{}`!", self.data);
}
}
然而,有时你可能需要提早清理某个值。一个例子是当使用智能指针管理锁时,你可能希望强制运行 drop
方法来释放锁以便作用域中的其他代码可以获取锁。Rust 并不允许我们主动调用 Drop
trait 的 drop
方法;当我们希望在作用域结束之前就强制释放变量的话,我们应该使用的是由标准库提供的 std::mem::drop
。类似这样:
let c = CustomSmartPointer { data: String::from("some data") };
drop(c);
Rust 有一个叫做 Rc
的类型。其名称为 引用计数(reference counting)的缩写。引用计数意味着记录一个值引用的数量来知晓这个值是否仍在被使用。如果某个值有零个引用,就代表没有任何有效引用并可以被清理。Rc
只能用于单线程场景;第十六章并发会涉及到如何在多线程程序中进行引用计数。
Rc
用于当我们希望在堆上分配一些内存供程序的多个部分读取,而且无法在编译时确定程序的哪一部分会最后结束使用它的时候。
如何使用它共享数据呢,想要b和c共享5、10是难以完成的,看下面的代码
enum List {
Cons(i32, Box<List>),
Nil,
}
use crate::List::{Cons, Nil};
fn main() {
let a = Cons(5,
Box::new(Cons(10,
Box::new(Nil))));
let b = Cons(3, Box::new(a));
let c = Cons(4, Box::new(a));
}
可以改变 Cons
的定义来存放一个引用,不过接着必须指定生命周期参数。通过指定生命周期参数,表明列表中的每一个元素都至少与列表本身存在的一样久。
也可以修改 List
的定义为使用 Rc
代替 Box
。当创建 b
时,不同于获取 a
的所有权,这里会克隆 a
所包含的 Rc
,这会将引用计数从 1 增加到 2 并允许 a
和 b
共享 Rc
中数据的所有权。创建 c
时也会克隆 a
,这会将引用计数从 2 增加为 3。每次调用 Rc::clone
,Rc
中数据的引用计数都会增加,直到有零个引用之前其数据都不会被清理。当 c
离开作用域时,计数减1。
enum List {
Cons(i32, Rc<List>),
Nil,
}
use crate::List::{Cons, Nil};
use std::rc::Rc;
fn main() {
let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
let b = Cons(3, Rc::clone(&a));
let c = Cons(4, Rc::clone(&a));
}
当然也可以使用 a.clone()
而不是 Rc::clone(&a)
,Rc::clone
的实现并不像大部分类型的 clone
实现那样对所有数据进行深拷贝。Rc::clone
只会增加引用计数,这并不会花费多少时间。深拷贝可能会花费很长时间。
查看数量是可以调用Rc::strong_count(&a)
获得
内部可变性(Interior mutability)允许你即使在有不可变引用时也可以改变数据,这通常是借用规则所不允许的。为了改变数据,该模式在数据结构中使用 unsafe
代码来模糊 Rust 通常的可变性和借用规则。
为什么用它呢(用到再说吧)?因为一些分析是不可能的,如果 Rust 编译器不能通过所有权规则编译,它可能会拒绝一个正确的程序;从这种角度考虑它是保守的。RefCell
正是用于当你确信代码遵守借用规则,而编译器不能理解和确定的时候。类似于 Rc
,RefCell
只能用于单线程场景。在需要绕过Rust静态借用规则(编译时借用检查)的情况,允许在运行时进行动态借用检查。这样的设计允许在特定条件下安全地进行内部可变性
Rc
允许相同数据有多个所有者;Box
和 RefCell
有单一所有者。Box
允许在编译时执行不可变或可变借用检查;Rc
仅允许在编译时执行不可变借用检查;RefCell
允许在运行时执行不可变或可变借用检查。RefCell
允许在运行时执行可变借用检查,所以我们可以在即便 RefCell
自身是不可变的情况下修改其内部的值。#[derive(Debug)]
enum List {
Cons(Rc<RefCell<i32>>, Rc<List>),
Nil,
}
use crate::List::{Cons, Nil};
use std::rc::Rc;
use std::cell::RefCell;
fn main() {
let value = Rc::new(RefCell::new(5));
let a = Rc::new(Cons(Rc::clone(&value), Rc::new(Nil)));
let b = Cons(Rc::new(RefCell::new(6)), Rc::clone(&a));
let c = Cons(Rc::new(RefCell::new(10)), Rc::clone(&a));
*value.borrow_mut() += 10;
println!("a after = {:?}", a);
println!("b after = {:?}", b);
println!("c after = {:?}", c);
}
可以理解为,比如a依赖b,b依赖a,在作用域结束时,Rust要先确定删谁,这种情况,谁都删不了。
可以用Rc::downgrade,代替Rc::clone。后者strong_count不为0就不能清理,前者weak_count不为0也能清理