速成课第6节

到目前为止,我们已经介绍了生命周期和值序列话的一些细节。 现在是时候深入研究并正确了解生命周期和切片了。

这篇文章是基于 FP 完成 Rust 教学系列的一部分。 如果你在博客之外阅读这篇文章,你可以在介绍文章的顶部找到这个系列中所有文章的链接。 也可订阅 RSS 频道。

打印 person

让我们来看看一些相当平常的代码:

#[derive(Debug)]
struct Person {
    name: Option<String>,
    age: Option<u32>,
}

fn print_person(person: Person) {
    match person.name {
        Some(name) => println!("Name is {}", name),
        None => println!("No name provided"),
    }

    match person.age {
        Some(age) => println!("Age is {}", age),
        None => println!("No age provided"),
    }
}

fn main() {
    print_person(Person {
        name: Some(String::from("Alice")),
        age: Some(30),
    });
}

相当简单,这是一个模式匹配的很好的示范。 但是,让我们再加上一行。 尝试在 print_person 函数的开头添加以下内容:

println!("Full Person value: {:?}", person);

一切正常。 先打印 person 的全部内容,然后是模式匹配。 但是尝试在函数末尾添加上这行,你会得到一个编译错误:

error[E0382]: use of partially moved value: `person`
  --> main.rs:18:41
   |
9  |         Some(name) => println!("Name is {}", name),
   |              ---- value moved here
...
18 |     println!("Full Person value: {:?}", person);
   |                                         ^^^^^^ value used here after move
   |
   = note: move occurs because the value has type `std::string::String`, which does not implement the `Copy` trait

error: aborting due to previous error

注意:这是我使用的Rust编译器1.30.1的错误。 但是,已经有计划来改善这种情况。

问题在于我们已经消耗了 person 的一部分,因此无法打印它。 我们可以通过再次设置来解决此问题。 让 person 参数可变,然后使用默认的None 值填充移动的 person.name:

fn print_person(mut person: Person) {
    match person.name {
        Some(name) => println!("Name is {}", name),
        None => println!("No name provided"),
    }

    match person.age {
        Some(age) => println!("Age is {}", age),
        None => println!("No age provided"),
    }

    person.name = None;

    println!("Full Person value: {:?}", person);
}

编译运行,但现在输出结果令人困惑:

Name is Alice
Age is 30
Full Person value: Person { name: None, age: Some(30) }

注意最后一行中name的值是None,理想情况下应该是 Some (Alice)。 我们可以做得更好,通过返回 match 中的 name:

person.name = match person.name {
    Some(name) => {
        println!("Name is {}", name);
        Some(name)
    },
    None => {
        println!("No name provided");
        None
    }
};

但是这样做是不雅的。 让我们后退一步。 我们真的需要 消费/move person.name 吗? 也不尽然。 它应该按照引用来做所有的事情。 因此,让我们回过头来,通过使用一个借词来完全避免这种移动:

match &person.name {
    Some(name) => println!("Name is {}", name),
    None => println!("No name provided"),
}

好多了! 不过我们不需要在 person.age 上引用,因为 u32 是可复制的。 在这里,我们对一个引用进行了模式匹配分析,因此这个 name 也是一个引用。

但是,我们可以使用 ref 关键字更明确地说明这一点。 这个关键词表明,当模式匹配时,我们希望模式是一个引用,而不是原始值的移动。 (更多请参考 《in the Rust book》) 。我们最终得到的是:

match person.name {
    Some(ref name) => println!("Name is {}", name),
    None => println!("No name provided"),
}

在我们的例子中,这与 &person.name 的基本结果相同。

Birthday!

让我们修改代码,以便在打印 age 时也将 age 增加1。首先是下面的问题,代码不能编译通过,请尝试预测原因:

match person.age {
    Some(age) => {
        println!("Age is {}", age);
        age += 1;
    }
    None => println!("No age provided"),
}

我们正在尝试更改局部 age 的绑定,但这是不可变的。 好吧,这很容易修复,只需将Some(age)替换为 Some(mut age)。 可以编译,但带有警告:

warning: value assigned to `age` is never read
  --> src/main.rs:16:13
   |
16 |             age += 1;
   |             ^^^
   |
   = note: #[warn(unused_assignments)] on by default

然后输出是:

Name is Alice
Age is 30
Full Person value: Person { name: Some("Alice"), age: Some(30) }

注意最后一行,age 仍然是30岁,而不是31岁。 花一分钟时间试着理解这里发生了什么... 完成了吗? 酷。

  1. 根据 person.age 模式匹配

  2. 如果是Some,我们需要将 age 移动到局部 age

  3. 但是由于类型是 u32,它将创建一个副本并移动该副本

  4. 当我们递增 age 时,我们递增了一个从未使用过的副本

我们可以通过引用 person.age 来解决这个问题:

fn print_person(person: Person) {
    match person.name {
        Some(ref name) => println!("Name is {}", name),
        None => println!("No name provided"),
    }

    match &mut person.age {
        Some(age) => {
            println!("Age is {}", age);
            age += 1;
        }
        None => println!("No age provided"),
    }

    println!("Full Person value: {:?}", person);
}

编译器抱怨:age是一个 &mut u32,但是我们试图在其上使用 += :

error[E0368]: binary assignment operation `+=` cannot be applied to type `&mut u32`
  --> src/main.rs:16:13
   |
16 |             age += 1;
   |             ---^^^^^
   |             |
   |             cannot use `+=` on type `&mut u32`
   |
   = help: `+=` can be used on 'u32', you can dereference `age`: `*age`

根据编译器的踏实,我们需要采用取消年龄引用。 但又出现一个错误:

error[E0596]: cannot borrow field `person.age` of immutable binding as mutable
  --> src/main.rs:13:16
   |
7  | fn print_person(person: Person) {
   |                 ------ consider changing this to `mut person`
...
13 |     match &mut person.age {
   |                ^^^^^^^^^^ cannot mutably borrow field of immutable binding

error: aborting due to previous error

同样,编译器准确地告诉我们如何解决这个问题: 使 person 可变。 继续,做出改变,一切都会顺利进行。

练习1

对于 person.name,我们提出了两种解决方案: 借用 person.name 或使用 ref 关键字。 同样的两种解决方案也适用于我们当前的问题。 我们刚刚演示了借用方法。 尝试使用 ref 关键字来解决这个问题。

单一迭代器

让我们做一个傻乎乎的小迭代器,它只产生一个值。 我们将通过使用 Option 来追踪是否产生了值:

struct Single<T> {
    next: Option<T>,
}

让我们创建一个辅助函数来创建单个值:

fn single<T>(t: T) -> Single<T> {
    Single {
        next: Some(t),
    }
}

让我们编写一个 main 函数来测试这个函数是否正常工作:

fn main() {
    let actual: Vec<u32> = single(42).collect();
    assert_eq!(vec![42], actual);
}

如果你试图编译它,你会得到一个错误消息:

error[E0599]: no method named `collect` found for type `Single<{integer}>` in the current scope
  --> src/main.rs:12:39
   |
1  | struct Single<T> {
   | ---------------- method `collect` not found for this
...
12 |     let actual: Vec<u32> = single(42).collect();
   |                                       ^^^^^^^
   |
   = note: the method `collect` exists but the following trait bounds were not satisfied:
           `&mut Single<{integer}> : std::iter::Iterator`
   = help: items from traits can only be used if the trait is implemented and in scope
   = note: the following trait defines an item `collect`, perhaps you need to implement it:
           candidate #1: `std::iter::Iterator`

为了使用collect(),我们需要提供一个 Iterator 实现。 Item 将会是 T。我们已经有了一个很好的 Option ,可以用于 next() 函数的返回值:

impl<T> Iterator for Single<T> {
    type Item = T;

    fn next(&mut self) -> Option<T> {
        self.next
    }
}

不幸的是,这并不奏效:

error[E0507]: cannot move out of borrowed content
  --> src/main.rs:21:9
   |
21 |         self.next
   |         ^^^^ cannot move out of borrowed content

error: aborting due to previous error

哦,对。 我们不能将结果值移出,因为我们的 next 函数只能可变借用 self。 让我们尝试一些模式匹配:

match self.next {
    Some(next) => Some(next),
    None => None,
}

除此之外,这还涉及从引用中移动,因此失败。 让我们再试一次:我们将进入一个局部变量,将 self.next 设置为 None(这样就不会再次重复该值),然后返回该局部变量:

fn next(&mut self) -> Option<T> {
    let res = self.next;
    self.next = None;
    res
}

不,编译器仍然不满意! 我想我们只需要放弃对单一迭代器的宏伟愿景。 我们当然可以作弊:

fn next(&mut self) -> Option<T> {
    None
}

但是,尽管编译完成,但它在运行时未能通过我们的测试:

thread 'main' panicked at 'assertion failed: `(left == right)`
  left: `[42]`,
 right: `[]`', src/main.rs:13:5
note: Run with `RUST_BACKTRACE=1` for a backtrace.

Swap

上面我们所做的是尝试将 self.next 与局部变量交换。 但是,借用检查员并不喜欢我们采用的方法。 但是,标准库有一个辅助函数std::mem::swap,该函数可以为我们提供帮助。 看起来像:

pub fn swap<T>(x: &mut T, y: &mut T)

毫无疑问,我们可以用它来解决我们的问题:

练习2

上面的代码不能完全编译,但是编译器可以指导您找到正确的解决方案。 试着找出上面的问题并自己解决它们。 如果做不到这一点,请求编译器帮助你。

replace 和 take

你是否觉得创建临时变量这个东西有点冗长? 是的,对于 Rust 标准库的作者来说也是如此。 有一个 helper 函数绕过了这个临时变量:

fn next(&mut self) -> Option<T> {
    std::mem::replace(&mut self.next, None)
}

好多了! 但这应该是很容易的事情,这似乎仍然需要更多工作。幸运的是,对于 Rust 标准库的作者来说也是如此。 使用 None 替换 Option 中的值,然后使用原始值,这种模式非常常见,以至于他们给了它一个名称和一个方法: take。

fn next(&mut self) -> Option<T> {
    self.next.take()
}

我们完成了!

生命周期

我们在前面的课程中已经简要地提到了生命,但是现在是时候更加认真地对待它们了。 让我们来看看引用的一个简单用法:

struct Person {
    name: String,
    age: u32,
}

fn get_name(person: &Person) -> String {
    person.name
}

fn main() {
    let alice = Person {
        name: String::from("Alice"),
        age: 30,
    };
    let name = get_name(&alice);
    println!("Name: {}", name);

这段代码不能编译。 我们的 get_name 函数接受一个 Person 的引用,然后尝试在其结果中移动这个 Person 的 name。 这不可能。 一个解决方案是克隆这个name:

fn get_name(person: &Person) -> String {
    person.name.clone()
}

虽然这样做是有效的,但是效率相对较低。 我们喜欢尽量避免复制。 相反,让我们可以简单地返回一个名称的引用:

fn get_name(person: &Person) -> &String {
    &person.name
}

万岁! 但是让我们把函数变得更复杂一点。 现在我们需要一个函数,它将接受两个 Person,并返回 age 较大的 Person 的 name。 这听起来相当容易写:

struct Person {
    name: String,
    age: u32,
}

fn get_older_name(person1: &Person, person2: &Person) -> &String {
    if person1.age >= person2.age {
        &person1.name
    } else {
        &person2.name
    }
}

fn main() {
    let alice = Person {
        name: String::from("Alice"),
        age: 30,
    };
    let bob = Person {
        name: String::from("Bob"),
        age: 35,
    };
    let name = get_older_name(&alice, &bob);
    println!("Older person: {}", name);
}

不幸的是,编译器非常生我们的气:

error[E0106]: missing lifetime specifier
 --> src/main.rs:6:58
  |
6 | fn get_older_name(person1: &Person, person2: &Person) -> &String {
  |                                                          ^ expected lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `person1` or `person2`

该错误消息非常明显。 我们的函数是返回借用的值。 该值必须从某个地方借来。 仅有的两个选项是 person1 和 person2。 似乎 Rust 由于某种原因需要知道这一点。

这是一个小故障,请参阅下面的 “静态生命周期”。

还记得关于引用的规则吗? 引用的生命周期不能超过它们的原始值。 我们需要跟踪结果值允许存活的时间,它必须小于或等于它来自生命的时间。 整个概念就是生命。

处于某种原因,我们稍后将要讨论(在“生命周期省略”之中)。通常我们可以使用显式生命周期绕过这个问题。 然而,有时候我们确实需要表达清楚。 为此,我们引入了一些新的参数。 但是这一次,它们是生存期参数,以单引号开头,小写。 通常它们只是一个单一的字母。 例如:

fn get_older_name<'a, 'b>(person1: &'a Person, person2: &'b Person) -> &String

我们仍然从编译器得到一个错误,因为我们的返回值没有生命周期。 我们应该选择 a 还是 b? 或者我们可以创建一个新的 c 并尝试一下? 让我们从 “ a” 开始。 我们得到了错误消息:

error[E0623]: lifetime mismatch
  --> src/main.rs:10:9
   |
6  | fn get_older_name<'a, 'b>(person1: &'a Person, person2: &'b Person) -> &'a String {
   |                                                         ----------     ----------
   |                                                         |
   |                                                         this parameter and the return type are declared with different lifetimes...
...
10 |         &person2.name
   |         ^^^^^^^^^^^^^ ...but data from `person2` is returned here

这是有道理的: 因为我们的结果可能来自于 person2,我们不能保证 ‘ a 寿命参数小于或等于 ’ b 寿命参数。 幸运的是,我们可以明确地声明,就像我们声明类型实现了一些 traits 一样:

fn get_older_name<'a, 'b: 'a>(person1: &'a Person, person2: &'b Person) -> &'a String {

这实际上可以编译! 或者,在这种情况下,我们可以完全绕过第二个生命周期参数,并说 person1 和 person2 必须具有相同的生命周期,该生命周期必须与返回值相同:

fn get_older_name<'a>(person1: &'a Person, person2: &'a Person) -> &'a String {

如果你像我一样,你可能会认为这样做过于局限。 例如,我一开始认为上面的签名不能编译这段代码:

fn main() {
    let alice = Person {
        name: String::from("Alice"),
        age: 30,
    };
    foo(&alice);
}

fn foo(alice: &Person) {
    let bob = Person {
        name: String::from("Bob"),
        age: 35,
    };
    let name = get_older_name(&alice, &bob);
    println!("Older person: {}", name);
}

Alice 的生命周期显然比Bob的生命周期要长。 但是,函数签名中生存期的语义是所有值至少具有相同的生存期。 如果他们碰巧活得久一点,没有伤害,没有犯规。

多生命周期参数需求

那么,我们能否举出一个绝对需要多个生存期参数的例子呢? 当然!

fn message_and_return(msg: &String, ret: &String) -> &String {
    println!("Printing the message: {}", msg);
    ret
}

fn main() {
    let name = String::from("Alice");
    let msg = String::from("This is the message");
    let ret = message_and_return(&msg, &name);
    println!("Return value: {}", ret);
}

这段代码不能编译,因为我们需要一些生存期参数。 所以让我们用上面的方法,使用相同的参数:

fn message_and_return<'a>(msg: &'a String, ret: &'a String) -> &'a String {

可以编译,但是让我们的调用代码更加复杂:

fn main() {
    let name = String::from("Alice");
    let ret = foo(&name);
    println!("Return value: {}", ret);
}

fn foo(name: &String) -> &String {
    let msg = String::from("This is the message");
    message_and_return(&msg, &name)
}

现在编译器不高兴了:

error[E0597]: `msg` does not live long enough
  --> src/main.rs:14:25
   |
14 |     message_and_return(&msg, &name)
   |                         ^^^ borrowed value does not live long enough
15 | }
   | - borrowed value only lives until here
   |
note: borrowed value must be valid for the anonymous lifetime #1 defined on the function body at 12:1...
  --> src/main.rs:12:1
   |
12 | / fn foo(name: &String) -> &String {
13 | |     let msg = String::from("This is the message");
14 | |     message_and_return(&msg, &name)
15 | | }
   | |_^

我们已经说过,返回值必须与 msg 参数使用相同的生命周期。 但我们将返回值返回到 foo 函数之外,而 msg 值的生命周期不会超出 foo 的结尾。

调用代码应该没问题,我们只需要告诉编译器 msg 参数的生存期短于返回值就可以。

练习3

修改 message_and_return 的签名,以便代码编译和运行。

省略生命周期

为什么有时我们跳过生命周期,有时又需要包括它们? 语言中有规则,称为“省略生命周期”。 与其试图自己解决这个问题,不如参考一下文档:

https://doc.rust-lang.org/nomicon/lifetime-elision.html

静态生命周期

上面,我暗示如果返回一个引用,那么它必须具有与其一个输入参数相同的生存期。这在大多数情况下是有道理的,因为否则我们将不得不凭空想出一些任意的生命周期。然而,这也是一个谎言。 有一个特殊的生命周期存在于整个程序中,叫做 'static。这是一个有趣的消息:自我们共同编写的第一个 Hello World 以来,您就暗中使用了它!

每个字符串实际上都是一个具有 static 生命周期的引用。

fn name() -> &'static str {
    "Alice"
}

fn main() {
    println!("{}", name());
}

数组、切片、Vec和字符串

这是另一个我们一直厚颜无耻的地方。 String 和 str 之间的区别是什么? 这两个词都出现了不少。 我们一会儿会讲到这些。 首先,我们需要讨论数组、切片和向量。

数组

据我所知,关于数组的最好的官方文档是 API 文档本身。 数组是包含固定长度的单一数据类型的连续内存块。 类型表示为 [ t; n ] ,其中 t 是值的类型,n 是数组的长度。 与任何正常的编程语言一样,数组在 Rust 中的索引是从0开始的。

初始化数组有两种语法,列出常量语法(如 Javascript、 Python 或 Haskell) :

fn main() {
    let nums: [u32; 5] = [1, 2, 3, 4, 5];
    println!("{:?}", nums);
}

和重复表达式:

fn main() {
    let nums: [u32; 5] = [42; 5];
    println!("{:?}", nums);
}

你可以让数组可以变,然后,对它们进行修改:

fn main() {
    let mut nums: [u32; 5] = [42; 5];
    nums[2] += 1;
    println!("{:?}", nums);
}

很好,但是如果您需要动态的东西怎么办? 为此,我们有……

Vec

Vec是 “ 连续的,可增长的数组类型”。 可以使用push或pull,可以在 O(1) 时间通过索引访问其长度。 我们还有一个漂亮的vec! 用于构建它们的宏:

fn main() {
    let mut v: Vec<u32> = vec![1, 2, 3];
    v.push(4);
    assert_eq!(v.pop(), Some(4));
    v.push(4);
    v.push(5);
    v.push(6);
    assert_eq!(v.pop(), Some(6));
    assert_eq!(v[2], 3);
    println!("{:?}", v); // 1, 2, 3, 4, 5
}

切片

我将编写一个 helper 函数,它将输出 Vec 中的所有值:

fn main() {
    let v: Vec<u32> = vec![1, 2, 3];
    print_vals(v);
}

fn print_vals(v: Vec<u32>) {
    for i in v {
        println!("{}", i);
    }
}

当然,因为这是一个传递值,所以下面的代码不能编译:

fn main() {
    let mut v: Vec<u32> = vec![1, 2, 3];
    print_vals(v);
    v.push(4);
    print_vals(v);
}

很容易修复:让 print_vals 引用一个 Vec:

fn main() {
    let mut v: Vec<u32> = vec![1, 2, 3];
    print_vals(&v);
    v.push(4);
    print_vals(&v);
}

fn print_vals(v: &Vec<u32>) {
    for i in v {
        println!("{}", i);
    }
}

不幸的是,这并不能泛化为,比如说,数组:

fn main() {
    let a: [u32; 5] = [1, 2, 3, 4, 5];
    print_vals(&a);
}

这个方法失败了,因为 print_vals 需要一个 &Vec<u32> ,但是我们提供了一个 &[u32; 5] 。 但是这是相当令人失望的。 一个动态Vec和一个固定长度的数组在很多情况下都是一样的。 有什么东西能把这两者都概括起来吗?

引入切片。 引用《The Rust book》:

允许您引用集合中的连续元素序列,而不是整个集合。

为了实现这些功能,我们需要将 print_vals 的签名改为

fn print_vals(v: &[u32]) {

&[u32] 是对切片 u32 的引用。 可以从数组或Vec创建切片,更不用说其他情况了。 (我们将稍微讨论&借位运算符的作用)。作为一般性建议,如果您收到的是一个由一系列值组成的参数,请尝试使用切片,因为它会给调用者更好地控制数据的来源。

我在上面玩了一些文字游戏,在“引用切片”和“切片”之间切换。 显然,我们正在使用引用。 我们可以取消对切片引用的引用并获取切片本身吗? 我们试试吧!

fn print_vals(vref: &[u32]) {
    let v = *vref;
    for i in v {
        println!("{}", i);
    }
}

编译器又跟我们发脾气了:

error[E0277]: the size for values of type `[u32]` cannot be known at compilation time
 --> src/main.rs:7:9
  |
7 |     let v = *vref;
  |         ^ doesn't have a size known at compile-time
  |
  = help: the trait `std::marker::Sized` is not implemented for `[u32]`
  = note: to learn more, visit <https://doc.rust-lang.org/book/second-edition/ch19-04-advanced-types.html#dynamically-sized-types-and-sized>
  = note: all local variables must have a statically known size

error[E0277]: the size for values of type `[u32]` cannot be known at compilation time
 --> src/main.rs:8:14
  |
8 |     for i in v {
  |              ^ doesn't have a size known at compile-time
  |
  = help: the trait `std::marker::Sized` is not implemented for `[u32]`
  = note: to learn more, visit <https://doc.rust-lang.org/book/second-edition/ch19-04-advanced-types.html#dynamically-sized-types-and-sized>
  = note: required by `std::iter::IntoIterator::into_iter`

error[E0277]: the trait bound `[u32]: std::iter::Iterator` is not satisfied
 --> src/main.rs:8:14
  |
8 |     for i in v {
  |              ^ `[u32]` is not an iterator; maybe try calling `.iter()` or a similar method
  |
  = help: the trait `std::iter::Iterator` is not implemented for `[u32]`
  = note: required by `std::iter::IntoIterator::into_iter`

基本上,没有办法取消对切片的引用。 从逻辑上讲,无论是在堆栈,堆还是在可执行文件本身上,只要保留对包含值的内存块的引用(例如字符串字面值,我们将在后面介绍),就合乎逻辑。

Deref

有些地方不太对劲, 为什么 ampersand/borrow 操作符给我们提供了不同的类型!

fn main() {
    let v = vec![1, 2, 3];
    let _: &Vec<u32> = &v;
    let _: &[u32] = &v;
}

事实证明,借用运算符与 “ Deref强制” 交互。 如果您对此感到好奇,请查看 Deref 特性的文档。 例如,我可以创建一个可以借用到切片中的新结构:

use std::ops::Deref;

struct MyArray([u32; 5]);

impl MyArray {
    fn new() -> MyArray {
        MyArray([42; 5])
    }
}

impl Deref for MyArray {
    type Target = [u32];

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

fn main() {
    let ma = MyArray::new();
    let _: &MyArray = &ma;
    let _: &[u32] = &ma;
}

感谢 udoprog 回答了这个问题。另外,仅仅因为你能做到这一点并不意味着你就应该这么做

使用切片

切片与其他类型一样是数据类型。您可以查看 std::slice 模块文档 slice 原语类型

一些常见的互动方式包括:

  • 使用它们作为迭代器

  • 使用 slice[idx] 语法对它们进行索引

  • 使用 slice[ start .. end ] 语法获取子切片

Byte 字面量

如果在字符串文本前面放置一个小写 b,就会得到一个字节数组。 你可以把它看作一个固定长度的数组,或者,更常见的,看作一个切片:

fn main() {
    let bytearray1: &[u8; 22] = b"Hello World in binary!";
    let bytearray2: &[u8] = b"Hello World in binary!";
    println!("{:?}", bytearray1);
    println!("{:?}", bytearray2);
}

请注意,您总是接收到对值的引用,而不是值本身。 数据存储在程序可执行文件中,因此无法修改(因此总是接收不可变引用)。

练习4

在上面的 bytearray1 和 bytearray2 类型中添加生存期参数。

Strings

最后我们可以谈谈字符串! 您可能认为字符串文本是一个固定长度的字符数组。 事实上,你可以创造这样一个东西:

fn main() {
    let char_array: [char; 5] = ['H', 'e', 'l', 'l', 'o'];
    println!("{:?}", char_array);
}

但是,这不是 str。 上面的表述是非常低效的。 由于 Rust 中的 char 具有完全的 Unicode 支持,所以它占用了4个内存字节(32位)。

然而,对于大多数数据来说,这是过度杀伤。 像 UTF-8这样的字符编码将会更有效率。

注意:如果您不熟悉Unicode字符编码,可以在此处忽略这些详细信息。 了解字符串在Rust中的工作方式并不是至关重要的。

取而代之的是,字符串切片(&str) 本质上是围绕字节切片(&[u8]) 的新型包装,该字节包装保证采用UTF-8编码。 这需要权衡一些重要事项:

  • 您可以很便宜地(自由地?)从一个 &str 转换为一个 &[u8] ,这对于进行系统调用非常有用

  • 您无法在字符串中获得 O(1)随机访问,因为 UTF-8 编码不允许这样做。 相反,您需要使用字符迭代器来查看单个字符。

练习5

在 String 上使用 std::env::args 和 chars() 方法打印出每个命令行参数中的字符数。 附加说明: 还要打印出字节数。 样本用法:

$ cargo run שלום
arg: target/debug/foo, characters: 16, bytes: 16
arg: שלום, characters: 4, bytes: 8

不要忘记,第一个参数是可执行文件的名称。

结构体的生命周期

今天的最后一个主题是结构体的生命周期。 在结构体中保留引用是完全可能的。 然而,当你这样做的时候,你需要清楚地说明他们的生命周期。 例如,这将无法编译:

struct Person {
    name: &str,
    age: u32,
}

相反,你需要这样写:

struct Person<'a> {
    name: &'a str,
    age: u32,
}

我收到的一般建议是,尽可能避免这种情况,我也会转达这个建议。 在处理结构体的生存期参数时,事情最终会变得非常复杂。 通常,应该在结构体中使用属性版本的值(例如,String 而不是 &str,Vec 或数组而不是 slice)。 在这种情况下,您需要确保结构体引用的生存期超过结构本身的生存期。

有时候,如果在结构体中使用引用,就可以避免一些额外的克隆和分配,而当您需要这样做时,可能就会出现这种情况。 但是我建议你等到你的分析指出是瓶颈时再说。 欲了解更多信息,请参阅《The Rust Book》。

Api 中的引用和切片

以下是我收到的一些建议,大多数情况下都是正确的:

接收参数时,尽可能选择切片

然而有时候这是过于简单化的。 如果你想深入了解,有一篇很棒的博客文章介绍了公共 api 的一些权衡:On dealing with owning and borrowing in public interfacesReddit discussion 也很棒。

最后更新于