什么是字符串(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字符的错误。


子康
10 声望0 粉丝