Rust 字符串详解

2022-09-19 rust language

很多语言中的字符串非常简单,但 Rust 与常规的语言有所区别。

简介

例如,如下代码。

pub fn greet(name: String) {
    println!("Hello {}!", name);
}

fn main() {
    greet("World");
}

编译时会有如下的报错。

error[E0308]: mismatched types
 --> src/main.rs:6:11
  |
6 |     greet("World");
  |     ----- ^^^^^^^- help: try using a conversion method: `.to_string()`
  |     |     |
  |     |     expected `String`, found `&str`
  |     arguments to this function are incorrect
  |
note: function defined here
 --> src/main.rs:1:8
  |
1 | pub fn greet(name: String) {
  |        ^^^^^ ------------

简单来说,就是传入的参数类型不对。Rust 中的字符采用 Unicode 编码,每个字符固定占用 4 个字节空间,而字符串采用 UTF-8 编码,占用 1~4 个字节。

关于编码更多可以参考 字符编码详解 中的介绍。

字符

在 Rust 中 char 可能占 1~4 字节,不过实际底层采用 u32 保存,这样就可以直接在 charu32 之间进行转换。不过,需要注意 Unicode 编码的合法性,同时单字节只支持 7Bits 编码。

除了常规的输入,还可以通过 \ 进行转义,例如 ASCII 编码 \x52 对应 R,也包括通过 \u{211D} 方式的 Unicode 编码。

fn main() {
    let c = '🌐'; // \u{1f310}
    println!("{} {} {}", 'a'.len_utf8(), '你'.len_utf8(), '🌐'.len_utf8());
    println!("{} {} {:x}", c, char::from_u32(0x1f310).unwrap(), c as u32);
}
// Output:
// 1 3 4
// 🌐 🌐 1f310

编码格式

String 存储的字节需要保证是一个有效的 UTF-8 编码,保存在堆中,非 Null 结尾。如果有很多字符需要转义,那么可以使用原始字符串,相对会更方便些。

fn main() {
    let r0 = r"Eescapes don't work: \x52\x75\x73\x74, \u{211D}";
    println!("{}", r0);
    let r1 = r#"Eescapes don't work: \x52\x75\x73\x74, "\u{211D}""#;
    println!("{}", r1);
}
// Output:
// Eescapes don't work: \x52\x75\x73\x74, \u{211D}
// Eescapes don't work: \x52\x75\x73\x74, "\u{211D}"

另外,如果要使用非 UTF-8 编码,那么可以使用字节串,支持转义,但不支持 Unicode 写法,此时转换到 UTF-8 可能会失败。

fn main() {
    let b0: &[u8; 18] = b"I'm writing \"\x52\x75\x73\x74\"";
    println!("{:?}", b0); // Display trait not implied.
    let b1 = br"Eescapes don't work: \u{211D}";
    if let Ok(s) = std::str::from_utf8(b1) {
        println!("{}", s);
    }
}
// Output:
// [73, 39, 109, 32, 119, 114, 105, 116, 105, 110, 103, 32, 34, 82, 117, 115, 116, 34]
// Eescapes don't work: \u{211D}

如果是中文或者 Emoji 可以通过如下方式查看,对应的中文时 3Bytes 编码,而 Emoji 一般是 4Bytes 编码,注意,使用 Unicode 码位时需要一些转换才行。

fn main() {
    let s1 = "你好🌐";
    println!("{:?}", s1.as_bytes());
    let s2 = "\u{4f60}\u{597d}\u{1f310}";
    println!("{:?}", s2.as_bytes());
    println!("{}", s2);
}
// Output:
// [228, 189, 160, 229, 165, 189, 240, 159, 140, 144]
// [228, 189, 160, 229, 165, 189, 240, 159, 140, 144]
// 你好🌐

如上两种方式相同。

字符流 VS. 字节流

字符串可以通过 as_bytes() 函数转换为字节,字符则可以进行遍历,如下是常用的方法。

fn main() {
    let strs = "你好🌐";

    let bytes: &[u8] = strs.as_bytes();
    println!("{:?}", bytes);
    println!("{}", String::from_utf8(bytes.to_vec()).unwrap());
    println!("{}", String::from_utf8(Vec::from(bytes)).unwrap());

    for (idx, c) in strs.char_indices() {
        println!("idx={} char={}", idx, c);
    }

    let mut iter = strs.chars();
    while let Some(c) = iter.next() {
        println!("char={}", c);
    }

    for c in strs.chars() {
        println!("char={}", c);
    }
}

String VS. str

实际上,在 Rust 语言层面只有 str 类型,只是通常以 &str 方式使用,可以理解为字符串切片,该类型会在编译阶段硬编码到二进制文件中,无法修改,也被称为字符串字面量。

为了修改,在标准库中提供了 String 类型,这也是最常用的,可以通过如下方式从 &str 生成 String 类型。

String::from("Hello World!");
"Hello World!".to_string();

而从 String 转换为 &str 也很简单,只需要取引用即可,例如 &s &s[..] s.as_str() 都可以。

Vec

这是 Rust 中的动态数组,可以通过 Vec::new() Vec::with_capacity(8) vec![1, 2] vec![0; 5] 这种方式创建,其中第一种方式默认不会申请内存,只有当写入的时候才会申请;空间不足时,会自动进行扩容。

与字符串结合最常用的就是 Vec<u8> 类型了。

其它

clone VS. to_owned

在遇到需要复制的情况时,需要调用 clone()to_owend() 方法,但是这两个方法有联系和差别。

源类型clone()to_owned()
TT => TT => T
&T&T => &T&T => T

如下是针对字符串的使用,大部分情况中 to_owned 在内部会调用 clone 实现。

对于引用类型,会对所有的不可变引用实现 CopyClone 特性,不可变引用的复制和克隆的效果都是一样,只获得了引用,没有所有权,而 to_owend() 内部通过 &T 调用 Tclone() 获得原始数据的拷贝。

let s: String = String::from("Hello");
let s_clone: String = s.clone();
let s_owend: String = s.to_owned();

let s: &str = "Hello";
let s_clone: &str = s.clone();
let s_owend: String = s.to_owned();

总的来说,对于引用类型当调用 clone() 时只是复制引用,而 to_owned() 则会拷贝原始数据获得所有权。

总结

&str    -> String   String::from(s)/s.to_string()/s.to_owned()
&str    -> &[u8]    s.as_bytes()
&str    -> Vec<u8>  s.as_bytes().to_vec()/s.as_bytes().to_owned()
String  -> &str     &s if possible* else s.as_str()
String  -> &[u8]    s.as_bytes()
String  -> Vec<u8>  s.into_bytes()
&[u8]   -> &str     std::str::from_utf8(s).unwrap()
&[u8]   -> String   String::from_utf8(s).unwrap()
&[u8]   -> Vec<u8>  s.to_vec()
Vec<u8> -> &str     std::str::from_utf8(&s).unwrap()
Vec<u8> -> String   String::from_utf8(s).unwrap()
Vec<u8> -> &[u8]    &s if possible* else s.as_slice()