背景
这是Rust九九八十难的第五篇。关于字符串这块,平常在用的时候,有没有感觉有点不一样。其他语言如java,js等,字符串就是字符串String,偶尔用下char,很好区分。在rust里,字符串分为 String
类型和 str
类型 ,有时候传 &String
类型 或者 &str
,用起来有点儿乱,有时传哪个都能跑,有时仅一个能用,让人火大。今天整理下 String
和 str
的区别,最后附个速查表,方便快速使用。
一、String和str的基本定义
熟悉的同学可以直接跳过,这块做个简单说明。
1、String
-
String
: 是封装了Vec的结构体。在堆上分配内存,支持修改扩容。rust栈: [String { ptr, len, cap }] | v 堆: [ h e l l o ]
-
&String
: 加上&就是对String
结构体的不可变借用(&符号不了解的可以看下这篇: 还在分不清 Rust 的 & 怎么用?10 分钟带你彻底搞懂)rust栈: [String { ptr, len, cap }] 栈: [&String] -----> String 结构体 | v 堆数据
-
&mut String
: 继续加mut就是可变借用,可以修改字符串:push、pop、clear、resize 等。rust栈: [String { ptr, len, cap }] 栈: [&mut String] --> String 结构体(可修改)
2、str
-
str
: 不能直接用,也很少用(比如借助Box,Box使用可以看这篇)。因为Rust 编译器在编译期需要知道一个类型的 大小 (size) ,才能在栈上分配它。官方文档定义str
是DST (Dynamically Sized Type),编译时不知道大小。它本质上是 一段 UTF-8 字节序列,大小只有在运行时才知道。所以编译器没法说 "str
类型占多少字节"。要用必须放在引用或智能指针里。 -
&str
:是对字符串内容的不可变借用。有三个来源:String、字符串字面量、甚至某个切片。如下面的示意图:rust&String 在栈: { ptr -> 堆数据, len, cap } &str 在栈: { ptr -> 堆数据, len } 堆: [ h e l l o ]
可以对比的看,
&String借用的是整个 String 对象
,&str借用的是堆中的字符串数据片段
。 -
&mut str
:对某段字符串内容的可变借用。很少单独使用,多见于切片操作:rustlet mut s = String::from("hello"); let slice: &mut str = &mut s[..]; // 可变切片
3、官方链接
String
类型 :doc.rust-lang.org/std/string/...str
类型 :doc.rust-lang.org/std/primiti...- Rust By Example:字符串章节 :doc.rust-lang.org/stable/rust...
二、String与str的相互转换
上面说到,&String是对 String 的不可变借用
,而&str可以看成是字符串的通用视图,不管底层是 String 还是字面量 "hello"
。在Rust 里 &String
和 &str
的转换非常自然:
1、&String 转 &str
-
自动解引用(Deref coercion ) 在大多数函数调用或赋值场景下,
&String
会自动转换成&str
。rustfn print_str(s: &str) { println!("{}", s); } fn main() { let s = String::from("hello"); print_str(&s); // &String 自动转成 &str }
-
手动调用
.as_str()
:rustlet s = String::from("world"); let slice: &str = s.as_str();
2、&str 转 String
-
克隆数据(堆分配新的
String
),有了新的所有权:rustlet s: &str = "hello"; let string: String = s.to_string();// 新的 String,堆上存放 "hello" let string2: String = String::from(s);
-
如果只是想借用,不需要转成
String
,直接用&str
就行。
三、使用场景
1、str
常用场景
-
&str
-
更加通用和高效,大多数函数参数都应该写成
&str
。它能同时接受字面量、String
、甚至别的切片。rustfn greet(name: &str) { println!("Hello, {}!", name); } fn main() { greet("Alice"); // 字面量 greet(&String::from("Bob")); // &String }
-
定义字符串字面量(放在了只读数据段),零开销,无需堆分配,自动安全管理。适合固定文本、常量配置、消息模板
ruststatic GREETING: &str = "Hello, world!"; fn main() { println!("{}", GREETING); }
-
定义原始字面量(r"...")
rustlet raw: &str = r"hello\nworld"; // 内容是: h e l l o \ n w o r l d //不处理转义字符,可以用多个 # 来包裹,避免和内部引号冲突: let raw: &str = r#"a string with "quotes""#; let raw2: &str = r##"raw with "# inside"##;
-
定义字节字符串字面量(b"...")
rustlet bytes: &[u8; 5] = b"hello";
-
原始字节字符串(
br"..."
或br#"..."#
)rustlet raw_bytes: &[u8] = br"hello\nworld"; let raw_bytes2: &[u8] = br#"with "quotes""#;
-
-
&mut str
-
就地修改内容长度不变,用
&mut str
更优雅,比如下面的例子:rustfn shout(buf: &mut str) { buf.make_ascii_uppercase(); } fn main() { let mut s = String::from("hello rust"); shout(&mut s); println!("{}", s); // "HELLO RUST" }
-
部分切片修改,只能用
&mut str
rustfn fix_prefix(buf: &mut str) { buf.make_ascii_uppercase(); } fn main() { let mut s = String::from("hello_world"); { let prefix = &mut s[0..5]; // "hello" fix_prefix(prefix); } println!("{}", s); // "HELLO_world" }
只有
&mut str
能直接操作子串;&mut String
操作的是整个字符串。
-
2、String常用场景
-
a、非引用方式。
当需要 可变、拥有所有权 的字符串时,用
String
。比如函数返回值。需要转移所有权常见场景:
-
需要拼接、修改字符串
-
需要把字符串存入
Vec
、HashMap
(这些容器需要拥有所有权)rustlet mut s = String::from("hi"); s.push_str(", Rust!"); // ✅ 可修改
-
-
b、
&String
- 一般不直接写函数参数为
&String
,因为这会限制调用者必须传入String
,而不能传入字面量。 - 所以 函数签名几乎总是用
&str
,除非你真的需要对String
的特殊操作(很少见)。
- 一般不直接写函数参数为
-
c、
&mut String
-
修改原有的String,传参必须用
&mut String
。典型场景:回溯、拼接、扩展、删除原来字符rustfn build_sentence(buf: &mut String) { buf.push('H'); buf.push_str("ello"); buf.push(' '); buf.push_str("Rust"); } fn shrink(buf: &mut String) { buf.push_str("abcde"); buf.pop(); // 删除 'e' buf.truncate(3); // 删除到 "abc" } fn main() { let mut s = String::new(); build_sentence(&mut s); println!("{}", s); // "Hello Rust" let mut s1 = String::new(); shrink(&mut s1); println!("{}", s1); // "abc" }
-
3、结构体的字符串怎么选
-
大多数情况 :用
String
(简单,灵活,最常见),结构体实例能够完全拥有字符串,常见于持久化数据模型(如数据库实体、配置对象)。业务开发首选,不用考虑生命周期。ruststruct User { name: String, email: String, }
-
只读数据,依赖外部生命周期 :用
&str
。如下面例子,必须加生命周期'a
,告诉编译器:User
的存活时间 ≤ 借用数据的存活时间。其他如果是全局字面量可以用'static。频率上不常用。ruststruct User<'a> { name: &'a str, email: &'a str, }
-
要兼顾引用和所有权 :用
Cow<'a, str>
。结构体既能接受引用的字符串,也能存拥有的字符串。如解析配置、文本处理函数的参数。做一些序列化,解析工具库的开发出场较多。rustuse std::borrow::Cow; struct User<'a> { name: Cow<'a, str>, }
-
需要优化内存布局 :用
Box<str>
。比和String
内存更紧凑,存储后不能再修改的字符串,占用更小,内存布局更稳定(适合 FFI 或内存优化场景)。嵌入式可能碰到,业务开发不建议用。ruststruct User { name: Box<str>, }
四、&str
API 与 String
兼容对照表
平常开发混着用,两者定义的方法很像,但也有区别,为了方便快速查阅,这里整理一份api对照表。
1、相同api(String
自动解引用为 str
,所以这些方法通用)
分类 | 方法 | 作用 |
---|---|---|
基本信息 | len |
返回字节长度 |
is_empty |
是否为空 | |
chars |
返回字符迭代器 | |
bytes |
返回字节迭代器 | |
char_indices |
字符+索引迭代 | |
as_ptr |
获取原始指针 | |
as_bytes |
获取字节切片 &[u8] |
|
子串检查 | contains |
是否包含子串 |
starts_with |
是否有前缀 | |
ends_with |
是否有后缀 | |
find |
查找子串位置 | |
rfind |
从右侧查找 | |
matches |
查找所有匹配 | |
match_indices |
查找所有匹配(带索引) | |
子串获取 | get |
安全切片 |
[range] |
下标切片 | |
split_at |
按下标拆分 | |
分割与迭代 | split |
按分隔符切分 |
rsplit |
从右边切分 | |
split_whitespace |
按空白切分 | |
lines |
按行切分 | |
修剪 | trim |
去掉首尾空白 |
trim_start |
去掉开头空白 | |
trim_end |
去掉结尾空白 | |
trim_matches |
去掉匹配模式 | |
转换 | to_lowercase |
转小写 (返回 String ) |
to_uppercase |
转大写 (返回 String ) |
|
to_string |
转 String |
|
repeat |
重复拼接 (返回 String ) |
|
比较 | eq |
判断相等 |
cmp |
比较顺序 |
2、只有String可用
类别 | 方法 | 作用 |
---|---|---|
容量管理 | capacity |
当前分配的容量 |
reserve |
预留额外容量 | |
reserve_exact |
精确预留容量 | |
shrink_to_fit |
释放多余容量 | |
修改 | push |
追加一个字符 |
push_str |
追加一个字符串 | |
insert |
在指定位置插入字符 | |
insert_str |
插入字符串 | |
remove |
删除某个字符 | |
retain |
按条件保留字符 | |
truncate |
截断到指定长度 | |
clear |
清空字符串 | |
make_ascii_lowercase |
ASCII 转小写 | |
make_ascii_uppercase |
ASCII 转大写 | |
转移所有权 | into_boxed_str |
转为 Box<str> |
3、只有str可用
这些方法是 切片级别 的,不会改变底层数据,通常返回新的切片或迭代器。
类别 | 方法 | 作用 |
---|---|---|
模式匹配 | strip_prefix |
去掉前缀,返回子串 |
strip_suffix |
去掉后缀,返回子串 | |
切片工具 | split_once |
按第一次匹配切分 |
rsplit_once |
按最后一次匹配切分 | |
指针工具 | as_ptr_range |
得到指针范围 |
转义工具 | escape_debug |
可打印调试转义 |
escape_default |
默认转义 | |
escape_unicode |
Unicode 转义 |
五、总结
字符串几乎是所有语言使用最广泛的结构,而且尽量做了封装,但Rust这里用起来并不舒服,它拆分String和Str。Stackoverflow上有讨论,可能跟它的设计哲学和所有权有关系。其他二和三总结了关系和场景等,熟悉的可以跳到第四节,用这个速查表,快速定位问题。
本人公众号大鱼七成饱,所有历史文章都会在上面同步。
