如何理解Rust中的字符串(二)

423 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第16天,点击查看活动详情

在上一篇文章中介绍了Rust字符串中的&str(切片),这篇文章将介绍String类型。Rust中的String类型比其他语言的String更加复杂。

Rust中的String类型

通常说的字符串是哪一个?

Rust中所说的字符串为&str和String,而不是其中的一种

其他类型的字符串

Rust的标准库还包含了很多其他的字符串类型,如:OsString、OsStr、CString、Cstr

上面的类型有String结尾的,有Str结尾的,这两者的区别在于String结尾的为可获得所有权的类型,而Str结尾的为可借用的。

此外,某些Libaray create(第三方库)针对存储字符串可提供更多的选项

创建一个新的字符串(String)

  • 很多Vec<T>的操作都可作用于String
  • String::new()函数
let mut s = String::new();

通常情况下,我们声明一个字符串都是带有初始值的。我们可以使用to_string()方法,可用于实现了Display trait的类型,包括字符串字面值

let data = "this is a data";
let s = data.to_string();
let s1 = "ssss ss".to_string();

此外,我们还可以使用String::from()函数来创建带初始值的字符串:

let s = String::from("ssss yige sss");

更新String

  • push_str()方法:把一个字符串切片附加到String:
let mut s = String::from("foo");
s.push_str("string is pushed");
println!("{}", s); //foostring is pushed

let mut s1 = "first ".to_string();
let s2 = String::from("ss");
s1.push_str(&s2);
println!("{}", s2); //ss

我们查看push_str的签名:pub fn push_str(&mut self, string: &str)传入的是一个引用,所以我们将s2传入s1后任然可以使用。

  • push()方法:把单个字符附加到String:
let mut x = "sss".to_string();
x.push('c');
println!("{}", x); //sssc
  • 使用+拼接字符串(前面为String类型,后面为一个&str)
let s1 = String::from("aa");
let s2 = String::from("bb");
let s3 = s1 + &s2;
println!("{}", s3); //aabb
//println!("{}", s1); //报错
println!("{}", s2); //bb

其中,s2为String类型,而&s2类型为&str,这是因为Rust会使用解引用进行类型的强制转换(deref coercion)

+类似于这个函数签名:add(self,s:&str)->String

如果我们连接多个字符串,使用+要这样写

let s1 = String::from("aa");
let s2 = String::from("bb");
let s3 = String::from("cc");
let s4 = s1 + "-" + &s2 + "-" + &s3;
println!("{}", s4); //aa-bb-cc

但我们有更灵活快速的方法:

  • format!宏:连接多个字符串:
let s1 = String::from("aa");
let s2 = String::from("bb");
let s3 = String::from("cc");
let s4 = format!("{}-{}-{}", s1, s2, s3);
println!("{}", s4); //aa-bb-cc
println!("{}", s1); //aa
println!("{}", s2); //bb
println!("{}", s3); //cc

我们可以发现format!println!有些相似,不过前者是返回一个String类型的值,而后者为打印值。并且,重要的是,format!不会获取任何变量的所有权。

对String按照索引的形式进行访问

按索引语法访问String的某部分,会报错:

let s1 = String::from("aa");
s1[1]; //the type String cannot be indexed by {integer}

所以,rust不支持索引语法访问String

内部表示

string是对Vec<u8>的包装

  • len()方法
let s1 = String::from("aabbcc");
let len = s1.len();
println!("{}", len); //6

但并不是说len()方法的返回值为字符串的长度:

let s1 = String::from("Этострока"); //9个字符(俄文)
let len = s1.len();
println!("{}", len); //18

let s2 = String::from("哈哈哈哈"); //4个字符
let len1 = s2.len();
println!("{}", len1); //12

在俄语中Unicode标量值占两字节,中文占3个。

所以说如果使用索引,不一定能取到一个合法的字符,比如Э,它的Unicode标量值为:208,151。如果能够使用索引访问字符串,s1[0]将会取到208,这并不是一个合法的字符(就算合法,也不是用户所想要的),所以Rust直接不允许这种操作。

字节、标量值、字形簇(Bytes,Scalar Values,Grapheme Clusters)

Rust有三种看待字符串的方式:

  • 字节
  • 标量值
  • 字形簇(最接近所谓的“字母”)

我们现在来使用对应的方法来对String进行遍历

字节:

let s1 = String::from("Это");
for b in s1.bytes() {
    println!("{}", b);
}
//208
// 173
// 209
// 130
// 208
// 190

Unicode标量值:

let s1 = String::from("Это");
for c in s1.chars() {
    println!("{}", c);
}
// Э
// т
// о

字形簇(最接近所谓的“字母”),实现比较复杂,标准库中没有提供这个功能,可以安装第三方库。

Rust不允许对String类型进行索引的最后一个原因:

  • 索引操作应消耗一个常量时间(O(1))
  • 而String无法保证:需要遍历所有内容,来确定有多少个合法的字符

切割String

可以使用[]和一个范围来创建字符串的切片:

let s1 = String::from("Этострока");
let s = &s1[4..8];//参数为字节的位置
println!("{}", s); //ос
  • 必须谨慎使用
  • 如果切割时跨越了字符边界,程序就会panic:

(b1,b2),(b3, b4),(b5,b6),(b7,b8)如果从b3,b4之间切割,程序将会panic

Rust中的String并不简单

Rust选择将正确处理String数据作为所有Rust程序的默认行为,程序员必须在处理UTF-8数据之前投入更多的精力

不过这样的好处也是明显的:可防止在开发后期处理涉及非ASCII字符的错误。