下面用一句话先概括:
String 是拥有所有权、可变、堆上的字符串;&String 是对 String 的引用;&str 是字符串切片/视图;字符串字面量 "hello" 的类型通常是 &'static str。
一、四者核心区别
| 类型 | 中文理解 | 是否拥有数据 | 是否可变 | 常见来源 |
|---|---|---|---|---|
String |
字符串对象 / 字符串容器 | 拥有 | 可变 | String::from("abc") |
&String |
String 的引用 |
不拥有 | 通常不可变 | &s |
&str |
字符串切片 / 字符串视图 | 不拥有 | 不可变 | "abc"、s.as_str() |
| 字符串字面量 | 写死在代码里的字符串 | 程序静态区拥有 | 不可变 | "hello" |
二、String 是什么?
String 是 Rust 标准库提供的可增长、可修改、拥有所有权的字符串类型。
它的数据通常存放在堆内存中。
fn main() {
let mut s = String::from("hello");
s.push_str(", rust");
println!("{}", s);
}
输出:
hello, rust
这里的 s 是一个 String,它拥有 "hello, rust" 这段字符串数据。
三、&String 是什么?
&String 是对 String 的引用。
它本身不拥有字符串数据,只是借用别人手里的 String。
fn main() {
let s = String::from("hello");
let r: &String = &s;
println!("{}", r);
}
可以理解成:
s 拥有 String 数据 r 只是指向 s
也就是说:
let s = String::from("hello");
let r = &s;
r 不能随便修改 s,因为它只是不可变引用。
四、&str 是什么?
&str 叫做字符串切片,也可以理解为“字符串的一段视图”。
它不拥有数据,只是指向某段 UTF-8 字符串内容。
fn main() {
let s = String::from("hello rust");
let part: &str = &s[0..5];
println!("{}", part);
}
输出:
hello
这里:
&s[0..5]
得到的是 &str,它只表示 String 中的一部分。
五、字符串字面量是什么?
字符串字面量就是代码里直接写的字符串:
let s = "hello";
很多初学者会以为 s 是 String,但其实不是。
它的类型是:
&'static str
也就是:
let s: &'static str = "hello";
'static 表示这段字符串在程序整个运行期间都有效。
它通常存放在程序的只读静态区域,不在堆上。
fn main() {
let s = "hello";
println!("{}", s);
}
这里的 s 是 &str,不是 String。
六、它们之间的关系
可以这样理解:
String └── 可以借用成 &String └── 可以转换/借用成 &str 字符串字面量 "hello" └── 本身就是 &'static str
例如:
fn main() {
let s1: String = String::from("hello");
let r1: &String = &s1;
let r2: &str = &s1;
let literal: &str = "hello";
}
其中:
let r2: &str = &s1;
之所以能成立,是因为
String可以自动解引用成str,所以&String经常可以被当成&str使用。
七、函数参数中应该用哪个?
通常推荐:
fn print_msg(msg: &str) {
println!("{}", msg);
}
而不是:
fn print_msg(msg: &String) {
println!("{}", msg);
}
为什么?
因为 &str 更通用。
fn print_msg(msg: &str) {
println!("{}", msg);
}
fn main() {
let s1 = String::from("hello");
let s2 = "world";
print_msg(&s1);
print_msg(s2);
}
这个函数既能接收 String,也能接收字符串字面量。
如果写成:
fn print_msg(msg: &String) {
println!("{}", msg);
}
那它只能接收 String 的引用,不能直接接收 "hello"。
八、对比示例:String 和 &str
1. String 可以修改
fn main() {
let mut s = String::from("hello");
s.push_str(" rust");
println!("{}", s);
}
输出:
hello rust
2. &str 不能修改
fn main() {
let s: &str = "hello";
// s.push_str(" rust"); // 错误,&str 没有 push_str 方法
println!("{}", s);
}
&str 只是一个视图,不负责管理和扩展字符串内容。
九、String 转 &str
常见方式有三种:
fn main() {
let s = String::from("hello");
let a: &str = &s;
let b: &str = s.as_str();
let c: &str = &s[..];
println!("{}, {}, {}", a, b, c);
}
推荐常用:
s.as_str()
或者函数传参时直接:
print_msg(&s);
十、&str 转 String
常见方式:
fn main() {
let s1: &str = "hello";
let s2: String = s1.to_string();
let s3: String = String::from(s1);
println!("{}", s2);
println!("{}", s3);
}
两种都可以。
十一、内存角度理解
String
let s = String::from("hello");
大致可以理解成:
栈上:
s = {
ptr: 指向堆内存,
len: 5,
capacity: 5
}
堆上:
hello
String 自己管理这块堆内存。
&str
let s = "hello";
大致可以理解成:
s = {
ptr: 指向某段字符串数据,
len: 5
}
它只是一个引用视图,不管理内存。
十二、为什么有时候 &String 不推荐?
例如:
fn print_name(name: &String) {
println!("{}", name);
}
这个写法可以运行,但不够灵活。
更推荐:
fn print_name(name: &str) {
println!("{}", name);
}
因为这样既可以传:
let name = String::from("Alice");
print_name(&name);
也可以传:
print_name("Bob");
所以在 Rust 里,函数参数能用 &str 就尽量用 &str。
十三、常见写法总结
创建 String
let s1 = String::from("hello");
let s2 = "hello".to_string();
let s3 = String::new();
创建 &str
let s1 = "hello";
let s2 = String::from("hello");
let s3 = s2.as_str();
函数接收字符串
推荐:
fn show(s: &str) {
println!("{}", s);
}
调用:
fn main() {
let a = String::from("hello");
let b = "world";
show(&a);
show(b);
}
十四、容易混淆的点
1. "hello" 不是 String
let s = "hello";
这里 s 是:
&str
不是:
String
如果要 String:
let s = String::from("hello");
或者:
let s = "hello".to_string();
2. &String 和 &str 不完全一样
let s = String::from("hello");
let a: &String = &s;
let b: &str = &s;
a 是对整个 String 对象的引用。
b 是对字符串内容的切片引用。
通常函数参数更应该使用 &str。
3. String 会发生所有权移动
fn take(s: String) {
println!("{}", s);
}
fn main() {
let s = String::from("hello");
take(s);
// println!("{}", s); // 错误,s 已经被移动
}
如果不想转移所有权,就用引用:
fn take(s: &str) {
println!("{}", s);
}
fn main() {
let s = String::from("hello");
take(&s);
println!("{}", s); // 可以继续使用
}
十五、一张图理解
String
拥有字符串数据,可修改
例如:String::from("hello")
&String
借用一个 String
例如:&s
&str
借用一段字符串内容
例如:"hello"、s.as_str()、&s[0..2]
字符串字面量
代码里直接写的字符串
例如:"hello"
类型:&'static str
十六、实际开发建议
记住这几个原则:
| 场景 | 推荐类型 |
|---|---|
| 需要拥有字符串并修改 | String |
| 只是读取字符串参数 | &str |
| 函数参数 | 优先 &str |
| 需要保存到结构体里 | 多数用 String |
| 字面量常量 | &'static str |
| 不推荐作为普通函数参数 | &String |
例如:
struct User {
name: String,
}
fn greet(name: &str) {
println!("Hello, {}", name);
}
fn main() {
let user = User {
name: String::from("Alice"),
};
greet(&user.name);
greet("Bob");
}
这个例子里:
name: String
因为结构体需要拥有用户名。
fn greet(name: &str)
因为函数只是读取,不需要拥有。