基础篇-String类型

  • 木头
  • 更新于 2023-02-27 14:56
  • 阅读 1846

字符串新建,更新,读取

Rust 的核心语言中只有一种字符串类型:字符串 slicestr,它通常以被借用的形式出现,&str。由于字符串字面值被储存在程序的二进制输出中,因此字符串字面值也是字符串slice

新建字符串

很多 Vec 可用的操作在String 中同样可用,事实上 String被实现为一个带有一些额外保证、限制和功能的字节 vector 的封装。其中一个同样作用于 Vec<T>String 函数的例子是用来新建一个实例的 new函数:

fn main() {
    let s = String::new();
}

这新建了一个叫做 s的空的字符串,接着我们可以向其中装载数据。通常字符串会有初始数据,因为我们希望一开始就有这个字符串。为此,可以使用 to_string 方法,它能用于任何实现了 Display trait的类型,比如字符串字面值:

fn main() {
    let s = "hello world".to_string();
}

这些代码会创建包含 hello world 的字符串。

也可以使用String::from 函数来从字符串字面值创建 String。等同于使用 to_string

fn main() {
    let s = String::from("hello world");
}

因为字符串应用广泛,这里有很多不同的用于字符串的通用 API可供选择。其中一些可能看起来多余,不过都有其用武之地!在这个例子中,String::from.to_string 最终做了完全相同的工作,所以如何选择就是代码风格与可读性的问题了。

更新字符串

String 的大小可以增加,其内容也可以改变,就像可以放入更多数据来改变 Vec 的内容一样。另外,可以方便的使用 + 运算符或 format! 宏来拼接 String 值。

使用 push_strpush 附加字符串

可以通过 push_str 方法来附加字符串 slice,从而使 String 变长:

fn main() {
    let mut s = String::from("hello ");
    s.push_str("world");
}

执行这两行代码之后,s 将会包含 hello worldpush_str 方法采用字符串 slice,因为我们并不需要获取参数的所有权。我们希望在将 s2 的内容附加到 s1 之后还能使用它:

fn main() {
    let mut s1 = String::from("hello ");
    let s2 = "world";
    s1.push_str(s2);
    println!("s2 : {}", s2);
}

如果 push_str 方法获取了 s2 的所有权,就不能在最后一行打印出其值了。

push 方法被定义为获取一个单独的字符作为参数,并附加到 String 中。使用 push 方法将字母 "l" 加入 String 的代码。`

fn main() {
    let mut s = String::from("lo");
    s.push('l');
}

执行这些代码之后,s 将会包含 “lol”,注意pushchar类型。

使用 + 运算符或 format! 宏拼接字符串

通常你会希望将两个已知的字符串合并在一起。一种办法是像这样使用+运算符:

fn main() {
    let s1 = String::from("hello ");
    let s2 = String::from("world");
    let s3 = s1 + &s2; // 注意 s1 被移动了,不能继续使用
}

执行完这些代码之后,字符串 s3 将会包含 Hello worlds1 在相加后不再有效的原因,和使用 s2 的引用的原因,与使用 + 运算符时调用的函数签名有关。+ 运算符使用了 add函数,这个函数签名看起来像这样:

fn add(self, s: &str) -> String {...}

如果想要级联多个字符串,+的行为就显得笨重了:

fn main() {
    let s1 = String::from("l");
    let s2 = String::from("o");
    let s3 = String::from("l");
    let s = s1 + "-" + &s2 + "-" + &s3;
}

这时 s 的内容会是 “l-o-l”。在有这么多 +" 字符的情况下,很难理解具体发生了什么。对于更为复杂的字符串链接,可以使用format! 宏:

fn main() {
    let s1 = String::from("l");
    let s2 = String::from("o");
    let s3 = String::from("l");
    let s = format!("{}-{}-{}", s1, s2, s3);
}

这些代码也会将s设置为“l-o-l”format!println! 的工作原理相同,不过不同于将输出打印到屏幕上,它返回一个带有结果内容的 String。这个版本就好理解的多,宏 format! 生成的代码使用引用所以不会获取任何参数的所有权。

字符串索引

在很多语言中,通过索引来引用字符串中的单独字符是有效且常见的操作。然而在 Rust 中,如果你尝试使用索引语法访问 String 的一部分,会出现一个错误:

fn main() {
    let s1 = String::from("lol");
    let s2 = s1[0];
}

这段代码会导致如下错误:

$ cargo run
   Compiling variables v0.1.0 (/projects/variables)
error[E0277]: the type `String` cannot be indexed by `{integer}`
 --> src/main.rs:3:14
  |
3 |     let s2 = s1[0];
  |              ^^^^^ `String` cannot be indexed by `{integer}`
  |
  = help: the trait `Index<{integer}>` is not implemented for `String`
  = help: the following other types implement trait `Index<Idx>`:
            <String as Index<RangeFrom<usize>>>
            <String as Index<RangeFull>>
            <String as Index<RangeInclusive<usize>>>
            <String as Index<RangeTo<usize>>>
            <String as Index<RangeToInclusive<usize>>>
            <String as Index<std::ops::Range<usize>>>

For more information about this error, try `rustc --explain E0277`.
error: could not compile `variables` due to previous error

错误和提示说明了全部问题:Rust 的字符串不支持索引。那么接下来的问题是,为什么不支持呢?,看例子:

fn main() {
    let s1 = String::from("lol");
    println!("s1 长度 {}", s1.len());

    let s2 = String::from("你好");
    println!("s2 长度 {}", s2.len());
}

输出结果:

s1 长度 3
s2 长度 6

这里每一个字母的 UTF-8 编码占用一个字节,一个汉字的 UTF-8 编码占用三个字节,Rust的字符串实际上是不支持通过下标访问的,但是呢,我们可以通过将其转变成数组的方式访问。

字符串遍历

操作字符串每一部分的最好的方法是明确表示需要字符还是字节。对于单独的 Unicode 标量值使用chars方法。对 “你好” 调用 chars 方法会将其分开并返回两个char 类型的值,接着就可以遍历其结果来访问每一个元素了:

fn main() {
    let s1 = String::from("你好");
    for s in s1.chars() {
        println!("{}", s);
    }
}

这些代码会打印出如下内容:

你
好

另外 bytes 方法返回每一个原始字节,这可能会适合你的使用场景:

fn main() {
    let s1 = String::from("你好");
    for s in s1.bytes() {
        println!("{}", s);
    }
}

这些代码会打印出组成String6 个字节:

228
189
160
229
165
189

Rust还为我们提供比较安全取索引下标的方法:

fn main() {
    let s1 = String::from("你好");
    if let Some(s) = s1.chars().nth(0) {
        println!("{}", s);
    } else {
        println!("索引无效")
    }
}

这些代码会打印出如下内容:

字符串切片

对字符串切片是一件非常危险的事,虽然Rust支持,但是我并不推荐。因为Rust的字符串Slice实际上是切的bytes。这也就造成了一个严重后果,如果你切片的位置正好是一个Unicode字符的内部,Rust会发生Runtimepanic,导致整个程序崩溃。 因为这个操作是如此的危险:

fn main() {
    let s1 = String::from("你好");
    println!("{}", &s1[0..3]);
}

这里,s1 会是一个 &str,它包含字符串的头三个字节。早些时候,我们提到了这些汉字都是三个字节长的,所以这意味着 s1 将会是 “你”

如果获取 &s1[0..1] 会发生什么呢?答案是:Rust 在运行时会 panic,就跟访问 vector中的无效索引时一样:

fn main() {
    let s1 = String::from("你好");
    println!("{}", &s1[0..1]);
}

运行报错

 $ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.00s
     Running `target/debug/variables`
thread 'main' panicked at 'byte index 1 is not a char boundary; it is inside '你' (bytes 0..3) of `你好`', src/main.rs:3:21
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
  • 原创
  • 学分: 2
  • 分类: Rust
  • 标签:
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。
127 订阅 31 篇文章

0 条评论

请先 登录 后评论
木头
木头
0xC020...10cf
江湖只有他的大名,没有他的介绍。