什么是字符串(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 errorFor 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个字节:

224164// --snip--165135

无效的Unicode标量值可能由1个以上的字节组成。

从字符串获取字素簇很简单,因而rust规范库不提供此性能。然而咱们能够在crates.io上获取应用

字符串不是那么简略

总而言之,字符串很简单。Rust抉择将正确处理String数据作为所有Rust程序的默认行为,这意味着咱们必须在解决UTF-8数据方面投入更多精力,尽管这种折衷会带来更多的字符串复杂性,然而这能够避免在开发生命周期的前期不得不解决波及非ASCII字符的谬误。