当前位置: 首页 > news >正文

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

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

我们在第4章讨论过字符串,但现在将更深入地探讨它们。新手 Rustacean 常常因为三个原因而卡在字符串上:Rust 倾向于暴露可能的错误、字符串比许多程序员想象的要复杂得多,以及 UTF-8。这些因素结合起来,对于来自其他编程语言的人来说,可能显得很难理解。

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

什么是字符串?

首先定义“字符串”这个术语。Rust 核心语言只有一种字符串类型,即通常以借用形式 &str 出现的字符串切片 str。在第4章,我们讲过字符串切片,它们是对存储在别处某些 UTF-8 编码数据的引用。例如,字符串字面量存储在程序二进制文件中,因此它们就是字符串切片。

String 类型由 Rust 标准库提供,而非内置核心语言,是一种可增长、可变、有所有权且采用 UTF-8 编码的字符串类型。当 Rustacean 提到 Rust 中“strings”时,他们可能指的是 String 或者 字符串切片 &str 两种类型中的任意一种,而不仅仅是一种。虽然本节主要讲解 String,但这两种类型都广泛用于标准库,而且都是 UTF-8 编码。

创建新的 String

许多 Vec 可用操作同样适用于 String,因为实际上,String 是围绕字节向量封装的一层包装器,并增加了一些额外保证、限制和能力。例如,用来创建实例的新函数 new,在 Vec 和 String 中工作方式相同,如清单 8-11 所示:

let mut s = String::new();

清单 8-11:创建一个新的空白 String
这一行代码创建了一个名为 s 的新空串,我们可以往里加载数据。通常,我们会有一些初始数据想放入该串,为此可以使用 to_string 方法,该方法适用于任何实现了 Display trait 的类型,比如字符字面量。如清单 8-12 所示:

let data = "initial contents";
let s = data.to_string();
// 此方法也能直接作用于字面量:
let s = "initial contents".to_string();

清单 8-12:使用 to_string 方法从字符字面量创建一个 String
这段代码生成包含初始内容的 string。

我们也可以使用函数 String::from 从字符字面量创建一个 String。如清单 8-13,其代码等价于使用 to_string 的版本:

let s = String::from("initial contents");

清单 8-13:利用 String::from 函数从字符字面量构造 String
由于 strings 用途广泛,可以通过很多通用 API 操作它们,给开发者提供大量选择。有些看似冗余,但各有其用途!这里,String::fromto_string 功能相同,你选哪个取决于风格与可读性。

记住,strings 是 UTF-8 编码,所以你可以包含任何正确编码的数据,如下例(见清单 8-14)所示:

let hello = String::from("السلام عليكم");
let hello = String::from("Dobrý den");
let hello = String::from("Hello");
let hello = String::from("שלום");
let hello = String::from("नमस्ते");
let hello = String::from("こんにちは");
let hello = String::from("안녕하세요");
let hello = String::from("你好");
let hello = String::from("Olá");let hello= String :: from ("Здравствуйте") ;lethello=Str ing :: from ("Hola") ;

清单 8-14 : 将不同语言问候语存入 strings
以上均为有效的 string 值。

更新一个 string

像 Vec 一样,一个 string 可以增长并改变其内容,只要你往里面推送更多数据。此外,还可以方便地用 + 运算符或 format! 宏连接多个 string 值。

通过 push_str 和 push 向 string 添加内容

我们可以调用 push_str 方法追加一个 string 切片,从而扩展已有 string,如下(见清单8-15):

    let mut s = String::from("foo");s.push_str("bar");

清 单8-15 : 使用push_str方法把string slice添加到string后

执行完上述两行后,s就成了foobar 。push_str 接受参数为string slice ,因为不一定需要取得参数所有权。例如,下面代码(见 清 单8-16)希望追加完之后还能继续访问s2 :

   let mut s1 = String::from("foo");let s2 = "bar";s1.push_str(s2);println!("s2 is {s2}");

清 单8-16 : 在追加后仍然能访问原来的slice

如果push_str取得了s2'所有权,那么最后一行打印s2’值就无法成功。但实际运行结果符合预期!

push 方法接受的是 char 类型参数,将该字符添加至末尾。如以下例子(见 清 单8-17 )把’l’加到了 ‘lo’:

   let mut s = String::from("lo");s.push('l');

清 单8-17 : 用push给string添加1个char

结果s == lol.

+运算符或format!宏进行拼接

经常需要合并两个已存在 strings,一种做法是 + 运算符,例如(见 清 单8-18 ):

   let s1 = String::from("Hello, ");let s2 = String::from("world!");let s3 = s1 + &s2; // 注意:此处移动了's1',不能再用了。

清 单8-18 : 利用+运算符合并两个Strings得到新值

变量s3== Hello, world!. 为什么`s1’失效?为什么传递的是&s2?这是因为 + 调用了 add 函数,其签名大致如下:

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

标准库里add定义较复杂,这里简化说明。当调用add时,第2个参数必须是&str,不支持两个完整 Strings 相加;但&s2 实际上是 &St ring ,为何编译没错呢?

答案是在调用 add 时发生了解引用强制转换(deref coercion),即把 &S tring 转换成对应范围内(&[…]) 的&st r . 我们将在第15章详细介绍deref coercion 。此外,由于是引用传参,没有转移所有权,所以$s2依旧有效。而 self 参数没有 &, 表明拥有self所有权,也就是说 $S1 被移动进add 调用了。因此表达式看似复制两次其实只移动一次,更高效无冗余拷贝.

当需拼接多个 strings 时,+ 会让表达式变得难懂,例如:

   let s1 = String::from("tic");let s2 = String::from("tac");let s3 = String::from("toe");let s = s1 + "-" + &s2 + "-" + &s3;

此时$s == tic-tac-toe. 多重+号及双引号使阅读困难,可改用 format! 宏代替:

   let s1 = String::from("tic");let s2 = String::from("tac");let s3 = String::from("toe");let s = format!("{s1}-{s2}-{s3}");

同样赋值$tic-tac-toe. format! 类似println!, 不过不是输出屏幕,而返回含格式化内容的新 String 。且内部采用引用,不转移参数所有权,使代码更易读、更安全.

字符串索引

在许多其他编程语言中,通过索引访问字符串中的单个字符是一种有效且常见的操作。然而,如果你尝试在 Rust 中使用索引语法访问 String 的部分内容,会得到一个错误。请看清单 8-19 中的无效代码。

let s1 = String::from("hi");
let h = s1[0];

清单 8-19:尝试对 String 使用索引语法
这段代码会产生如下错误:

$ cargo runCompiling collections v0.1.0 (file:///projects/collections)
error[E0277]: the type `str` cannot be indexed by `{integer}`--> src/main.rs:3:16|
3 |     let h = s1[0];|                ^ string indices are ranges of `usize`|= note: you can use `.chars().nth()` or `.bytes().nth()`for more information, see chapter 8 in The Book: <https://doc.rust-lang.org/book/ch08-02-strings.html#indexing-into-strings>= help: the trait `SliceIndex<str>` is not implemented for `{integer}`but trait `SliceIndex<[_]>` is implemented for `usize`= help: for that trait implementation, expected `[_]`, found `str`= note: required for `String` to implement `Index<{integer}>`For more information about this error, try `rustc --explain E0277`.
error: could not compile `collections` (bin "collections") due to 1 previous error

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

内部表示

String 是 Vec<u8> 的封装。让我们看看之前 UTF-8 编码正确的示例字符串(清单 8-14)中的一些例子。首先是:

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

此时,len 为4,意味着存储字符串 “Hola” 的向量长度为4字节。每个字母用 UTF-8 编码时占用一个字节。然而,下面这一行可能会让你感到惊讶(注意该字符串以大写西里尔字母 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 在 byte index=0 时拥有的数据。如果&“hi”[0] 是合法代码且返回的是原始字节,它将返回104而非’h’。

因此,为避免返回意外值并导致难以发现的 bug,Rust 干脆不允许编译这类代码,从开发初期就防止误解发生。

字节、标量值与图形簇!哎呀!

关于 UTF-8,还有一点需要注意的是,从 Rust 的角度来看,有三种相关方式来观察字符串:作为字节、标量值以及图形簇(最接近我们所谓“字符”的概念)。

例如印地语词 “नमस्ते”,它使用天城文书写,在计算机中被存储为 u8 向量,如下所示:

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

共18个字节,这是计算机最终如何保存这些数据。如果把它们视作 Unicode 标量值,也就是 Rust 中 char 类型,这些 bytes 对应于:

[‘न’, ‘म’, ‘स’, ‘्’, ‘त’, ‘े’]

这里有6个 char 值,但第四和第六不是独立意义上的“字符”:它们是不完整不能独立存在的变音符号。最后,如果从图形簇角度看,则相当于人们认知中的四个印地文字母组成词汇:

[“न”, “म”, “स्”, “ते”]

Rust 提供不同方法解释底层原始数据,使程序可以根据需求选择合适的人类语言处理方式。

另一个原因是不允许通过索引直接获取某一位置上的字符,是因为期望所有索引操作都能保证常数时间复杂度(O(1))。但对于 String 来说无法保证这一点,因为必须从开头遍历直到目标位置才能确定有多少有效字符。

切片 Strings

对 string 索引用途往往不好界定——到底应该返回什么类型?是单一 byte 值、char 字符、图形簇还是 string slice?如果确实需要基于范围创建切片,那么 Rust 要求更明确指定范围,而非简单数字下标,例如:

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

此处 s 是包含前四个 bytes 的 &str 切片。如前所述,每两个 bytes 表示一个 character,因此s 包含 “Зд”。

若尝试只截取部分 character 所占 bytes,比如 &hello[0…1] ,则运行时会 panic,就像 vector 越界一样报错:

$ cargo run Compiling collections v0.1.0 (file:///projects/collections) Finished dev profile [unoptimized + debuginfo] target(s) in 0.43s Running target/debug/collections thread ‘main’ panicked at src/main.rs:4:19:
byte index 1 is not a char boundary; it is inside ‘З’ (bytes 0..2) of ‘Здравствуйте’
note : run with RUST_BACKTRACE=1 environment variable to display a backtrace 

因此,在使用范围创建 string slices 时务必小心,否则程序可能崩溃。

迭代 Strings 方法

处理 strings 最好明确自己想要的是 characters 或者 bytes 。针对单独 Unicode 标量,可以调用 chars 方法。“Зд”.chars() 会分离出两个 char 类型元素,可迭代访问:

for c in "Зд".chars() {println!("{c}");
}

输出结果为:

З
д

另外也可调用 bytes 返回每个位元组(byte),适用于特定场景:

for b in "Зд".bytes() {println!("{b}");
}

输出四个位元组(bytes):

208
151
208
180

但请记住,有效 Unicode 标量可能由多个 byte 构成,不可简单按 byte 操作替代 char 。

由于提取如天城文等复杂脚本中的 grapheme clusters 较困难,此功能未纳入标准库。如需此功能,可查找 crates.io 上相关第三方库实现。

Strings 并非那么简单
总结来说,string 很复杂,不同语言对此做出了不同设计权衡。Rust 默认要求正确处理 String 数据,这使得程序员必须提前认真考虑如何处理 UTF-8 数据。这虽然暴露了更多细节,却避免了后续因非 ASCII 字符带来的潜在错误风险。

好消息是标准库提供大量基于 String 和 &str 类型的方法帮助正确应对这些复杂情况,比如 contains 用于搜索,以及 replace 用于替换子串等,非常实用,请务必查看官方文档了解详情。

接下来,让我们转向稍微简单一点的话题:哈希映射(hash maps)!

http://www.lryc.cn/news/606071.html

相关文章:

  • RAG:让AI更聪明的“外接大脑“ | AI小知识
  • ECMAScript2023(ES14)新特性
  • C# 基于halcon的视觉工作流-章27-带色中线
  • HTM 5 的离线储存的使用和原理
  • JavaEE初阶1.0
  • 认知绞肉机:个体实践视域下认知暴力与元认知升维的活体实验研究
  • 今日做题练习
  • 记录自己使用gitee和jenkins
  • PHP 核心特性全解析:从实战技巧到高级应用(2)
  • 按键精灵iOS工具元素命令SetText:自动化输入的终极解决方案
  • .NET Core部署服务器
  • Linux网络-------3.应⽤层协议HTTP
  • Java 大视界 -- Java 大数据在智能交通公交客流预测与线路优化中的深度实践(15 城验证,年省 2.1 亿)(373)
  • 快速搭建Node.js服务指南
  • 前端核心技术Node.js(四)——express框架
  • 8,FreeRTOS时间片调度
  • RPA-重塑企业自动化流程的智能引擎
  • 《能碳宝》AI辅助开发系统方案
  • 免费语音识别(ASR)服务深度指南​
  • 深入解析域名并发请求限制与HTTP/2多路复用技术
  • 电脑远程关机的重要性
  • vue3+arcgisAPI4示例:轨迹点模拟移动(附源码下载)
  • 实战教程 ---- Nginx结合Lua实现WAF拦截并可视化配置教程框架
  • 融合数字孪生的智慧能源光伏场站检测系统应用解析
  • 生产管理升级:盘古IMS MES解锁全链路可控可溯,激活制造效率
  • 从 MySQL 迁移到 TiDB:使用 SQL-Replay 工具进行真实线上流量回放测试 SOP
  • 【NLP舆情分析】基于python微博舆情分析可视化系统(flask+pandas+echarts) 视频教程 - 微博评论数据可视化分析-点赞区间折线图实现
  • 保姆级别IDEA关联数据库方式、在IDEA中进行数据库的可视化操作(包含图解过程)
  • 技术速递|GitHub Copilot for Eclipse 迈出重要一步
  • SQL极简函数实战:巧用GREATEST()与LEAST()实现智能数据截断