Rust之常用集合(二):字符串(String)

news2024/11/23 17:09:20

开发环境

  • Windows 10
  • Rust 1.65.0

 

   VS Code 1.73.1

 项目工程

这里继续沿用上次工程rust-demo

用字符串存储UTF-8编码的文本

我们在之前的章节中讨论了字符串,但现在我们将更深入地研究它们。新的Rust人会因为三个原因而陷入字符串:Rust倾向于暴露可能的错误,字符串是比许多程序员认为的更复杂的数据结构,以及UTF-8。当您来自其他编程语言时,这些因素以一种看起来很困难的方式结合在一起。

我们在集合的上下文中讨论字符串,因为字符串是作为字节的集合实现的,加上一些在那些字节被解释为文本时提供有用功能的方法。在这一节中,我们将讨论每个集合类型对String的操作,比如创建、更新和读取。我们还将讨论String与其他集合的不同之处,即人和计算机解释String数据的方式不同,如何使String的索引变得复杂。

字符串

我们先来定义一下字符串这个词的含义。Rust在核心语言中只有一个字符串类型,那就是字符串切片str,通常以其借用的形式&str出现。在之前的章节中,我们谈到了字符串切片,它是对存储在其他地方的一些UTF-8编码的字符串数据的引用。例如,字符串字面意义存储在程序的二进制中,因此是字符串切片。

String类型是由Rust的标准库提供的,而不是编入核心语言的,它是一个可增长的、可变异的、自有的、UTF-8编码的字符串类型。当Rust人在Rust中提到 "字符串 "时,他们可能指的是String类型或字符串切片&str类型,而不仅仅是这些类型中的一个。虽然本节主要是关于String,但这两种类型在Rust的标准库中都被大量使用,而且String和字符串切片都是UTF-8编码的。

创建新字符串

许多与Vec<T>相同的操作也可以用在String上,因为String实际上是作为一个字节向量的封装器来实现的,有一些额外的保证、限制和能力。一个在Vec<T>String中以同样方式工作的函数的例子是创建实例的new函数。如下例所示,

fn main() {
    let mut s = String::new();        // 创建字符串
}

这一行创建了一个新的名为s的空字符串,然后我们可以将数据加载到其中。通常,我们会有一些想要开始字符串的初始数据。为此,我们使用to_string方法,该方法可用于任何实现Display特征的类型,就像字符串一样。如下所示,

fn main() {
    let data = "initial contents";

    let s = data.to_string();         // 创建字符串s,并将data的值赋给s

    // 该方法也可以直接使用
    let s = "initial contents".to_string();
}

 这段代码创建了一个包含初始内容的字符串。

我们也可以使用函数String::from来从一个字符串字面创建一个字符串。下例中的代码等同于上例中使用to_string的代码。

fn main() {
    let s = String::from("initial contents");   // 通过from函数创建并初始化字符串变量s
}

由于字符串的用途非常多,我们可以使用许多不同的字符串通用API,为我们提供了很多选择。其中有些看起来是多余的,但它们都有自己的位置 在这种情况下,String::fromto_string做同样的事情,所以你选择哪个是风格和可读性的问题。

请记住,字符串是UTF-8编码的,所以我们可以在其中包含任何正确编码的数据,如下例所示。

fn main() {
    let hello = String::from("السلام عليكم");       // 字符串创建示例
    print!("{}\n", hello);
    let hello = String::from("Dobrý den");
    print!("{}\n", hello);
    let hello = String::from("Hello");
    print!("{}\n", hello);
    let hello = String::from("שָׁלוֹם");
    print!("{}\n", hello);
    let hello = String::from("नमस्ते");
    print!("{}\n", hello);
    let hello = String::from("こんにちは");
    print!("{}\n", hello);
    let hello = String::from("안녕하세요");
    print!("{}\n", hello);
    let hello = String::from("你好");
    print!("{}\n", hello);
    let hello = String::from("Olá");
    print!("{}\n", hello);
    let hello = String::from("Здравствуйте");
    print!("{}\n", hello);
    let hello = String::from("Hola");
    print!("{}\n", hello);
}

编译运行

cargo run

结果 

 

 所有这些都是有效的String值。

更新字符串

如果你把更多的数据推入一个String,它的大小可以增长,它的内容可以改变,就像Vec<T>的内容一样。此外,你可以方便地使用+运算符或format!宏来连接String的值。

追加字符串:push_str和push

我们可以通过使用push_str方法追加一个String切片来增长一个字符串,下例所示。

fn main() {
    let mut s = String::from("foo");
    s.push_str("bar");     // 追加字符串,push_str接口
}

在这两行之后,s将包含foobarpush_str方法需要一个字符串切片,因为我们不一定要取得参数的所有权。例如,在下例中,我们希望在将s2的内容追加到s1后能够使用它。

fn main() {
    let mut s1 = String::from("foo");     // 创建字符串s1
    let s2 = "bar";                       // 初始化字符串s2
    s1.push_str(s2);                      // 将s2追加到s1
    println!("s2 is {}", s2);
    println!("s1 is {}", s1);
}

编译运行

cargo run

 如果push_str方法取得了s2的所有权,我们就不能在最后一行打印它的值。然而,这段代码正如我们所期望的那样工作。

push方法接受一个单一的字符作为参数并将其添加到String中。下例中使用push方法将字母 "l "添加到一个String中。

fn main() {
    let mut s = String::from("lo");
    s.push('l');            // push接口
    println!("s is {}", s);
}

编译运行

cargo run

结果 

结果显示,s将包含lol

格式化字符串:用 "+"运算符或 "format!宏

通常情况下,你会想把两个现有的字符串结合起来。一种方法是使用+运算符,如下例所示。

fn main() {
    let s1 = String::from("Hello, ");
    let s2 = String::from("world!");
    let s3 = s1 + &s2; // + 运算符
    println!("s3 is {}", s3);
}

编译运行

cargo run

结果 

字符串s3将包含Hello, world!s1在添加后不再有效的原因,以及我们使用对s2的引用的原因,都与我们使用+运算符时调用的方法的签名有关。+操作符使用add方法,其签名看起来像这样。

fn add(self, s: &str) -> String {

在标准库中,你会看到使用泛型和关联类型定义的add。在这里,我们用具体的类型来代替,这就是我们用String值调用这个方法时的情况。我们将在后续章节讨论泛型。这个签名为我们提供了理解+运算符的棘手部分所需的线索。

首先,s2有一个&,意味着我们要把第二个字符串的引用添加到第一个字符串中。这是因为add函数中的s参数:我们只能把一个&str加到一个String上;我们不能把两个String值加在一起。但是等等--&s2的类型是&String,而不是&str,正如add的第二个参数所指定的那样。那么,为什么上例会被编译?

我们能够在调用add时使用&s2的原因是,编译器可以将&String参数强制变成&str。当我们调用add方法时,Rust使用了一个deref coercion,在这里它把&s2变成了&s2[...]。我们将在后续章节更深入地讨论deref coercion。因为add并没有取得s参数的所有权,所以s2在这个操作之后仍然是一个有效的String

第二,我们可以在签名中看到,add取得了self的所有权,因为self没有&。这意味着上例中的s1将被移到add调用中,此后将不再有效。因此,尽管let s3 = s1 + &s2;看起来会复制两个字符串并创建一个新的字符串,但这个语句实际上取得了s1的所有权,附加了s2内容的副本,然后返回结果的所有权。换句话说,它看起来是在做大量的拷贝,但其实不是;实现起来比拷贝更有效率。

如果我们需要串联多个字符串,那么+运算符的行为就会变得很不方便。

fn main() {
    let s1 = String::from("tic");
    let s2 = String::from("tac");
    let s3 = String::from("toe");

    let s = s1 + "-" + &s2 + "-" + &s3;          // 多字符串追加
    
    println!("s is {}", s);
}

编译运行

cargo run

 结果

在这一点上,s将是tic-tac-toe。由于所有的 "+"和""字符,我们很难看到发生了什么事。对于更复杂的字符串组合,我们可以改用format!宏。 

fn main() {
    let s1 = String::from("tic");
    let s2 = String::from("tac");
    let s3 = String::from("toe");

    let s = format!("{}-{}-{}", s1, s2, s3);       // format!宏
    
    println!("s is {}", s);
}

编译运行

cargo run

结果

这段代码还将s设置为tic-tac-toeformat!宏的工作原理与println!类似,但它不是将输出打印到屏幕上,而是返回一个包含内容的字符串。使用format! 的代码版本更容易阅读,由format! 宏生成的代码使用了引用,这样这个调用就不会占用任何参数的所有权。

对字符串进行索引

在许多其他编程语言中,通过索引来访问字符串中的单个字符是一种有效的、常见的操作。然而,如果你试图用Rust中的索引语法访问字符串的一部分,你会得到一个错误。请看下例中的无效代码。

fn main() {
    let s1 = String::from("hello");
    let h = s1[0];
}

编译

cargo run

这个错误和注释说明了问题。Rust字符串不支持索引。但为什么不呢?为了回答这个问题,我们需要讨论Rust如何在内存中存储字符串。 

内部表述

字符串是对Vec<u8>的一个封装。让我们看看上例中的一些正确编码的UTF-8示例字符串。首先是这个。

 let hello = String::from("Hola");

在这种情况下,len将是4,这意味着存储 "Hola "字符串的向量有4个字节长。当用UTF-8编码时,每个字母需要1个字节。然而,下面一行可能会让你吃惊。(请注意,这个字符串以大写的西里尔字母Ze开始,而不是阿拉伯数字3)。

let hello = String::from("Здравствуйте");

当被问及这个字符串有多长时,你可能会说12。事实上,Rust的答案是24:这是UTF-8编码 "Здравствуйте "所需的字节数,因为该字符串中的每个Unicode标量值需要2个字节的存储空间。因此,对字符串字节的索引并不总是与有效的Unicode标量值相关。为了证明这一点,请看这个无效的Rust代码。

let hello = "Здравствуйте";
let answer = &hello[0];

你已经知道answer不会是З,第一个字母。当用UTF-8编码时,З的第一个字节是208,第二个字节是151,所以看起来answer实际上应该是208,但是208本身不是一个有效的字符。如果用户要求得到这个字符串的第一个字母,返回208可能不是他们想要的;但是,这是Rust在字节索引0处的唯一数据。用户一般不希望返回字节值,即使字符串只包含拉丁字母:如果&"hello"[0]是返回字节值的有效代码,它将返回104,而不是h

那么答案是,为了避免返回一个意外的值,造成可能无法立即发现的bug,Rust根本不编译这段代码,并在开发过程的早期防止误解。

字节和标量值以及字母簇

关于UTF-8的另一点是,从Rust的角度来看,实际上有三种相关的方式来看待字符串:作为字节、标量值和字素簇(最接近于我们所说的字母)。

如果我们看一下用Devanagari文字写的印地语单词 "नमस्ते",它被存储为一个u8值的向量,看起来像这样。

[224, 164, 168, 224, 164, 174, 224, 164, 184, 224, 165, 141, 224, 164, 164,
224, 165, 135]

这就是18个字节,也是计算机最终存储这些数据的方式。如果我们把它们看成Unicode标量值,也就是Rust的char类型,这些字节看起来是这样的。

['न', 'म', 'स', '्', 'त', 'े']

这里有六个char值,但第四个和第六个不是字母:它们是本身没有意义的变音符号。最后,如果我们把它们看作是字素簇,我们会得到一个人所说的组成印地语单词的四个字母。

["न", "म", "स्", "ते"]

Rust提供了不同的方式来解释计算机存储的原始字符串数据,这样每个程序都可以选择它所需要的解释,无论数据是用什么人类语言。

Rust不允许我们在一个String中建立索引来获得一个字符的最后一个原因是,索引操作预计总是需要恒定的时间 (O(1))。但是用一个String不可能保证这样的性能,因为Rust必须从开始到索引的内容中走一遍,以确定有多少个有效字符。

字符串切片

对字符串进行索引往往是一个坏主意,因为不清楚字符串索引操作的返回类型应该是什么:一个字节值、一个字符、一个字素簇,还是一个字符串片断。因此,如果你真的需要使用索引来创建字符串片,Rust会要求你更加具体。

与使用[]与单个数字进行索引相比,你可以使用[]与一个范围来创建一个包含特定字节的字符串切片。

#![allow(unused)]
fn main() {
    let hello = "Здравствуйте";

    let s = &hello[0..4];      // 字符串切片
}

这里,s将是一个&str,包含了字符串的前4个字节。早些时候,我们提到这些字符中的每一个都是2个字节,这意味着s将是Зд

如果我们试图用&hello[0..1]这样的东西只对一个字符的部分字节进行切分,Rust会在运行时出现恐慌,就像在向量中访问一个无效的索引一样。

#![allow(unused)]
fn main() {
    let hello = "Здравствуйте";

    let s = &hello[0..1];
}

编译

cargo run

 你应该谨慎地使用范围来创建字符串片,因为这样做会使你的程序崩溃。

 字符串迭代

对字符串片段进行操作的最好方法是明确说明你要的是字符还是字节。对于单个Unicode标量值,使用chars方法。在 "Зд "上调用chars,可以分离并返回两个char类型的值,你可以对结果进行迭代以访问每个元素。

#![allow(unused)]
fn main() {
    for c in "Зд".chars() {
        println!("{}", c);
     }
}

编译运行

cargo run

或者,字节方法返回每个原始字节,这可能适合你的领域。 

#![allow(unused)]
fn main() {
    for b in "Зд".bytes() {
       println!("{}", b);
   }
}

编译运行

cargo run

但一定要记住,有效的Unicode标量值可能由1个以上的字节组成。 

像Devanagari脚本那样从字符串中获取字素簇是很复杂的,所以标准库没有提供这个功能。如果你需要这个功能,可以在crates.io上找到crates

字符串并不简单

总而言之,字符串是复杂的。不同的编程语言对如何向程序员展示这种复杂性做出了不同的选择。Rust选择将正确处理String数据作为所有Rust程序的默认行为,这意味着程序员必须在处理UTF-8数据上花费更多心思。这种权衡使字符串的复杂性比其他编程语言更明显,但它可以防止你在开发周期的后期处理涉及非ASCII字符的错误。

好消息是,标准库提供了很多基于String&str类型的功能,以帮助正确处理这些复杂的情况。请务必查看文档,了解有用的方法,如用于在字符串中搜索的contains和用于用另一个字符串替换部分字符串的replace

本章重点

  • 字符串的概念
  • 创建字符串,以及初始化字符串
  • 更新字符串,push_str和push的使用
  • 字符串连接和格式化:+操作符和format!宏
  • 字符串索引及注意事项
  • 字符串切片及注意使用范围
  • 字符串的迭代

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/43530.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【季报分析】美团:真的很美

11月25日&#xff0c;港股盘后美团发布了2022年第三季度业绩报告。整体来看&#xff0c;财报数据还算不错&#xff0c;毕竟大部分指标都已经超预期&#xff0c;探员利用这两天假期&#xff0c;把美团的这份财报仔细过了一遍&#xff0c;当然探员也把2022年Q1、Q2的数据也进行了…

Linux代码调试----gdb使用介绍

目录 一、简介 二、gdb使用流程&#xff1a; &#xff08;1&#xff09;gdb的启动 &#xff08;2&#xff09;调试中查看源代码 &#xff08;3&#xff09;开始调试 一、简介 GDB 全称“GNU symbolic debugger”&#xff0c;是 Linux 下常用的程序调试器。发展至今&#xff…

[附源码]SSM计算机毕业设计校园超市进销存管理系统JAVA

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

【Node.js】第九章 数据库

目录 1. 数据库的基本概念 1.1 数据库的概念 1.2 数据库的分类 1.3 数据组织结构 2. MySQL使用介绍 2.1 安装MySQL和MySQL Workbench 2.2 MySQL的基本使用 2.3 使用SQL管理数据库 3. 在项目中使用MySQL 3.1 安装和配置MySQL模块 3.2 操作数据 1. 数据库的基本概念…

word怎么转pdf?word转pdf借助pdf软件即可搞定!

我们在办公的时候&#xff0c;会有word转pdf的办公需求&#xff0c;但是很多人都不清楚word转pdf的方法&#xff0c;那么word怎么转pdf呢&#xff1f;其实方法很简单&#xff0c;利用pdf软件来进行word转pdf的操作即可&#xff0c;如今&#xff0c;市面上pdf软件应接不暇&#…

五、Javascript 空间坐标[尺寸、滑动]

一、尺寸 1.视窗尺寸 document.documentElement.clientWidth&#xff1a;视窗宽度document.documentElement.clientHeight&#xff1a;视窗高度 2.各种尺寸 举例&#xff1a;<div id"gao"></div> 前提&#xff1a;var a document.getElementById(g…

多重共线性如何分析?

判断标准 常见的直观判断方法共有四个&#xff0c;如下&#xff1a; &#xff08;1&#xff09;某些自变量的相关系数值较大&#xff08;比如大于0.8&#xff09;等&#xff0c;可以利用pearson相关系数检验法一般是利用解释变量之间的线性相关程度判断&#xff0c;一般标准是…

Java常用类(二)

目录 JDK8之前的日期时间API java.lang.System类 java.util.Date类 两个构造器的使用 两个方法的使用 java.sql.Date类 涉及两个问题&#xff1a; java.text.SimpleDateFormat类 格式化&#xff1a;日期--->字符串 解析&#xff1a;字符串--->日期&#xff08;格…

基于TM的遥感数据的叶面积指数估算解决方案及或取途径

1、背景与技术路线 叶面积指数是重要的植被结构参数&#xff0c;反演叶面积指数是植被遥感的重要研究内容之一&#xff0c;其影响生 态系统的物质和能量循环&#xff0c;成为作物生长、路面过程、水文和生态等模型的输入参数或状态变量。今 年来&#xff0c;对也铭记指数的反演…

Spark系列之Spark的RDD详解

title: Spark系列 第五章 Spark 的RDD详解 5.1 RDD概述 ​ RDD 是 Spark 的基石&#xff0c;是实现 Spark 数据处理的核心抽象。那么 RDD 为什么会产生呢&#xff1f; ​ Hadoop的MapReduce是一种基于数据集的工作模式&#xff0c;面向数据&#xff0c;这种工作模式一般是从…

Python_数据容器_集合set

一、集合set的定义 考虑使用集合的场景&#xff0c;通过已经学习的列表、元组、字符串三个数据容器特性来分析&#xff1a; 列表可以修改、支持重复元素且有序 元组、字符串、不可修改、支持重复元素且有序 局限就在于&#xff1a;它们支持重复元素 集合定义语法&#xff1…

KubeVirt with DPDK

发布于2022-11-25 15:52:32阅读 1020 Kubernetes优秀的架构设计&#xff0c;借助multus cni intel userspace cni 可以屏蔽了DPDK底层的复杂&#xff0c;让KubeVirt 支持DPDK变得比较容易。 因为 e2e验证 等原因&#xff0c;KubeVirt社区至今未加入对DPDK支持&#xff0c;本篇…

有用的CSS代码块

文章目录调试 DOM 元素的 border通用的网页样式调试 DOM 元素的 border 显示所有DOM元素的border&#xff0c;方便调试网页元素的相对布局。 * {outline: auto; }如何用javascript设置某个网页的style(复制以下代码到浏览器控制台执行即可)&#xff1f; // wuyujin1997 var …

PIC单片机5——串口 中断

//10M晶振 波特率9600 BRG8位波特率发生器 异步模式 #include "p18f458.h" #include "mydelay.h" #pragma config OSCHS,WDTOFF,LVPOFF,DEBUGON void PIC18F_High_isr(void);/*中断服务函数声明*/ void PIC18F_Low_isr(void); void usart_tx(unsigned ch…

基于javaweb房屋租赁管理系统的设计与实现

摘要 当今社会不管房屋出租、出售买卖是必不可少的&#xff0c;人们不管走到哪里都需要有一个温馨的家&#xff0c;有一个落脚之地&#xff0c;所以房源出租市场也是非常火爆&#xff01;不管是房源出租公司或者是个人都需要一套完整的管理系统来掌握整个市场信息。针对这一需求…

观察者模式在spring中的应用

作者&#xff1a;王子源 1 观察者模式简介 1.1 定义 指多个对象间存在一对多的依赖关系&#xff0c;当一个对象的状态发生改变时&#xff0c;所有依赖于它的对象都得到通知并被自动更新。这种模式有时又称作发布-订阅模式、模型-视图模式&#xff0c;它是对象行为型模式。 …

离散卡尔曼滤波实现

离散卡尔曼滤波基本理论 卡尔曼预报器、平滑器可以参考之前的博客&#xff1a;(2条消息) 卡尔曼滤波器_KPer_Yang的博客-CSDN博客 下面贴上一张图1&#xff0c;很直观&#xff1a;分成时间更新和测量更新两步&#xff0c;其中的KKK和PPP有可能随着时间推移变成常数&#xff0…

STM32实战总结:HAL之RTC

RTC基础知识参考&#xff1a; 51单片机内部外设&#xff1a;实时时钟(SPI)_路溪非溪的博客-CSDN博客 STM32中的RTC 51单片机通常是外置的RTC芯片如DS1302&#xff0c;那么STM32的RTC是什么情况呢&#xff1f; STM32芯片自带RTC&#xff0c;因此不须像其他MCU需外接RTC模块。 先…

年末盘点Android 过去一年与未来的一个走势~

随着Android的发展&#xff0c;有些人对Android未来感到茫然&#xff0c;不少人可能会产生这样的疑惑&#xff1a;“从事Android是不是没有前途&#xff0c;Android开发还有什么值得学&#xff1f;“这类话题一直让大家争论不休&#xff0c;它并没有一个确切、唯一的标准答案&a…

中介者模式

思考中介者模式 当多个类&#xff08;对象&#xff09;耦合严重时&#xff0c;通过中介者模式创建一个中介者&#xff0c;多个类不直接交互了&#xff0c;变成和中介者进行交互&#xff0c;松散耦合 1.中介者模式的本质 中介者模式的本质:封装交互。 中介者模式的目的&#xff…