1

还有一种不拥有所属权的数据类型为:slice。

slice使我们可以引用集合中元素的连续序列而不是整个集合。

一个小的编程问题:编写一个函数,该函数需要一个字符串并返回在该字符串中找到的第一个单词。 如果函数在字符串中找不到空格,则整个字符串必须是一个单词,因此应返回整个字符串。

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()
}

在上例中:
1、因为我们需要逐个检查String元素,并检查值是否为空格,所以我们将使用as_bytes方法将String转换为字节数组:let bytes = s.as_bytes();
2、我们使用iter方法在字节数组上创建一个迭代器:for (i, &item) in bytes.iter().enumerate() {

3、在for循环内,我们使用字节文字语法搜索表示空格的字节。 如果找到空格,则返回位置。 否则,我们使用s.len()返回字符串的长度:

    if item == b' ' {
         return i;
     }
 }
 
 s.len()

4、现在,我们可以找到字符串中第一个单词结尾的索引,但这是有问题的, 我们将自己返回一个usize,但是在&String的上下文中,这只是一个有意义的数字。 换句话说,由于它是与String分开的值,因此无法保证它在将来仍然有效,所以将其清除:

fn main() {
    let mut s = String::from("hello world");

    let word = first_word(&s); // word将要获取到的值为5

    s.clear(); // 这将清空字符串,使其等于""

    // word仍然在这里具有值5,但是没有更多的字符串可用于有意义地使用值5.这个词现在完全无效!
    
}

该程序编译时没有任何错误,如果在调用s.clear()之后使用word,也可以这样做。 因为word根本没有连接到s的状态,所以word仍然包含值5。我们可以将值5与变量s一起使用以尝试提取第一个单词,但这将是一个bug,因为自从我们保存了5到word中之后,s发生了变化。

不必担心word中的索引与s中的数据不同步,这既繁琐又容易出错! 如果我们编写second_word函数,则管理这些索引的难度更大。 其签名必须如下所示:
fn second_word(s: &String) -> (usize, usize) {

现在,我们正在跟踪起始索引和结束索引,并且我们有更多的值是根据特定状态下的数据计算得出的,但与该状态完全无关。 现在,我们有三个不相关的变量需要保持同步。

幸运的是,Rust解决了这个问题:字符串切片。

String Slices

字符串切片是对字符串一部分的引用,它看起来像这样:

fn main() {
    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的第7个字节(从1开始)的指针,其长度值为5。

使用Rust的..范围语法,如果想从第一个索引(零)开始,则可以在两个句点之前删除该值。 换句话说,这些是相等的:

#![allow(unused_variables)]
fn main() {
    let s = String::from("hello");

    let slice = &s[0..2];
    let slice = &s[..2];
}

同样,如果分片包含字符串的最后一个字节,则可以删除尾随数字。 这意味着这些是相等的:


#![allow(unused_variables)]
fn main() {
    let s = String::from("hello");

    let len = s.len();

    let slice = &s[3..len];
    let slice = &s[3..];
}

同样的可以删除这两个值以截取整个字符串。 所以这些是相等的:

#![allow(unused_variables)]
fn main() {
    let s = String::from("hello");

    let len = s.len();

    let slice = &s[0..len];
    let slice = &s[..];
}

考虑到所有这些信息后,让我们重写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[..]
}

同样的,返回切片也可以用于second_word函数:fn second_word(s: &String) -> &str {

但是仍旧有一个问题,就是s.clear,废话不多说:

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[..]
}

fn main() {
    let mut s = String::from("hello world");

    let word = first_word(&s);

    s.clear(); // 出错!

    println!("第一个单词是: {}", word);
}

$ cargo run
   |
16 |     let word = first_word(&s);
   |                           -- immutable borrow occurs here
17 | 
18 |     s.clear(); // 出错!
   |     ^^^^^^^^^ mutable borrow occurs here
19 | 
20 |     println!("第一个单词是: {}", word);
   |                                       ---- immutable borrow later used here

error: aborting due to previous error

For more information about this error, try `rustc --explain E0502`.
error: could not compile `ownership`.

To learn more, run the command again with --verbose.

从借阅规则中回想起,如果我们对某物有不可变的引用,那么我们也不能采用可变的引用。 由于clear需要截断String,因此需要获取可变的引用。 Rust不允许这样做,并且编译失败。 Rust不仅使我们的API易于使用,而且还消除了编译时的一整类错误!(解决该问题的方法为,将s.clear语句放到最后)

字符串文字是切片

let s = "Hello, world!";
s的类型是&str:它是指向二进制文件的特定点的切片。 这也是字符串文字不可变的原因。 &str是不可变的引用。

字符串切片作为参数
知道可以分割文字和字符串值后,这使我们对first_word进行了另一项改进,这就是其签名:
fn first_word(s: &String) -> &str { 与其等价的写法是:fn first_word(s: &str) -> &str {

如果我们有一个字符串切片,我们可以直接传递它。 如果我们有一个字符串,我们可以传递整个字符串的一部分。 定义一个函数以采用字符串切片而不是对String的引用使我们的API更通用和有用,而不会丢失任何功能:

fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}

fn main() {
    let my_string = String::from("hello world");

    // first_word适用于`String`s的切片
    let word = first_word(&my_string[..]);

    let my_string_literal = "hello world";

    // first_word适用于字符串文字的切片
    let word = first_word(&my_string_literal[..]);

    // 因为字符串文字*已经是*字符串切片,所以它也可以使用,而无需使用切片语法!
    let word = first_word(my_string_literal);
}

Other Slices

其他类型的切片跟String类型的切片类似:


#![allow(unused_variables)]
fn main() {
    let a = [1, 2, 3, 4, 5];
    let slice = &a[1..3];
}

该切片的类型为&[i32]。 通过存储对第一个元素和长度的引用,它的工作方式与字符串切片相同。

所属权总结:

所有权,借用和切片的概念可确保在编译时Rust程序中的内存安全。 Rust可让我们控制内存使用情况,当数据所有者超出范围时,让数据所有者自动清除该数据意味着我们不必再编写和调试额外的代码。


子康
10 声望0 粉丝