rust字符串

2023-01-13 11:00:51 浏览数 (1)

字符串类型

诸位在入门rust的时候,要认真,因为字符串类型在rust中有好几种,一不小心就搞混了类型,导致代码编译报错。好在有强大的rust-analyzer和vscode帮助我们。我们直接通过一段代码来开始认识rust的字符串类型。

代码语言:javascript复制
fn main() {
    let s = "Hello, Rust string!";      // s是&str类型
    println!("{s}");

    let s = "Hello, Rust string!".to_string();  // s是String类型
    println!("{s}");
    
    let s1 = &s;                    // &String类型
    println!("{s1}");
    
    let s2 = &s[0..5];              // &str类型
    println!("{s2}");

    // let s1 = s[0..5];       error 这应该是什么类型?是str吗?那么如何使用str类型?
}

字符串切片引用类型(&str)

首先,我们还是从字符串字面值来谈起,在rust中,字符串字面值常量的类型是&str,这个类型称之为“字符串切片引用”。字符串字面值是特殊的,它实际上存储在可执行程序的只读内存段中(rodata)。通过&str可以引用rodata中的字符串。同样,对于在堆上存放的字符串String类型,也可以通过&str来引用其中的部分。就和python的切片类似。但是如果想要直接使用str类型,是不行的,只能通过Box<str>来使用。例如上面通过切片引用&s[0..5]来使用s的第1个字节到第5个字节的内容。索引下标是从0开始的,范围是区间[0, 5),注意这个区间是左闭右开的。例如:

代码语言:javascript复制
let s = String::from("hello");
let slice = &s[2..5];
println!("{slice}");

这段代码中的slice是&str类型,切片引用了s的第3个字节到第5个字节的内容。即输出llo,在rust的切片中,下标也不能超过字符串长度的边界,否则会导致运行时错误。例如:

代码语言:javascript复制
let s = String::from("hello");
let slice = &s[2..8];
println!("{slice}");

这段代码在执行的时候会直接panic,异常信息如下所示:

代码语言:javascript复制
thread 'main' panicked at 'byte index 8 is out of bounds of `hello`', src/main.rs:23:18
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

它告诉我们索引8超出了"hello"的界限。我们再次尝试让起始索引越界。

代码语言:javascript复制
let s = String::from("hello");
let slice = &s[-1..5];
println!("{slice}");

这次将无法通过编译,编译时报错如下。

rust要求索引必须是usize类型的,这意味着索引不能是负数。另外,如果起始索引是0,可以简写为&s[..3];同样如果终止索引是String的最后一个字节,那么可以简写为&s[1..];因此如果要引用整个String,那么可以简写为&s[..]。下面是一个简单的演示,它们每组都是等价的。

代码语言:javascript复制
let slice = &s[0..3];
println!("{slice}");
let slice = &s[..3];
println!("{slice}");

let len = s.len();
let slice = &s[1..len];
println!("{slice}");
let slice = &s[1..];
println!("{slice}");

let slice = &s[0..len];
println!("{slice}");
let slice = &s[..];
println!("{slice}");

最后,字符串切片引用的索引必须落在字符之间的边界位置,但是由于rust的字符串是UTF-8编码的,因此必须要小心。后文会讲述如何取出UTF-8编码的一个字符。切片类型是对集合的部分引用,不仅有字符串切片引用,其它的集合类型也有。

字符串类型(String)

Rust 中的字符是 Unicode 类型,因此每个字符占据 4 个字节内存空间,但是在字符串中不一样,字符串是 UTF-8 编码,也就是字符串中的字符所占的字节数是变化的(1 - 4)

Rust 在语言级别,只有一种字符串类型: str,它通常是以引用类型出现 &str,也就是上文提到的字符串切片引用。虽然语言级别只有上述的 str 类型,但是在标准库里,还有多种不同用途的字符串类型,其中使用最广的即是 String 类型。

前面说个,str类型被存放在了rodata区域,无法被修改。而String是一个可增长,可变且具有所有权的utf-8编码的字符串。

String和&str的相互转换

前文已经看到了如何将&str转为String,例如:

代码语言:javascript复制
let s = String::from("hello");
let s = "hello".to_string();

将String转为&str前面也演示了,例如:

代码语言:javascript复制
let s = String::from("hello");
let slice = &s[..2];
println!("{slice}");    // 直接打印,没有解引用。

其中实际上还有一个问题,可能有部分读者已经注意到了,那就是我们直接打印了slice这个切片引用,而没有解引用。实际上这是因为deref 隐式强制转换,这是由编译器帮我们完成的。而且你不能直接使用str类型。如果手动解引用,会引起编译错误。

不能使用字符串索引

由于rust的字符串类型是utf-8编码的,如果允许使用索引来取出字符串中的某个字符,那么这将牺牲一部分性能,而rust期望索引操作的时间复杂度是O(1)。因此,你不能像python那样使用索引去访问第n个字符。当然rust提供了其它方式来获取其中某个字符。例如chars方法。

操作字符串

操作字符串,主要是针对String类型来进行的。无非就是涉及到增删改查。

创建String字符串

通过官方文档可以得知,我们可以有下面三种方式从字符串字面值来创建一个新的String字符串。

代码语言:javascript复制
let s = "Hello".to_string();
let s = String::from("world");
let s: String = "also this".into();

追加

在字符串尾部可以使用 push() 方法追加字符 char,也可以使用 push_str() 方法追加字符串字面量。这两个方法都是在原有的字符串上追加,并不会返回新的字符串。由于字符串追加操作要修改原来的字符串,则该字符串必须是可变的,即字符串变量必须由 mut 关键字修饰。例如:

代码语言:javascript复制
fn main() {
    let mut s = String::from("Hello ");
    s.push('r');
    println!("追加字符 push() -> {}", s);

    s.push_str("ust!");
    println!("追加字符串 push_str() -> {}", s);
}

插入

可以使用 insert() 方法插入单个字符 char,也可以使用 insert_str() 方法插入字符串字面量。例如:

代码语言:javascript复制
fn main() {
    let mut s = String::from("Hello rust!");
    s.insert(5, ',');
    println!("插入字符 insert() -> {}", s);
    s.insert_str(6, " I like");
    println!("插入字符串 insert_str() -> {}", s);
}

这俩方法需要传入两个参数,第一个参数是字符(串)插入位置的索引,第二个参数是要插入的字符(串),索引从 0 开始计数,如果越界则会发生错误。它们都是在原字符串上做修改,因此原字符串必须可变。

替换

replace

该方法可适用于 String 和 &str 类型。replace() 方法接收两个参数,第一个参数是要被替换的字符串,第二个参数是新的字符串。该方法会替换所有匹配到的字符串。该方法是返回一个新的字符串,而不是操作原来的字符串。

代码语言:javascript复制
fn main() {
    let string_replace = String::from("I like rust. Learning rust is my favorite!");
    let new_string_replace = string_replace.replace("rust", "RUST");
    dbg!(new_string_replace);
    let s = "12345";
    let new_s = s.replace("3", "t");
    dbg!(new_s); 
}
1

dbg!是rust提供的调试使用的宏,方便rust使用者进行打印输出。它会打印出其所在的文件,代码行,变量名。非常便于调试。

replacen

该方法可适用于 String 和 &str 类型。replacen() 方法接收三个参数,前两个参数与 replace() 方法一样,第三个参数则表示替换的个数。该方法是返回一个新的字符串,而不是操作原来的字符串。例如:

代码语言:javascript复制
fn main() {
    let string_replace = "I like rust. Learning rust is my favorite!";
    let new_string_replacen = string_replace.replacen("rust", "RUST", 1);
    dbg!(new_string_replacen);
}

replace_range

该方法仅适用于 String 类型。replace_range 接收两个参数,第一个参数是要替换字符串的范围(Range),第二个参数是新的字符串。该方法是直接操作原来的字符串,不会返回新的字符串。该方法需要使用 mut 关键字修饰。例如:

代码语言:javascript复制
fn main() {
    let mut string_replace_range = String::from("I like rust!");
    string_replace_range.replace_range(7..8, "R");
    dbg!(string_replace_range);
}

这个方法在字符串中的字符都是由ASCII组成的时候,是非常有用的。但是如果存在非ASCII编码的字符时,就需要计算出正确的utf-8字符的起始位置和结束位置,否则会造成运行时错误。

删除

与字符串删除相关的方法有 4 个,他们分别是 pop(),remove(),truncate(),clear()。这四个方法仅适用于 String 类型。

pop

删除并返回字符串的最后一个字符(按字符处理,不是字节),该方法是直接操作原来的字符串。但是存在返回值,其返回值是一个 Option 类型,如果字符串为空,则返回 None。我们在这里都是用dbg!这个宏来打印的,他能够将Option<char>类型打印出来,后面我们再来介绍Option类型。

代码语言:javascript复制
fn main() {
    let mut string_pop = String::from("rust pop 中文!");
    let p1 = string_pop.pop();
    let p2 = string_pop.pop();
    dbg!(p1);
    dbg!(p2);
    dbg!(string_pop);
}

remove

删除并返回字符串中指定位置的字符,该方法是直接操作原来的字符串,其返回值是删除位置的字符串,只接收一个参数,表示该字符起始索引位置。remove() 方法是按照字节来处理字符串的,如果参数所给的位置不是合法的字符边界,则会发生错误。

代码语言:javascript复制
fn main() {
    let mut string_remove = String::from("测试remove方法");
    println!(
        "string_remove 占 {} 个字节",
        std::mem::size_of_val(string_remove.as_str())
    );
    // 删除第一个汉字
    string_remove.remove(0);
    // 下面代码会发生错误
    // string_remove.remove(1);
    // 直接删除第二个汉字
    // string_remove.remove(3);
    dbg!(string_remove);
}

truncate

删除字符串中从指定位置开始到结尾的全部字符,该方法是直接操作原来的字符串。无返回值。该方法 truncate() 方法是按照字节来处理字符串的,如果参数所给的位置不是合法的字符边界,则会发生错误。例如:

代码语言:javascript复制
fn main() {
    let mut string_truncate = String::from("测试truncate");
    string_truncate.truncate(3);
    dbg!(string_truncate);
}

clear

清空字符串,该方法是直接操作原来的字符串。调用后,删除字符串中的所有字符,相当于 truncate() 方法参数为 0 的时候。例如:

代码语言:javascript复制
fn main() {
    let mut string_clear = String::from("string clear");
    string_clear.clear();
    dbg!(string_clear);
}

连接

使用 或者 = 连接字符串

使用 或者 = 连接字符串,要求右边的参数必须为字符串的切片引用(Slice)类型。其实当调用 的操作符时,相当于调用了 std::string 标准库中的 add() 方法,这里 add() 方法的第二个参数是一个引用的类型。因此我们在使用 , 必须传递切片引用类型。不能直接传递 String 类型。(类似于C 的运算符重载) 和 = 都是返回一个新的字符串。所以变量声明可以不需要 mut 关键字修饰。例如:

代码语言:javascript复制
fn main() {
    let string_append = String::from("hello ");
    let string_rust = String::from("rust");
    // // &string_rust会自动解引用为&str,这是因为deref coercing特性。这个特性能够允许把传进来的&String,在API执行之前转成&str。
    let result = string_append   &string_rust;
    let mut result = result   "!";
    result  = "!!!";

    println!("连接字符串   -> {}", result);
}

这里出现了deref coercing这个特性,这是为了使用起来更方便,但是个人认为deref coercing是一个不一致性设计。另外一点是add函数的函数原型是fn add(self, s: &str) -> String,self 是 String 类型的字符串,因此string_append在经过连接运算( )之后,转移了所有权,导致string_append被释放。

使用 format! 连接字符串

format! 这种方式适用于 String 和 &str,和C/C 提供的sprintf函数类似。例如:

代码语言:javascript复制
fn main() {
    let s1 = "hello";
    let s2 = String::from("rust");
    let s = format!("{} {}!", s1, s2);
    println!("{}", s);
}

format宏使用起来更加方便,而且你可以自由的连接多个字符串,并且由于宏使用的是引用,因此不会发送所有权的转移。

标准库中String和&str有非常多的方法,可以在rust官方文档中进行查看。rust的官方文档编写的算是非常Nice的,几乎每个函数都有例子。

在这里,你能几乎能找到关于rust的一切。最常用的可能就是标准库,cargo书册和编译错误索引表。

关于字符串的其他部分

  1. 我们可以通过转义的方式 输出 ASCII 和 Unicode 字符
代码语言:javascript复制
fn main() {
    // 通过    字符的十六进制表示,转义输出一个字符
    let byte_escape = "I'm writing x52x75x73x74!";
    println!("What are you doingx3F (\x3F means ?) {}", byte_escape);

    // u 可以输出一个 unicode 字符
    let unicode_codepoint = "u{211D}";
    let character_name = ""DOUBLE-STRUCK CAPITAL R"";

    println!(
        "Unicode character {} (U 211D) is called {}",
        unicode_codepoint, character_name
    );
}

将一个物理行字符串拆分到多行

使用连接两行(多行)为一个物理行。

代码语言:javascript复制
fn main() {
    let long_string = "String literals
                    can span multiple lines.
                    The linebreak and indentation here     
                    can be escaped too!";
    println!("{}", long_string);
}

这段代码输出如下所示:

String literals和can span multiple lines.是分开的两行,而The linebreak and indentation here和can be escaped too!由连接为一个物理行。

原始字符串

现在的大多数语言中都提供了原始字符串的语法,rust也不例外,在字符串前面加上小写字母r,那么字符串在被使用的时候将不会发生转义。

代码语言:javascript复制
fn main() {
    println!("{}", "hello x52x75x73x74");           // 输出hello Rust
    let raw_str = r"Escapes don't work here: x3F u{211D}";    // 原始字符串
    println!("{}", raw_str);        // 输出Escapes don't work here: x3F u{211D}
}

字符中出现双引号

在C/C 中,字符串中出现双引号的时候,需要进行转义;rust提供了r#这种方式来解决这个问题,当然你也可以使用转义的方式。

代码语言:javascript复制
fn main() {
    // 如果字符串包含双引号,可以在开头和结尾加 #
    let quotes = r#"And then I said: "There is no escape!""#;
    println!("{}", quotes);

    // 如果还是有歧义,可以继续增加#,没有限制
    let longer_delimiter = r###"A string with "# in it. And even "##!"###;
    println!("{}", longer_delimiter);
}

字符串和字符数组

由于rust的字符串是uft-8编码的,而String类型不允许以字符为单位进行索引。有时候会不方便。String提供了chars()方法遍历字符以及bytes()方法遍历字节。另外就是,如果需要从String中获取子串是比较困难的,标准库没有提供相关的方法。 你需要在 crates.io 上搜索 utf8 来寻找想要的功能。可以使用utf8_slice这个库。当然了另一种解决方案就是使用字符数组,这样就可以非常方便的进行操作了。

参考资料

rust圣经

rust标准库String

rust标准库str

0 人点赞