字符串类型
诸位在入门rust的时候,要认真,因为字符串类型在rust中有好几种,一不小心就搞混了类型,导致代码编译报错。好在有强大的rust-analyzer和vscode帮助我们。我们直接通过一段代码来开始认识rust的字符串类型。
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),注意这个区间是左闭右开的。例如:
let s = String::from("hello");
let slice = &s[2..5];
println!("{slice}");
这段代码中的slice是&str类型,切片引用了s的第3个字节到第5个字节的内容。即输出llo
,在rust的切片中,下标也不能超过字符串长度的边界,否则会导致运行时错误。例如:
let s = String::from("hello");
let slice = &s[2..8];
println!("{slice}");
这段代码在执行的时候会直接panic,异常信息如下所示:
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"的界限。我们再次尝试让起始索引越界。
let s = String::from("hello");
let slice = &s[-1..5];
println!("{slice}");
这次将无法通过编译,编译时报错如下。
rust要求索引必须是usize类型的,这意味着索引不能是负数。另外,如果起始索引是0,可以简写为&s[..3]
;同样如果终止索引是String的最后一个字节,那么可以简写为&s[1..]
;因此如果要引用整个String,那么可以简写为&s[..]
。下面是一个简单的演示,它们每组都是等价的。
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,例如:
let s = String::from("hello");
let s = "hello".to_string();
将String转为&str前面也演示了,例如:
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字符串。
let s = "Hello".to_string();
let s = String::from("world");
let s: String = "also this".into();
追加
在字符串尾部可以使用 push() 方法追加字符 char,也可以使用 push_str() 方法追加字符串字面量。这两个方法都是在原有的字符串上追加,并不会返回新的字符串。由于字符串追加操作要修改原来的字符串,则该字符串必须是可变的,即字符串变量必须由 mut 关键字修饰。例如:
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() 方法插入字符串字面量。例如:
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() 方法接收两个参数,第一个参数是要被替换的字符串,第二个参数是新的字符串。该方法会替换所有匹配到的字符串。该方法是返回一个新的字符串,而不是操作原来的字符串。
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); }
dbg!是rust提供的调试使用的宏,方便rust使用者进行打印输出。它会打印出其所在的文件,代码行,变量名。非常便于调试。
-
replacen
该方法可适用于 String 和 &str 类型。replacen() 方法接收三个参数,前两个参数与 replace() 方法一样,第三个参数则表示替换的个数。该方法是返回一个新的字符串,而不是操作原来的字符串。例如:
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 关键字修饰。例如:
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类型。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() 方法是按照字节来处理字符串的,如果参数所给的位置不是合法的字符边界,则会发生错误。
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() 方法是按照字节来处理字符串的,如果参数所给的位置不是合法的字符边界,则会发生错误。例如:
fn main() { let mut string_truncate = String::from("测试truncate"); string_truncate.truncate(3); dbg!(string_truncate); }
-
clear
清空字符串,该方法是直接操作原来的字符串。调用后,删除字符串中的所有字符,相当于 truncate() 方法参数为 0 的时候。例如:
fn main() { let mut string_clear = String::from("string clear"); string_clear.clear(); dbg!(string_clear); }
连接
-
使用 + 或者 += 连接字符串
使用 + 或者 += 连接字符串,要求右边的参数必须为字符串的切片引用(Slice)类型。其实当调用 + 的操作符时,相当于调用了 std::string 标准库中的 add() 方法,这里 add() 方法的第二个参数是一个引用的类型。因此我们在使用 +, 必须传递切片引用类型。不能直接传递 String 类型。(类似于C++的运算符重载)+ 和 += 都是返回一个新的字符串。所以变量声明可以不需要 mut 关键字修饰。例如:
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函数类似。例如:
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书册和编译错误索引表。
关于字符串的其他部分
-
我们可以通过转义的方式 \ 输出 ASCII 和 Unicode 字符
fn main() { // 通过 \ + 字符的十六进制表示,转义输出一个字符 let byte_escape = "I'm writing \x52\x75\x73\x74!"; println!("What are you doing\x3F (\\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 ); }
-
将一个物理行字符串拆分到多行
使用\连接两行(多行)为一个物理行。
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,那么字符串在被使用的时候将不会发生转义。
fn main() { println!("{}", "hello \x52\x75\x73\x74"); // 输出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#这种方式来解决这个问题,当然你也可以使用转义的方式。文章来源:https://www.toymoban.com/news/detail-431605.html
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这个库。当然了另一种解决方案就是使用字符数组,这样就可以非常方便的进行操作了。文章来源地址https://www.toymoban.com/news/detail-431605.html
参考资料
- rust圣经
- rust标准库String
- rust标准库str
到了这里,关于rust字符串的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!