Rust 语言从入门到实战 唐刚--读书笔记04

基础篇 (11讲)

04|字符串:对号入座,字符串其实没那么可怕!

字符串,最频繁、太常见:

        如 Web 开发、解析器等,主要就是处理字符串。

可怕的字符串?

 Rust 里字符串:

String, &String, 
str, &str, &'static str
[u8], &[u8], &[u8; N], Vec
as_str(), as_bytes()
OsStr, OsString
Path, PathBuf
CStr, CString

Rust 字符串的复杂性:

Rust 语言从入门到实战 唐刚--读书笔记04_第1张图片

C 的字符串叫 char *,是统一的抽象,简洁,代价是丢失了很多额外的信息

        抽象是用来解决现实问题建模的工具。

Rust 把字符串在各种场景下的使用给模型化、抽象化了。比 C 的 char *,多了建模的过程,模型里多了很多额外的信息。

不同类型的字符串

fn main() {
  let s1: &'static str = "I am a superman."; 
  let s2: String = s1.to_string(); 
  let s3: &String = &s2;
  let s4: &str = &s2[..];
  let s5: &str = &s2[..6];
}

4 种不同类型的字符串表示。在内存中的结构图。

Rust 语言从入门到实战 唐刚--读书笔记04_第2张图片

s1:"I am a superman." (双引号括起来部分)字符串的字面量,存在静态数据区。s1 是指向静态数据区中字符串的切片引用,形式是 &'static str,这是静态数据区中的字符串的表示方法。

s2:执行 s1.to_string(),Rust 将静态数据区中的字符串字面量拷贝了一份到堆中,通过指向,s2 具有堆内存字符串的所有权,String 代表具有所有权的字符串

s3 是对 s2 的不可变引用,类型为 &String。

s4 是对 s2 的切片引用,类型是 &str。切片是一块连续内存的某种视图,提取目标对象的全部或一部分。s4 取的目标对象字符串的全部。

s5 是对 s2 的另一个切片引用,类型是 &str。与 s4 不同,s5 是 s2 的部分视图。是 "I am a" 这一部分。

详细解释:

  • String 是字符串的所有权形式,常在堆中分配。String 字符串的内容大小可以动态变化
  • str 是字符串的切片类型,常以切片引用 &str 形式出现,是字符串的视图的借用形式。
  • 字符串字面量默认存在静态数据区,贯穿程序运行的整个生命期,直到程序结束时才被释放。不需要某一个变量对其拥有所有权(也就是这个资源的分配责任)。只能拿到字符串字面量的借用形式 &'static str。 'static 表示这个引用贯穿整个程序的生命期,直到程序运行结束。
  • &String 仅是对 String 类型的字符串的普通引用
  • 对 String 做字符串切片操作,得到 &str。&str 是指向由 String 管理的内存资源的切片引用,是目标字符串资源的借用形式,不会再把字符串内容复制一份。

&str 既可引用堆中的字符串,也可引用静态数据区的字符串(&'static str 是 &str 的一种特殊形式)。内存本来就是一个线性空间,一个指针(引用是指针的一种)可指向线性空间中的任何地址。

&str 可转换为 String。

let s: String = "I am a superman.".to_string(); 
let a_slice: &str = &s[..];
let another_String: String = a_slice.to_string();

切片

切片(slice),是一段连续内存的一个视图(view),用 [T] 表示,T 为元素类型。视图可以是连续内存的全部或一部分。切片通过切片的引用来访问。

let s = String::from("abcdefg");
let s1 = &s[..];    // s1 内容是 "abcdefg"
let s2 = &s[0..4];  // s2 内容是 "abcd"
let s3 = &s[2..5];    // s3 内容是 "cde"

s 是堆内存中所有权型字符串类型。

s1 作为 s 的一个切片引用,也指向堆内存中那个字符串的头部,表示 s 的完整内容。

s2 与 s1 指向的堆内存地址是相同的,但内容不同,s2 是 "abcd", s1 是 "abcdefg"。

s3 是 s 的中间位置的一段切片引用,内容是 "cde"。s3 指向的地址与 s、s1、s2 不同。

Rust 语言从入门到实战 唐刚--读书笔记04_第3张图片

字符串切片引用,可以转换成所有权型字符串:

let s: &str = "I am a superman.";
let s1: String = String::from(s);  // 使用 String 的from构造器
let s2: String = s.to_string();    // 使用 to_string() 方法
let s3: String = s.to_owned();     // 使用 to_owned() 方法

[u8]、&[u8]、&[u8; N]、Vec

不直接与字符串相关,但具有类比性。

  • [u8] 是字节串切片,大小是可以动态变化的。
  • &[u8] 是对字节串切片的引用,即切片引用,与 &str 类似。
  • &[u8; N] 是对 u8 数组(其长度为 N)的引用。
  • Vec 是 u8 类型的动态数组。与 String 类似,是具有所有权的类型。

Vec 与 &[u8] 的关系:

let a_vec: Vec = vec![1,2,3,4,5,6,7,8];
// a_slice 是 [1,2,3,4,5]
let a_slice: &[u8] = &a_vec[0..5];   
// 用 .to_vec() 方法将切片转换成Vec
let another_vec = a_slice.to_vec();
// 或者用 .to_owned() 方法
let another_vec = a_slice.to_owned();

整理出一个对比表格。

Rust 语言从入门到实战 唐刚--读书笔记04_第4张图片

as_str()、as_bytes()、as_slice()

String类型上 as_str() 方法,返回 &str 类型。等价于 &a_string[..],包含完整字符串内容的切片。

let s = String::from("foo");
assert_eq!("foo", s.as_str());

String 类型上 as_bytes()方法,返回 &[u8] 类型。

let s = String::from("hello");
assert_eq!(&[104, 101, 108, 108, 111], s.as_bytes());

上面两个示例,显示这两个方法的不同之处。

猜想 &str 也可以转成 &[u8] ,查询标准库文档发现,用的正是同名方法。

let bytes = "bors".as_bytes();
assert_eq!(b"bors", bytes);

Vec 上 as_slice() 函数,与 String 上 as_str() 对应,把完整内容转换成切片引用 &[T],等价于 &a_vec[..]。

let a_vec = vec![1, 2, 3, 5, 8];
assert_eq!(&[1, 2, 3, 5, 8], a_vec.as_slice());

隐式引用类型转换

Rust 中 &String 与 &str 其实是不同的。这种细节的区分,会造成一些不方便,还比较常见:

fn foo(s: &String) {  
}

fn main() {
  let s = String::from("I am a superman.");
  foo(&s);
  let s1 = "I am a superman.";    
  foo(s1);                        
}

上例中,函数的参数类型定义成 &String。函数调用时,函数只接受 &String 类型的参数传入。一个字符串字面量变量,想传进 foo 函数中,不行。

error[E0308]: mismatched types
 --> src/main.rs:8:7
  |
8 |   foo(s1);                        // error on this line
  |   --- ^^ expected `&String`, found `&str`
  |   |
  |   arguments to this function are incorrect
  |
  = note: expected reference `&String`
             found reference `&str`

Rust 严格的一面。可以通过一些精妙的机制,实现一定程度上的灵活性:

fn foo(s: &str) {      // 只需要把这里的参数改为 &str 类型
}

fn main() {
  let s = String::from("I am a superman.");
  foo(&s);
  let s1 = "I am a superman.";    
  foo(s1);                        
}

把 foo 参数的类型由 &String 改为 &str,编译通过了。为什么?

Rust 中对 String 做引用操作时,若想把 &String 直接转换到 &str 类型,在代码中明确指定目标类型就可以了。

let s = String::from("I am a superman.");
let s1 = &s;
let s2: &str = &s;
  • s1 不指定具体类型,对所有权字符串 s 的引用操作,只转换成 &String 类型。
  • 若指定目标类型为 &str,那对所有权字符串 s 的引用操作,就进一步转换成了 &str 类型。

于是 foo() 函数中,只定义一种参数,就可以接收两种入参类型:&String 和 &str。这让函数的调用更符合直觉,使用更方便了。

具体是怎么做到的呢?知识点:Deref。

同理,可以作用在 String 上,也可以作用在 Vec 上 ,还可以作用在 Vec 上。

Rust 语言从入门到实战 唐刚--读书笔记04_第5张图片

示例:一个函数可以接受 &Vec 和 &[u32] 两种类型的传入。

fn foo(s: &[u32]) {
}

fn main() {
  let v: Vec = vec![1,2,3,4,5];
  foo(&v);
  let a_slice = v.as_slice();
  foo(a_slice);
}

字节串转换成字符串

as_bytes() 方法将字符串转换成 &[u8]。

相反的操作,把 &[u8] 转换成字符串。

Rust 的字符串实际是 UTF-8 序列,转换的过程与 UTF-8 编码相关。

  • String::from_utf8() 可把 Vec 转成 String,转换不一定成功(一个字节序列不一定是有效的 UTF-8 编码序列),返回 Result(Result会专题讲解),要自行做错误处理。
  • String::from_utf8_unchecked() 可把 Vec 转成 String。不检查字节序列是不是无效的 UTF-8 编码,直接返回 String 类型。这个函数是 unsafe 的,不推荐使用
  • -----
  • str::from_utf8() 可将 &[u8] 转成 &str ,返回 Result,要自行做错误处理。
  • str::from_utf8_unchecked() 可把 &[u8] 转成 &str,直接返回 &str 类型。这个函数是 unsafe 的,不推荐使用

注意 :from_utf8 系列函数,返回 Result。Rust 的严谨性要求对这种转换不成功的情况做严肃的自定义处理。其他语言,对于这种转换不成功的情况往往用一种内置的策略做处理,而无法自定义。

字符串切割成字符数组

&str 类型 chars() 函数,可把字符串转换为一个迭代器(迭代器是一种通用的抽象,用来按顺序安全迭代)。通过这个迭代器,就可以取出 char。用法:

fn main() {
    let s = String::from("中国你好");                                                                                 
    let char_vec: Vec = s.chars().collect();                                                                     
    println!("{:?}", char_vec); 
    
    for ch in s.chars() {
        println!("{:?}", ch); 
    }
}

输出:

['中', '国', '你', '好']
'中'
'国'
'你'
'好'

其他字符串相关类型

Path、PathBuf、OsStr、OsString、CStr、CString。是具体场景下的字符串,比 String 或 &str,包含了更多的特定场景的信息。

1、Path 类型,要处理跨平台的目录分隔符(Unix 是 /,Windows 是\),以及一些其他信息。而 PathBuf 与 Path 的区别就对应于 String 与 str 的区别。

2、OsStr 的存在:各个操作系统平台上的原生字符串定义是不同的。

  • Unix 系统,原生字符串是任意非 0 字节序列,常解释为 UTF-8 编码;
  • Windows 上,原生字符串为任意非 0 字节 16 位序列,解释为 UTF-16 编码序列。
  • Rust 的标准 str 定义和它们都不同,是一个可以包含 0 这个字节的严格 UTF-8 编码序列。

        开发平台相关的应用,往往需要处理这种类型转换的细节,有了 OsStr 类型。而 OsString 与 OsStr 的关系对应于 String 与 str 的关系。

3、CStr 是 C 语言风格的字符串,字符串以 0 这个字节作结束符,在字符串中不能包含 0。Rust 要无缝集成 C 的能力。所以这些类型出现在 Rust 中就很合理了。而 CString 与 CStr 的关系就对应于 String 与 str 的关系。

这些平台细节的处理相当繁琐和专业,Rust 把已处理好这些细节的类型提供给我们,我们直接使用就好了。

理解了这一点,还觉得 C 语言中唯一的 char * 是更好的设计吗?

需要理解 Rust 中为什么存在这些类型,这些类型之间的关系。

Parse 方法

str 的 parse() 方法非常强大,可从字符串转换到任意 Rust 类型,只要这个类型实现了 FromStr 这个 Trait(Trait 是 Rust 中一个极其重要的概念)即可。

把字符串解析成 Rust 类型(有不成功的可能),返回 Result 。

fn main() {
    let a = "10".parse::();
    let aa: u32 = "10".parse().unwrap(); // 这种写法也很常见
    println!("{:?}", a);
    
    let a = "10".parse::();
    println!("{:?}", a);
    
    let a = "4.2".parse::();
    println!("{:?}", a);
    
    let a = "true".parse::();
    println!("{:?}", a);

    let a = "a".parse::();
    println!("{:?}", a);
    
    let a = "192.168.1.100".parse::();
    println!("{:?}", a);
}

可以看看哪些标准库类型已实现了 FromStr trait

parse() 函数相当于 Rust 语言内置的统一的解析器接口,若自己实现的类型要与字符串互相转换,要考虑实现这个接口 --> Rust 地道风格。

对于更复杂和更通用的与字符串转换的场景,更倾向于序列化和反序列化的方案。Rust 生态中有标准的方案——serde(序列化框架),支持各种数据格式协议,功能非常强大、统一。

小结

相比 C 中的字符串,Rust 把字符串按场景划分成了不同的类型,每种类型都包含有不同的额外信息。通过将研究目标(字符串)按场景类型化,在代码中加入了更多的信息,让编译器做更多的校验和推导的事情,确保程序的正确性,并尽可能做性能优化。

新的概念,如迭代器、Trait 等,

主要任务:

  1. 熟悉 Rust 语言中的字符串的各种类型形式,以它们之间的区别。
  2. 知道 Rust 语言中字符串相关类型的基本转换方式。
  3. 体会地道的 Rust 代码风格及对称性。

字符串在 Rust 代码中使用广泛,贯穿整个课程。一定多加练习,牢牢掌握字符串相关类型在不同场景下的转换以及一些常用的方法。

Rust 语言从入门到实战 唐刚--读书笔记04_第6张图片

思考题

chars 函数是定义在 str 上的,为什么 String 类型能直接调用 str 上定义的方法?实际上 str 上的所有方法,String 都能调用,请问这是为什么呢?

答:

1.String类型为struct类型,实现了Deref特征。

2.当String类型调用chars方法时,编译器会检查String类型是否实现了chars方法,检查项包括self,&self,&mut self

3.如果都没有实现chars方法,编译器则调用deref方法解引用(智能指针),得到str,此时编译器才会调用chars方法,也就是可以调用str实现的所有方法

在 Rust 中,String 是一个可变的字符串类型,是用结构体定义的,而且实现了 Deref trait。str 是一个不可变的字符串切片类型。当调用一个 str 上的方法时,实际上就是通过 Deref 的自动转换机制(解引用),将 String 转换为对应的 str 切片,从而可以调用 str 上的方法。

什么时候用to_owned(),什么时候用to_string()呢?

答: to_owned() 是更通用的,目的就是在只能拿到引用的情况下获得所有权(通过克隆一份资源)。to_string() 就只是转成字符串而已,这两个用法重叠,但是不完全相同。

你可能感兴趣的:(Rust,语言从入门到实战,唐刚,学习笔记,rust,开发语言,后端)