Rust字符串范例全剖析

立山  金牌会员 | 2024-9-25 15:04:42 | 显示全部楼层 | 阅读模式
打印 上一主题 下一主题

主题 909|帖子 909|积分 2727

字符串是每种编程语言都绕不开的范例,
不过,在Rust中,你会看到远比其他语言更加丰富多样的字符串范例。
如下图:

为什么Rust中需要这么多种表示字符串的范例呢?
初学Rust时,可能无法明白为什么要这样设计?为什么要给使用字符串带来这么多不必要的复杂性?
其实,Rust中对于字符串的设计,优先考虑的是安全高效灵活
以是在易用性方面,感觉没有其他语言(比如python,golang)那么易于明白和把握。
本文实验表明Rust中的所有不同的字符串范例,以及它们各自的特点。
希望能让大家更好的明白Rust为了安全和发挥最大性能的同时,是怎样处理字符串的。
1. 机器中的字符串

我们代码中的字符串或者数字,存储在机器中,都是二进制,也就是0和1构成的序列。

程序将二进制数据转换为人类可读的字符串 需要两个关键信息:

  • 字符编码
  • 字符串长度
常见的编码有ASCII,UTF-8等等,编码就是二进制序列对应的字符,
比如,ASCII是8位二进制对应一个字符,以是它最多只能表示256种不同的字符。
而UTF-8可以使用8位~32位二进制来表示一个字符,这意味着它可以编码超过一百万个字符,
包括天下上的每种语言和各种表情符号等复杂字符。
通过字符编码,我们可以将二进制和字符互相转换,
再通过字符串长度信息,我们将内存中的二进制转换为字符串时,就能知道何时克制。
Rust中的字符串,统一接纳UTF-8编码,下面逐一介绍各种字符串范例及其使用场景。
2. String 和 &str

String和&str是Rust中使用最多的两种字符串范例,也是在使用中轻易肴杂的两种范例。
String是分配在堆上的,可增长的UTF-8字符串,
它拥有底层的数据,并且在超出其界说的范围被自动清算释放。
  1. let my_string = String::from("databook");
  2. println!(
  3.     "pointer: {:p}, length: {}, capacity: {}",
  4.     &my_string,
  5.     my_string.len(),
  6.     my_string.capacity()
  7. );
复制代码

对于一个String,主要部门有3个:

  • Pointer:指向堆内存中字符串的起始位置
  • Length:有效字符串的长度
  • Capacity:字符串my_string统共占用的空间
留意这里Length和Capacity的区别,Length是my_string中有效字符的长度,也就是字符串实际的长度;
Capacity表示体系为my_string分配的内存空间,一般来说,Capacity >= Length。
通常不需要直接处理Capacity,但它的存在对于编写高效且资源敏感的Rust代码时很重要。
特殊是,当你知道即将向String添加大量内容时,可能会事先手动保留足够的Capacity以克制多次内存重新分配。
&str则是一个字符串的切片,它表示一个连续的字符序列,
它是一个借用范例,并不拥有字符串数据,只包含指向切片开头的指针和切片长度。
  1. let my_str: &str = "databook";
  2. println!("pointer: {:p}, length: {}", &my_str, my_str.len());
复制代码

留意,&str没有Capacity方法,因为它只是一个借用,内容不可能增加。
最后,对于String和&str,使用时建议:

  • 在运行时动态创建或修改字符串数据时,请使用 String
  • 读取或分析字符串数据而不对其进行更改时,请使用 &str
3. Vec[u8] 和 &[u8]

这两种情势是将字符串表示位字节的情势,此中Vec[u8]是字节向量,&[u8]是字节切片。
它们只是将字符串中的各个字符转换成字节情势。
as_bytes方法可将&str转换为&[u8];
into_bytes方法可将String转换为Vec。
  1. let my_str: &str = "databook";
  2. let my_string = String::from("databook");
  3. let s: &[u8] = my_str.as_bytes();
  4. let ss: Vec<u8> = my_string.into_bytes();
  5. println!("s: {:?}", s);
  6. println!("ss: {:?}", ss);
  7. /* 运行结果
  8. s: [100, 97, 116, 97, 98, 111, 111, 107]
  9. ss: [100, 97, 116, 97, 98, 111, 111, 107]
  10. */
复制代码

在UTF-8编码中,每个英文字母对应1个字节,而一个中文汉字对应3个字节
  1. let my_str: &str = "中文";
  2. let my_string = String::from("中文");
  3. let s: &[u8] = my_str.as_bytes();
  4. let ss: Vec<u8> = my_string.into_bytes();
  5. println!("s: {:?}", s);
  6. println!("ss: {:?}", ss);
  7. /* 运行结果
  8. s: [228, 184, 173, 230, 150, 135]
  9. ss: [228, 184, 173, 230, 150, 135]
  10. */
复制代码

Vec[u8]和&[u8]以字节的情势存储字符串,不用关心字符串的具体编码,
这在网络中传输二进制文件或者数据包时非常有效,可以有效每次传输多少个字节。
4. str 系列

str范例自己是不能直接使用的,因为它的大小在编译期无法确定,不符合Rust的安全规则。
但是,它可以与其他具有特殊用途的指针范例一起使用。
4.1. Box

如果需要一个字符串切片的所有权(&str是借用的,没有所有权),那么可以使用Box智能指针。
当你想要冻结字符串以防止进一步修改或通过删除额外容量来节省内存时,它非常有效。
比如,下面的代码,我们将一个String转换为Box,
这样,可以确保它不会在其他地方被修改,也可以删除它,因为Box拥有字符串的所有权。
  1. let my_string = String::from("databook");
  2. let my_box_str = my_string.into_boxed_str();
  3. println!("{}", my_box_str);
  4. // 这一步会报错,因为所有权已经转移
  5. // 这是 Box<str> 和 &str 的区别
  6. // println!("{}", my_string);
复制代码
4.2. Rc

当你想要在多个地方共享一个不可变的字符串的所有权,但是又不克隆实际的字符串数据时,
可以实验使用Rc智能指针。
比如,我们有一个非常大的文本,想在多个地方使用,又不想复制多份占用内存,可以用Rc。
  1. let my_str: &str = "very long text ....";
  2. let rc_str1: Rc<str> = Rc::from(my_str);
  3. let rc_str2 = Rc::clone(&rc_str1);
  4. let rc_str3 = Rc::clone(&rc_str1);
  5. println!("rc_str1: {}", rc_str1);
  6. println!("rc_str2: {}", rc_str2);
  7. println!("rc_str3: {}", rc_str3);
  8. /* 运行结果
  9. rc_str1: very long text ....
  10. rc_str2: very long text ....
  11. rc_str3: very long text ....
  12. */
复制代码
这样,在不实际克隆字符串数据的情况下,让多个变量拥有其所有权。
4.3. Arc

Arc与Rc的功能类似,主要的区别在于Arc是线程安全的。
如果在多线程情况下,请使用Arc。
  1. let my_str: &str = "very long text ....";
  2. let arc_str: Arc<str> = Arc::from(my_str);
  3. let mut threads = vec![];
  4. let mut cnt = 0;
  5. while cnt < 5 {
  6.     let s = Arc::clone(&arc_str);
  7.     let t = thread::spawn(move || {
  8.         println!("thread-{}: {}", cnt, s);
  9.     });
  10.     threads.push(t);
  11.     cnt += 1;
  12. }
  13. for t in threads {
  14.     t.join().unwrap();
  15. }
  16. /* 运行结果
  17. thread-0: very long text ....
  18. thread-3: very long text ....
  19. thread-2: very long text ....
  20. thread-1: very long text ....
  21. thread-4: very long text ....
  22. */
复制代码
上面的代码中,在5个线程中共享了字符串数据。
上面运行结果中,线程顺序是不固定的,多执行几遍会有不一样的顺序。
4.4. Cow

Cow是Copy-on-Write(写入时复制)的缩写,
当你需要实现一个功能,根据字符串的内容来决定是否需要修改它,使用Cow就很符合。
比如,过滤敏感词汇时,我们把敏感词汇替换成xx。
  1. fn filter_words(input: &str) -> Cow<str> {
  2.     if input.contains("sb") {
  3.         let output = input.replace("sb", "xx");
  4.         return Cow::Owned(output);
  5.     }
  6.     Cow::Borrowed(input)
  7. }
复制代码
当输入字符串input中含有敏感词sb时,会重新分配内存,生成新字符串;
否则直接使用原字符串,提高内存效率。
5. CStr 和 CString

CStr和CString是与C语言交互时用于处理字符串的两种范例。
CStr用于在Rust中安全地访问由C语言分配的字符串;
而CString用于在Rust中创建和管理可以安全传递给C语言函数的字符串。
C风格的字符串与Rust中的字符串实现方式不一样,
比如,C语言中的字符串都是以null字符\0末端的字节数组,这点就与Rust很不一样。
以是Rust单独封装了这两种范例(CStr和CString),可以安全的与C语言进行字符串交互,从而实现与现有的C语言库和API无缝集成。
6. OsStr 和 OsString

OsStr 和 OsString 是用于处理与操作体系兼容的字符串范例。
主要用于需要与操作体系API进行交互的场景,这些API一般特定于平台的字符串编码(比如Windows上的UTF-16,以及大多数Unix-like体系上的UTF-8)。
OsStr 和OsString 也相当于str和String的关系,以是OsStr 一般不直接在代码中使用,
使用比较多的是&OsStr和OsString。
这两个范例一般用于读取/写入操作体系情况变量或者与体系API交互时,帮助我们确保字符串以精确的格式传递。
7. Path 和 PathBuf

这两个范例看名字似乎和字符串关系不大,实际上它们是专门用来处理文件路径字符串的。
在不同的文件体系中,对于文件路径的格式,路径中答应使用的字符都不一样,比如,windows体系中文件路径甚至不区分大小写。
使用Path 和 PathBuf,我们编码时就不用分散精力去关心具体使用的是哪种文件体系。
Path和PathBuf的主要区别在于可变性和所有权,
如果需要频繁读取和查询路径信息而不修改它,Path是一个好选择;
如果需要动态构建或修改路径内容,PathBuf则更加符合。
8. 总结

总之,Rust中字符串范例之以是多,是因为根据不同的用途对字符串范例做了分类。
这也是为了处理不同的应用场景时让程序发挥最大的性能,毕竟,安全高性能一直是Rust最大的卖点。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

立山

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表