首页 > 其他分享 >Rust——切片

Rust——切片

时间:2024-08-08 13:17:57浏览次数:7  
标签:word String 切片 let 字符串 Rust first

前言

这一章我们一起来学习下切片类型,通过切片,您可以引用集合中连续的元素序列,而不是整个集合。切片是一种引用,因此它没有所有权。

内容

切片类型

这里有一个小的编程问题:编写一个函数,该函数接受一个由空格分隔的单词字符串,并返回它在该字符串中找到的第一个单词。如果函数在字符串中找不到空格,则整个字符串必须是一个单词,因此应返回整个字符串。

让我们来看看如何在不使用切片的情况下编写此函数的签名,以理解切片将解决的问题:

fn first_word(s: &String) -> ?

first_word 函数有一个参数 &String。因为我们不需要所有权,所以这没有问题。不过应该返回什么呢?我们并没有一个真正获取 部分 字符串的办法。不过,我们可以返回单词结尾的索引。

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 方法在字节数组上创建一个迭代器:

 for (i, &item) in bytes.iter().enumerate() {

我们将在后续详细讨论迭代器。现在,只需知道 iter 方法返回集合中的每一个元素,而 enumerate 包装了 iter 的结果,将这些元素作为元组的一部分来返回。从 enumerate 返回的元组的第一个元素是索引,第二个元素是对集合中元素的引用。这比自己计算索引要方便一些。

由于 enumerate 方法返回一个元组,因此我们可以使用模式来解构该元组。我们将在后续进一步讨论有关模式的问题。在 for 循环中,我们指定了一个模式,该模式 i 表示元组中的索引,&item 表示元组中的单个字节。因为我们从 .iter().enumerate() 获取了对集合元素的引用,所以我们在模式中使用 &

for 循环中,我们通过字节的字面量语法来搜索代表空格的字节。如果找到了一个空格,返回它的位置。否则,使用 s.len() 返回字符串的长度:

   if item == b' ' {
            return i;
        }
    }

    s.len()

我们现在有一种方法可以找出字符串中第一个单词末尾的索引,但是有一个问题。我们单独返回一个 usize,但它在 &String 的上下文中只是一个有意义的数字。换言之,由于它是独立于 String 的值,因此无法保证它将来仍然有效。现在让我们来使用first_word 函数。

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

    let word = first_word(&s); // word 的值为 5

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

    // word 在此处的值仍然是 5,
    // 但是没有更多的字符串让我们可以有效地应用数值 5。word 的值现在完全无效!
}

这个程序编译时没有任何错误,而且在调用 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 的一部分的引用,它看起来像这样:

    let s = String::from("hello world");

    let hello = &s[0..5];
    let world = &s[6..11];

你好不是对整个 String 的引用,而是对 String 的一部分的引用,在额外的 [0..5] 位中指定。我们通过指定 [starting_index..ending_index],其中 starting_index 是切片中的第一个位置,ending_index 是切片中的最后一个位置多 1。在内部,切片数据结构存储切片的起始位置和长度,这对应于 ending_index减去starting_index。因此,在 let world = &s[6..11]; 的情况下,world 将是一个切片,其中包含指向 s 索引 6 处的字节的指针,长度值为 5

<iframe frameborder="0" id="embed_dom" name="embed_dom" src="https://www.processon.com/embed/66b2bf2c80d3552cffa4ecaa?cid=66b2bf2c80d3552cffa4ecad" style="display: block; width: 489px; height: 275px"></iframe>

对于 Rust 的 .. range 语法,如果想要从索引 0 开始,可以不写两个点号之前的值。换句话说,如下两个语句是相同的:

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

    let slice1 = &s[0..2];
    let slice2 = &s[..2];
    println!("{}", slice1);
    println!("{}", slice2);
}

同样,如果切片包含 String 的最后一个字节,则可以删除尾随数字。

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

    let len = s.len();

    let slice1 = &s[3..len];
    let slice2 = &s[3..];
    println!("{}", slice1);
    println!("{}", slice2)
}

也可以同时舍弃这两个值来获取整个字符串的 slice。所以如下亦是相同的:

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

    let len = s.len();

    let slice1 = &s[..len];
    let slice2 = &s[..];
    println!("{}", slice1);
    println!("{}", slice2)
}

注意:字符串切片范围索引必须出现在有效的 UTF-8 字符边界内。如果尝试在多字节字符的中间创建字符串切片,则程序将退出并显示错误。

考虑到所有这些信息,让我们重写first_word以返回一个切片。表示“字符串切片”的类型写为 &str

fn main() {
    let my_string = String::from("hello world");
    let word = first_word(&my_string);
    println!("{}", word)
}
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 时,我们会返回一个与基础数据相关联的值。该值由对切片起点的引用和切片中的元素数组成。

返回切片也适用于second_word函数:

fn second_word(s: &String) -> &str {

我们现在有一个简单的 API,因为编译器将确保对 String 的引用保持有效。还记得前面程序中的错误吗,当时我们获取了第一个单词末尾的索引,但随后清除了字符串,因此我们的索引无效?该代码在逻辑上是错误的,但没有立即显示任何错误。如果我们继续尝试使用第一个带有空字符串的单词索引,问题就会暴露出来。slice 就不可能出现这种 bug 并让我们更早的知道出问题了。使用 slice 版本的 first_word 会抛出一个编译时错误:

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

    let word = first_word(&s);

    s.clear(); // error!

    println!("the first word is: {word}");
}

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

编译错误:

error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
  --> src/main.rs:87:5
   |
85 |     let word = first_word(&s);
   |                           -- immutable borrow occurs here
86 |
87 |     s.clear(); // error!
   |     ^^^^^^^^^ mutable borrow occurs here
88 |
89 |     println!("the first word is: {word}");
   |                                  ------ immutable borrow later used here

回忆一下借用规则,如果我们对某物有一个不可变的引用,我们就不能也接受一个可变的引用。因为 clear 需要清空 String,所以它需要获取一个可变的引用。在调用 clear 之后的 println! 使用了 word 中的引用,所以这个不可变的引用在此时必须仍然有效。Rust 不允许 clear 中的可变引用和 word 中的不可变引用同时存在,因此编译失败。Rust 不仅使得我们的 API 简单易用,也在编译时就消除了一整类的错误!

字符串字面量就是切片

回想一下,我们讨论过将字符串文字存储在二进制文件中。现在我们知道了切片,我们可以正确理解字符串字面量了:

let s = "Hello, world!";

这里的 s 类型是 &str:它是一个指向二进制文件特定位置的切片。这也是字符串字面量是不可变的原因,&str 是不可变的引用。

字符串切片作为参数

在知道了能够获取字面量和 String 的 slice 后,我们对 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 word1 = first_word(&my_string[0..6]);
    let word2 = first_word(&my_string[..]);
    // `first_word` also works on references to `String`s, which are equivalent
    // to whole slices of `String`s
    let word3 = first_word(&my_string);
    println!("1{}", word1);
    println!("2{}", word2);
    println!("3{}", word3);

    let my_string_literal = "hello world";

    // `first_word` works on slices of string literals, whether partial or whole
    let word4 = first_word(&my_string_literal[0..6]);
    let word5 = first_word(&my_string_literal[..]);
    println!("4{}", word4);
    println!("5{}", word5);

    // Because string literals *are* string slices already,
    // this works too, without the slice syntax!
    let word6 = first_word(my_string_literal);
    println!("6{}", word6);
}
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 a = [1, 2, 3, 4, 5];
}

就跟我们想要获取字符串的一部分那样,我们也会想要引用数组的一部分。我们可以这样做:

fn main() {
    let a = [1, 2, 3, 4, 5];
    println!("a has {} elements", a.len());
    let slice = &a[1..3];
    println!("{:?}", slice);
}

此切片的类型为 &[i32]。它的工作方式与字符串切片相同,通过存储对第一个元素的引用和一个集合总长度。您将把这种切片用于各种其他集合。

总结

所有权、借用和切片的概念确保了 Rust 程序在编译时的内存安全。Rust 语言提供了跟其他系统编程语言相同的方式来控制你使用的内存,但拥有数据所有者在离开作用域后自动清除其数据的功能意味着你无须额外编写和调试相关的控制代码。

所有权会影响 Rust 其他部分的工作方式,因此我们将在本书的其余部分进一步讨论这些概念。让我们继续阅读第 5 章,来看看如何将多份数据组合进一个 struct 中。

标签:word,String,切片,let,字符串,Rust,first
From: https://www.cnblogs.com/wangyang0210/p/18348720

相关文章

  • Rust_learn_1
    变量与可变性变量声明变量使用let关键字,在默认情况下,变量是不可变的(Immutable)。为此解决该问题,声明变量时在前面加上mut,就可以使变量可变常量常量(constant),在绑定值之后也是不可变的,但是与不可变的变量有很多区别:不可以使用mut,常量永远是不变的声明常量用const关键......
  • 16.python索引和切片
    (一)索引定义:索引也叫下标或角标作用:可以直接使用索引来访问序列中的元素,索引分为两种:正向索引和负向索引正向索引:从索引0开始负向索引:从-1开始(二)切片1、定义:切片象截是指对操作的截取其中一部分的操作,字符串,列表,元组都支持切片操作2、切片的语法:【开始索引:结束索引:步长】......
  • [Rust]使用Rocket框架搭建简单Web服务
    本文主要讲述如何在Rust中使用Rocket搭建简易Web服务1.添加Rocket库Cargo.toml[dependencies]rocket={version="0.5.1",features=["secrets"]}2.创建服务2.1创建一个启动脚本main.rsuserocket::{launch,routes};#[launch]fnrocket()->_{rocket......
  • Rust项目的代码组织
    学习一种编程语言时,常常优先关注在语言的语法和标准库上,希望能够尽快用上新语言来开发,我自己学习新的开发语言时也是这样。不过,想用一种新的语言去开发实际的项目,或者自己做点小工具的话,除了语言本身之外,了解它在项目中如何组织代码也是至关重要的。毕竟在实际项目中,不可能像学习......
  • 【转载】在Android中使用Rust:Rust与Android的结合
    声明:处于学习目的转载本文,若文章侵犯原作者权益,联系本人立即删除,联系方式:[email protected]文章转载于:https://developer.baidu.com/article/detail.html?id=3011246 简介:本文将介绍如何在Android平台上使用Rust编程语言,以及Rust与Android的结合所带来的优势和挑战。我们将探......
  • 如何从系列(切片)创建 Dicom 卷?
    我有一系列Dicom文件(切片)。现在,我想从该系列创建一个dicom卷。我的理解是Dicom卷不仅仅是一堆dicom文件。那么,如何从堆栈创建dicom卷呢?有Python代码可以做到这一点吗?我的目的是研究创建体积数据的方法任何帮助将不胜感激你说的对,“DICOM卷”的概念并不仅......
  • 【Rust光年纪】提升数据安全性与完整性:Rust语言哈希算法库深度对比
    深入探索Rust中的哈希算法库:安装配置与API解析前言在现代软件开发中,数据的安全性和完整性是至关重要的。哈希算法作为一种常见的数据加密和校验手段,在Rust语言中有着广泛的应用。本文将介绍几个用于Rust语言的常见哈希算法库,包括blake2、sha2、md5、crc32、xxhash以及siph......
  • Windows的Docker安装RustDesk自建服务
    一、安装DockerDesktopInstaller 二、CMD拉取RustDesk镜像dockerimagepullrustdesk/rustdesk-server三、创建docker-compose.yml文件services:hbbs:container_name:hbbsimage:rustdesk/rustdesk-server:latestcommand:hbbs-r公网IP:端口(21117......
  • 7-Python数据类型——列表和元组的详解(增删改查、索引、切片、步长、循环)
    一、列表1.1列表list有序且可变的容器,可以存放多个不同类型的元素列表就是专门用来记录多个同种属性的值列表:存储同一个类别的数据,方便操作字符串,不可变:即:创建好之后内部就无法修改【内置功能都是新创建一份数据】name="xiaochaun"data=name.upper()print(nam......
  • 用matlab中的stlread函数得到三维模型后应该怎么得到模型根据z轴变化的切片?z轴上的每
    用matlab中的stlread函数得到三维模型后应该怎么得到模型根据z轴变化的切片?z轴上的每个面我都需要一个模型截面在MATLAB中,从STL文件读取三维模型后,您可以使用以下步骤获取根据Z轴变化的切片。这里提供一个基本的步骤和示例代码,帮助您实现这一目标:读取STL文......