前言

相比与 Golang 或者是其他语言的字符串的设计 Rust 对于字符串的设计就要复杂不少,新手第一坑可能就容易掉进去,并且觉得很难用。不过其实掌握其中的几个基本概念,就会发现也就这么回事。今天我们来唠唠让人头疼的字符串。

两个问题

一般来说,很少有人第一个语言上手就是 rust ,当带着其他语言的影子来使用 rust 的时候势必会遇到下面两个问题。

问题 1: 直接写的不是字符串

如下面的代码,就会报错

1
2
3
4
5
6
7
8
fn main() {
let s = "我是一个字符串";
check(s);
}

fn check(s: String) {
println!("不,{},你不是个字符串!", s);
}

新手遇到的第一个问题就是直接写字符串 “xxx” ,发现不好用。

那么第一个误区就来了,”xxx” 这样的表示通常用字面量来称呼,而在 rust 里面,直接这样写,s 就是一个 &str 类型,可以理解为

1
let s: &str = "xxx";

至于 &str 是什么后面会说到。

问题 2: 不能索引

这个问题一开始我也痛苦了半天,不理解的时候特别难受。

1
2
3
fn check(s: String) {
let a = s[1];
}

像别的语言,都可以通过下标来索引到字符串具体的位置上的字符,而 rust 不行,如上的代码就会报错。

其实原因说起来也简单,在其他语言,比如 Go,在做字符串切分的时候,比如取字符串长度前面为 8 的字串,那么 s[0:8] 对吧。但是如果内容是中文或其他语言,截取的长度就是不对的,由于每个字符所占用的字节是不一样的,utf8 编码下。所以在做字符串截取的时候,要特别处理。而由于我们常常截取都是英文,所以没出现问题,当然截取的本质就是索引下标。

而 Rust 到好,直接一刀切,压根就不能索引也就不存在截取的问题了。直接从语言层面就让你明白这个问题其实一直都在

如何遍历

先解决一下你的疑虑,如果不能索引,那么字符串如何遍历呢?答案是 chars()

1
2
3
for c in "我是一个字符串".chars() {
println!("{}", c);
}

是的,你只能通过这样的方式去做了

其实就两个东西

首先,rust 里面有很多其他类型的字符串,但最基础的情况下,你只需要认识两个东西就可以了,&strString

  • &str 其本质就是切片的引用,切片在很多语言下也有,你可以简单把它理解成为视图的概念
  • String 而这才是真正我们常用的字符串,可以对它做删除、拼接等等操作

&str 可以转 String 通过 to_string() 方法就可以,也就是一开始的代码需要这样写。

1
2
3
4
5
6
7
8
fn main() {
let s = "我是一个字符串";
check(s.to_string());
}

fn check(s: String) {
println!("不,{},你不是个字符串!", s);
}

而 String 的使用更为常见,主要是声明:

1
let s = String::from("我是一个字符串");

这样才是一个 String 该有的样子,虽然我知道这样写很多人就会觉得,哇,好麻烦。

而字符串就很容易了,它就可以使用 push、replace 等等方法做操作了,指定注意的是,你需要关心它的方法是否操作了原来的字符串还是新返回了一个字符串。这个在 IDE 里面都是有提示的,问题不大。

弄清楚可变不可变

可,rust 为什么要这样设计呢?将一个原本简单的 “” 变得复杂?

答案其实在 mut ,当然不是这个关键字,而是可变不可变。当一个语言把 “让可以对象可变” 需要特别使用可以关键字 mut 的时候,你就知道它对可变不可变的关心有多么强烈

字符串也是一样的。之所以 “xxx” 是 &str 类型,本质来说就是想让大家知道它不可变,由于 “xxx” 这样的声明往往都是以静态方式放在最终应用里面的,也就是说硬编码到了可执行文件里面去。而对于动态可变的 String 来说,rust 需要时刻关注它使用完成之后需要将内存释放掉。也就是对于这样可变不可变的关注,从而让原本 “xxx” 变得不太一样。

总结

在你看来 rust 很多奇怪的设计,往往都和 “为了去掉 GC” 相挂钩。除了这点,从字符串不能索引也可以看出,在语言层面 rust 想要限制很多它认为不对的事情。最后,依然要夸一下 rust 编译器的强大,当你字符串使用的过程中出现意外的时候,编译器往往提供了大量的信息,并且明确指出了出现错误的原因,我给你的建议是,慢慢看,其实它不是在说废话。