Rust 字符串与切片
字符串和切片
切片
let s = String::from("hello-world.");
let len = s.len();// 包头不包尾
println!("{}", &s[0..5]); // hello
println!("{}", &s[..5]); // hello
println!("{}", &s[6..]); // world.
println!("{}", &s[..]); // hello-world.
println!("{}", &s[0..len]); // hello-world.
切片的类型标识:&str
fn main() {let mut s = String::from("hello world.");let word = first_word(&s); // 返回一个不可变引用// s.clear(); // ERROR 因为 clear() 需要操作可变引用println!("{}", word);
}fn first_word(s: &String) -> &str {&s[..1]
}
其它切片
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3];
字符串切片类型
&str
不具有所有权。
字符串字面量
let s = "hello";
let s: &str = "hello";
这种形式的字符串其类型是 &str
切面类型。
字符串
在 Rust 中,默认只有一种字符串类型,即 str
,其表现形式为 &str
,是对字符串的不可变引用。
但是在标准库中提供了多种不同用途的字符串类型,其中最常用的是 String
类型。
&str
字符串切片类型和 String
类型的编码均采用 UTF-8(强制规定)。
除了 String 类型外,标准库中还提供:OsString
, OsStr
, CsString
和 CsStr
等。
注意这些以 String
或者 Str
结尾:它们分别对应的是具有所有权和被借用的变量。
&str 和 String
特性 | &str 字符串切片 | String 动态字符串 |
---|---|---|
所有权 | 无所有权,仅仅只是借用 | |
内存分配 | 不分配空间,它指向现有的数据,没有则创建 | 在堆上动态分配 |
可变性 | 不可变(因为是只读引用) | 可变 |
将 &str 转为 String
let slice = "hello";
let string = String::from(slice); // 或 slice.to_string()
需要先将数据复制到堆上。
将 String 转为 &str
fn main() {let s = String::from("hello,world!");say_hello(&s);say_hello(&s[..]);say_hello(s.as_str());
}fn say_hello(s: &str) {println!("{}",s);
}
对 String 进行取引用即可,无需复制数据,直白点就是直接借用(引用)原数据。
字符串索引
let s = String::from("hello");
let i = s[0];
Rust 中的 String 字符串并不能像 Python 或 JavaScript 那样直接通过
[index]
索引下标得到。
Rust String 字符串底层
查看 string.rs
源码可以发现,String 字符串的底层采用的是一个 u8 容器。也就是说 String 的基本单位是 1 个字节,是一个字节数组。
在 UTF-8 中,hello
每个字符占用 1 字节,总共 4 字节。反卷
字符串每个占用 3 字节,总共 6 字节。所以无法通过索引来直接获取数据。如图:
如果对 反卷
字符串进行 [0]
操作,则只能得到“反”字的第一个字节。
字符串的不同表现形式
示例
let s = String::from("反卷");
let ch = &s[0..3];
println!("{}", ch); // [SUCCESS] 反
现在看一下落在“反”中间的效果
let s = String::from("反卷");
let ch = &s[0..2];
println!("{}", ch); // ERROR
thread 'main' panicked at src/main.rs:263:16:
byte index 2 is not a char boundary; it is inside '反' (bytes 0..3) of `反卷`
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
字符串切片是切割的字节数组,采用的 UTF-8 编码,所以需要额外小心使得切割的边界要保证恰好落在边界上。
String 字符串常用操作
追加(Push)
fn main() {let mut s = String::from("Hello ");s.push_str("rust");println!("追加字符串 push_str() -> {}", s);
}
插入(Insert)
fn main() {let mut s = String::from("Hello rust!");s.insert(5, ',');println!("插入字符 insert() -> {}", s); // Hello, rust!s.insert_str(6, " I like");println!("插入字符串 insert_str() -> {}", s); // Hello, I like rust!
}
替换(replace)
replace
fn main() {let string_replace = String::from("I like rust. Learning rust is my favorite!");// 可适用于 String 和 &str 类型// (被替换的表达式, 要替换的新内容)let new_string_replace = string_replace.replace("rust", "RUST");// 方法是返回一个新的字符串dbg!(new_string_replace);
}
new_string_replace = "I like RUST. Learning RUST is my favorite!"
replacen
fn main() {let string_replace = "I like rust. Learning rust is my favorite!";// 可适用于 String 和 &str 类型// (被替换的表达式, 要替换的新内容, 替换次数)let new_string_replacen = string_replace.replacen("rust", "RUST", 1);// 方法是返回一个新的字符串dbg!(new_string_replacen);
}
new_string_replacen = "I like RUST. Learning rust is my favorite!"
replace_range
fn main() {let mut string_replace_range = String::from("I like rust!");// 该方法仅适用于 String 类型// (要被替换的范围, 要替换的新内容)string_replace_range.replace_range(7..8, "R");// 直接操作原来的字符串,不会返回新的字符串,所以需要使用 mut 修饰dbg!(string_replace_range);
}
string_replace_range = "I like Rust!"
删除(Delete)
pop
- 作用:删除并返回字符串的最后一个字符。
- 返回:一个
Option
类型,如果字符串为空,则返回None
。 - 备注:直接操作原字符串。
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);
}
p1 = Some('!',
)
p2 = Some('文',
)
string_pop = "rust 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);
}
string_remove 占 18 个字节
string_remove = "试remove方法"
truncate
- 作用:删除字符串中从指定位置开始到结尾的全部字符
- 返回:无
- 备注:直接操作原来的字符串。truncate 是按照字节来处理字符串的。
fn main() {let mut string_truncate = String::from("测试truncate");string_truncate.truncate(3);dbg!(string_truncate);
}
string_truncate = "测"
clear
- 作用:清空字符串
- 返回:无
- 备注:直接操作原来的字符串。
fn main() {let mut string_clear = String::from("string clear");string_clear.clear();dbg!(string_clear);
}
string_clear = ""
字符串拼接
+ 和 +=
let s1 = String::from("hello ");
let s2 = String::from("rust");
let res = s1 + &s2; // add(s1, &s2)
let mut res = res + "!"; // add(res, "!") 别忘记字面量是字符串切片类型,是引用的。
res += "!!!";
println!("{}", res); // hello rust!!!!
使用 +
或 +=
时,相当于调用 add(self, s: &str)
这个函数,而该函数的第二个参数是字符串切片类型,因此必须要保证 +
和 +=
的第二个操作数是字符串切片类型。
通过这种方式得到的一个新的字符串。可以加 mut
修饰符,也可以不加。
add
add() 方法的定义:
fn add(self, s: &str) -> String
fn main() {let s1 = String::from("hello,");let s2 = String::from("world!");// 在下句中,s1 的所有权被转移走了,因此后面不能再使用 s1let s3 = s1 + &s2;assert_eq!(s3,"hello,world!");// 下面的语句如果去掉注释,就会报错// println!("{}",s1);
}
为什么 s1 + &s2
中的 s1 的所有权被转移走了?
因为 add(self, s: &str)
函数的第一个参数 self
指向自身(Python 中的 self,Java 中的 this),也就是说该函数的作用是:在原字符串 s1 上追加 s2,最后得到一个新的字符串。
由于需要将 s1 传递给形参 self,自然就需要将所有权转移给 self,又因为 self 是不可变引用,所以当 add() 函数执行完成后将释放掉 self 的内存空间。此时外部的 s1 也就失效了!
由此可以推导和理解:
let s1 = String::from("A");
let s2 = String::from("B");
let s3 = String::from("C");let s = s1 + "-" + &s2 + "-" + &s3; // A-B-C
// let s = s1 + "-" + &s2 + &s3; // A-BC
问题:这 s1 s2 s3 这里面哪些失去了所有权?
答案:仅 s1 失去了所有权。(我相信很好理解。)
format
let s1 = "hello";
let s2 = String::from("rust");
let s = format!("{} and {}", s1, s2);
println!("{}", s); // hello and rust
字符串转移
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);// 换行了也会保持之前的字符串格式// 使用\忽略换行符let long_string = "String literalscan span multiple lines.The linebreak and indentation here ->\<- can be escaped too!";println!("{}", long_string);
}
希望保持字符串的原样,不要转义:
fn main() {println!("{}", "hello \\x52\\x75\\x73\\x74");let raw_str = r"Escapes don't work here: \x3F \u{211D}";println!("{}", raw_str);// 如果字符串包含双引号,可以在开头和结尾加 #let quotes = r#"And then I said: "There is no escape!""#;println!("{}", quotes);// 如果字符串中包含 # 号,可以在开头和结尾加多个 # 号,最多加255个,只需保证与字符串中连续 # 号的个数不超过开头和结尾的 # 号的个数即可let longer_delimiter = r###"A string with "# in it. And even "##!"###;println!("{}", longer_delimiter);
}
操作 UTF-8 字符串
let s = "中国人";let a = &s[0..2];println!("{}",a);
以字符的形式操作字符串
通过 chars() 函数
let s = "反卷大队长Blog";
for ch in s.chars() {print!("{} ", ch);
}
以字节的形式操作字符串
通过 bytes() 函数
let s = "反卷大队长Blog";
for ch in s.bytes() {print!("{} ", ch);
}
229 143 141 229 141 183 229 164 167 233 152 159 233 149 191 66 108 111 103
------------------------------------------
229 143 141 反
229 141 183 卷
229 164 167 大
233 152 159 队
233 149 191 长
66 B
108 l
111 o
103 g
获取子串
由于 Rust 的底层是一个字节数组,而非字符,没法绝对准确的确保边界,所以没办法很方便的得到字符串的子串。
只能借助第三方库,或者自己实现相关功能,可以考虑尝试下这个库:utf8_slice。
字符串深度剖析
为什么 String 可变,而 &str 不可变?
- &str 字面量是直接硬编码到二进制文件中的。(其实就是各个段中)
- String 是存储在堆中的。
向系统申请的内存空间总归是需要归还的,那么怎么归还呢?
- 采用自动 GC 的话就会牺牲性能。
- 采用手动管理内存的话就会牺牲安全。
解决方案:变量在离开作用域后,就自动释放其占用的内存。
https://fanjuanddz.com/article/42