什么是字符串(String)?
Rust在核心语言中只有一种字符串类型,即字符串切片str,通常以借用形式&str看到。
String类型是Rust的标准库提供的,而不是编码为核心语言,它是一种可增长、可变、可拥有的以UTF-8形式编码的字符串类型。 在Rust中引用"strings"时,它们通常指的是String和字符串切片&str类型,但strings不仅仅是这两种类型。但两种类型在Rust的标准库中使用最多,并且String和字符串切片&str都是UTF-8编码的。
Rust的标准库还包括许多其他字符串类型,例如OsString,OsStr,CString和CStr。 图书馆包装箱(Library crates)可以提供更多用于存储字符串数据的选项。为啥这些名称如何都以String或Str结尾? 因为它们指的是拥有和借用的变体,就像String和str类型一样。例如,这些字符串类型可以用不同的编码存储文本,或以不同的方式在内存中表示。
创建一个新的字符串
fn main() {
let mut s = String::new(); //创建一个新的空字符串名为s,之后就可以使用s。
}
通常,我们会使用一些初始数据作为字符串的开头。 为此,我们使用to_string方法,该方法可在实现Display特征的任何类型上使用,就像字符串文字一样。如下所示两个效果完全相同的示例:
//此代码创建一个包含初始内容的字符串。
fn main() {
let data = "初始内容";
let s = data.to_string();
// 该方法也可以直接处理文字:
let s = "初始内容".to_string();
}
我们还可以使用函数String::from从字符串文字创建字符串。效果等同于使用to_string:
fn main() {
let s = String::from("初始内容");
}
以为字符串是UTF-8编码的,所以我们可以在其中包含任何正确编码的数据(不管它长啥样),如下所示:
fn main() {
let hello = String::from("你好");
let hello = String::from("Hello");
let hello = String::from("السلام عليكم");
let hello = String::from("Dobrý den");
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("Здравствуйте");
let hello = String::from("Hola");
}
更新字符串
如果将更多数据推入字符串中,则字符串的大小会增加,其内容也会发生变化,就像Vec <T>的内容一样。另外,我们可以方便地使用+运算符或格式!
使用push_str和push附加到字符串
fn main() {
let mut s1 = String::from("foo");
let s2 = "bar";
s1.push_str(s2);
println!("s2 is {}", s2);
}
//如果运行的话可以正常打印出结果:s2 is bar。所以push_str方法不会取得所有权
push方法将单个字符作为参数,并将其添加到String。如下示例显示了使用push方法将字母l添加到String的代码:
fn main() {
let mut s = String::from("lo");
s.push('l');
}
注意使用push方法的时候必须使用单引号,如果使用了双引号的话会出现错误:
|
4 | s1.push("l");
| ^^^ expected `char`, found `&str`
与+运算符或格式串联——宏
通常,需要合并两个现有字符串。 一种方法是使用+运算符,下所示:
fn main() {
let s1 = String::from("Hello, ");
let s2 = String::from("world!");
let s3 = s1 + &s2; // 注意s1已移至此处,无法在之后使用
println!("s1 + &s2 = {}", s3);
}
D:\learn\cargo_learn>cargo run
Compiling cargo_learn v0.1.0 (D:\learn\cargo_learn)
Finished dev [unoptimized + debuginfo] target(s) in 0.60s
Running `target\debug\cargo_learn.exe`
s1 + &s2 = Hello, world!
+运算符使用add方法,其签名如下所示:fn add(self, s: &str) -> String {
通过上例我们可以看到我们可以使用+号运算获取一个新的字符串,但是如果加号比较多的情况下会很难看到发生了什么。 对于更复杂的字符串组合,我们可以使用format!
宏:
fn main() {
let s1 = String::from("你");
let s2 = String::from("好");
let s3 = String::from("啊!");
let s = format!("{}-{}-{}", s1, s2, s3);
println!("{}", s1);
println!("{}", s)
}
D:\learn\cargo_learn>cargo run
Compiling cargo_learn v0.1.0 (D:\learn\cargo_learn)
Finished dev [unoptimized + debuginfo] target(s) in 0.59s
Running `target\debug\cargo_learn.exe`
你
你-好-啊!
可以看到使用format!
只是参考借阅变量,并不会获取变量的所有权
索引到字符串
我们是否可以像js中获取js某一个片段的方式一样在rust中使用呢,比如:
fn main() {
let s1 = String::from("hello");
let h = s1[0];
println!("{}", h);
}
答案是不可以的:
D:\learn\cargo_learn>cargo run
Compiling cargo_learn v0.1.0 (D:\learn\cargo_learn)
error[E0277]: the type `std::string::String` cannot be indexed by `{integer}`
--> src\main.rs:3:13
|
3 | let h = s1[0];
| ^^^^^ `std::string::String` cannot be indexed by `{integer}`
|
= help: the trait `std::ops::Index<{integer}>` is not implemented for `std::string::String`
error: aborting due to previous error
For more information about this error, try `rustc --explain E0277`.
error: could not compile `cargo_learn`.
To learn more, run the command again with --verbose.
为什么不可以?先来看看Rust是如何在内存中存储字符串的。
内部代表
字符串是Vec<u8>的包装。先看一下经过正确编码的UTF-8示例字符串:let hello = String::from("Hola");
在这种情况下,len将为4,这意味着存储字符串“ ”Hola的向量的长度为4个字节。 以UTF-8编码时,每个字母占用1个字节。 但要是这样呢?let hello = String::from("Здравствуйте"); //(请注意,此字符串以西里尔字母大写Ze开头,而不是阿拉伯数字3。)
当询问该字符串有多长时,我们可能会说12。但是,Rust的答案是24:这是在UTF-8中编码“Здравствуйте”所需的字节数,因为该字符串中的每个Unicode标量值都占用2个字节的存储空间。
因此,字符串字节的索引并不总是与有效的Unicode标量值相关。 因此为了演示,看看以下无效的Rust代码:
let hello = "Здравствуйте";
let answer = &hello[0];
答案的价值是什么? 应该是З,第一个字母吗? 当以UTF-8编码时,З的第一个字节为208,第二个字节为151,因此答案实际上应为208,但208本身不是有效字符。如果用户要求输入此字符串的第一个字母,则返回208可能不是用户想要的。 但是,这是Rust拥有字节索引0的唯一数据。即使字符串仅包含拉丁字母,用户也通常不希望返回字节值:如果&"hello"[0]是返回字节值的有效代码 ,它将返回104,而不是h。为了避免返回意外的值并导致可能不会立即发现的错误,Rust完全不编译该代码,并避免了开发过程早期的误解。
字节(bytes)和标量值(scalar values)以及字素簇(grapheme clusters)!
关于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类型),则这些字节如下所示:['न', 'म', 'स', '्', 'त', 'े']
这里有六个char值,但是第四个和第六个不是字母:它们是变音符号,它们自己没有意义。 最后,如果我们将它们视为字素簇,我们将得到一个人所说的组成印地语单词的四个字母:["न", "म", "स्", "ते"]
Rust提供了不同的方式来解释计算机存储的原始字符串数据,以便每个程序都可以选择所需的解释方式,而与数据所用的语言无关。
Rust不允许我们索引到String中以获取字符的最后一个原因是索引操作总是需要恒定的时间(O(1))。 但是用String不能保证性能,因为Rust必须从头到尾遍历所有内容以确定有多少个有效字符。
切片字符串
将字符串索引化通常是一个坏主意,因为不清楚字符串索引操作的返回类型应该是什么:字节值,字符,字形簇或字符串切片。 因此,Rust要求更具体地说明是否真的需要使用索引来创建字符串切片。为了更具体地指示索引并指示要使用字符串切片,而不是使用带单个数字的[]进行索引,可以将[]与范围结合使用以创建包含特定字节的字符串切片:
#![allow(unused_variables)]
fn main() {
let hello = "Здравствуйте";
let s = &hello[0..4];
println!("{}",s);
}
D:\learn\cargo_learn>cargo run
Compiling cargo_learn v0.1.0 (D:\learn\cargo_learn)
Finished dev [unoptimized + debuginfo] target(s) in 0.55s
Running `target\debug\cargo_learn.exe`
Зд
遍历字符串的方法
幸运的是,您可以通过其他方式访问字符串中的元素。
如果您需要对单个Unicode标量值执行操作,最好的方法是使用chars方法。 在“नमस्ते”上调用char会分离出并返回char类型的六个值,您可以遍历结果以访问每个元素:
#![allow(unused_variables)]
fn main() {
for c in "नमस्ते".chars() {
println!("{}", c);
}
}
D:\learn\cargo_learn>cargo run
Compiling cargo_learn v0.1.0 (D:\learn\cargo_learn)
Finished dev [unoptimized + debuginfo] target(s) in 0.52s
Running `target\debug\cargo_learn.exe`
न
म
स
्
त
े
bytes方法返回每个原始字节,这可能适合我们的初衷:
#![allow(unused_variables)]
fn main() {
for b in "नमस्ते".bytes() {
println!("{}", b);
}
}
这段代码将打印出组成此字符串的18个字节:
224
164
// --snip--
165
135
有效的Unicode标量值可能由1个以上的字节组成。
从字符串获取字素簇很复杂,因此rust标准库不提供此功能。但是我们可以在crates.io上获取使用
字符串不是那么简单
总而言之,字符串很复杂。Rust选择将正确处理String数据作为所有Rust程序的默认行为,这意味着我们必须在处理UTF-8数据方面投入更多精力,虽然这种折衷会带来更多的字符串复杂性,但是这可以防止在开发生命周期的后期不得不处理涉及非ASCII字符的错误。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。