所有权:编译器为什么拒绝我
切片:借用一段,而不是整个
它在解决什么
这一章一路在用 &str,但一次都没解释它。移动那篇里
那个 &String 参数还专门标了一句「能跑但不地道」。这一篇把这笔账结掉。
先说结论,它值得单独占一行:
想借一整个,写
&String/&Vec<T>;想借一段,写&str/&[T]。 而实践中你几乎总该写后者 —— 因为它连前者的调用方一起收了。
切片就是「指向某个连续片段的借用」。它不复制数据,只记两件事: 从哪开始、有多长。
参数写 &str,两种调用方都能进来
这段代码里,first_word 既能接 String,也能接字面量:
fn first_word(s: &str) -> &str { /* ... */ }
first_word(&owned); // owned 是 String
first_word("literal here"); // 这是字面量
第一行能过,靠的是解引用强制转换(deref coercion):String 实现了
Deref<Target = str>,编译器在类型对不上时会自动插一次转换。
⚠️ 反过来不成立。把参数改成 &String之后,
字面量那一行当场 E0308 —— 它根本不是 String,没有任何转换能把它变成一个。
一个字符的改动,把一半调用方挡在了门外。
Vec 和切片是完全一样的故事:写 &[i32],
Vec、数组、以及 &v[1..3] 这样的片段全都能传进来;写 &Vec<i32>,
就只剩完整的 Vec 一种。
⇒ 记一条:参数位置上,优先写那个「借来的」类型。
| 你拥有的 | 借一整个 | 借一段(几乎总该用这个) |
|---|---|---|
String |
&String |
&str |
Vec<T> |
&Vec<T> |
&[T] |
[T; N] |
&[T; N] |
&[T] |
📌 返回值不适用这条 —— 返回时你得想清楚所有权归谁,那是 移动和生命周期的事。
UTF-8 埋的两个坑,一个在编译期一个不在
这两条必须对照着看,因为它们看起来是同一件事,实际差别很大。
坑一:数字下标,编译期就被拦下。
s[0] 编译不过,报 E0277 ——
str 压根没实现按整数索引。
原因不是「Rust 觉得你不该这么写」,而是这个操作本身有歧义: 「第 0 个」是第 0 个字节,还是第 0 个字符?在 ASCII 上两者一样, 在中文上完全不同。Rust 拒绝替你猜。
坑二:范围切片,编译器放行 —— 然后运行时炸。
let s = String::from("中文");
println!("{}", &s[0..1]); // 编译通过
end byte index 1 is not a char boundary; it is inside '中' (bytes 0..3 of string)
一个中文字在 UTF-8 下占 3 个字节,切在第 1 个字节上就是切进了字符内部。
而编译器无从知道运行时那个 1 会不会落在边界上 —— 它可能是个变量、
可能是算出来的。所以这一类只能留到运行时。
⇒ 这两条摆在一起给出的判据是:编译器拦住的那部分是有限的。 它拦得住「类型上不可能对」的写法,拦不住「类型对、值不对」的写法。 凡是下标、范围、长度,都属于后者。
越界也是运行时的事
同一个形状的还有一条:v[10] 在一个长度 3 的 Vec 上
编译通过,运行时 panic。
index out of bounds: the len is 3 but the index is 10
对照项用 .get(10),返回的是 Option,
打印出来是 None —— 程序照常跑完。
⇒ 判据很直接:下标是你自己算出来的就用 .get(),是遍历给的才用 []。
后者的越界基本只会在你写错循环时发生,那种情况下 panic 反而是想要的。
切片是借用 —— 所以它会挡住修改
最后一条把切片和前两篇接上。这段代码编译不过,
报的是 E0502:
let mut s = String::from("hello world");
let word = &s[..5];
s.clear(); // ← E0502:word 后面还要用
println!("{word}");
这是「切片到底是什么」最有说服力的证据:它不是一小段复制出来的字符串, 而是一个指向原串某一段的借用 —— 所以借用那篇的两条规则 原封不动地适用。
在别的语言里,s.clear() 之后 word 会变成一个指向已释放内存的东西
(C++)或者一个和原串脱钩的旧副本(Java/Python,因为它们的字符串不可变)。
Rust 既不悬空也不偷偷复制,它让你在编译期就面对这个选择:
要么别改原串,要么明说你要一份拷贝(to_string())。
下一步
到这里,讲的全是「编译器为什么拒绝你」,而给出的解法基本都是 「改写法,别 clone」。但真实代码里有大量场景,正确答案就是 clone —— 怎么判断是哪一种,见《什么时候就该 clone》。
全部篇目见 Rust 教程首页。
本篇示例
下面每一条都是完整的、能单独编译的程序,由npm run test:rust 在每次构建前用真的 rustc 跑一遍。 「编译不过、报 E0382」这种话在这里是被验证过的断言。 报错原文和对照项的结果由同一道闸门自动回写,会随工具链更新,但不作为断言。
字符串参数写 `&str`,两种调用方都能进来
fn first_word(s: &str) -> &str {
match s.find(' ') {
Some(i) => &s[..i],
None => s,
}
}
fn main() {
let owned = String::from("hello world");
println!("{}", first_word(&owned));
println!("{}", first_word("literal here"));
}编译通过 · 输出 "hello\nliteral\n"
对照:把参数类型改成 `&String`
fn first_word(s: &String) -> &str {
match s.find(' ') {
Some(i) => &s[..i],
None => s,
}
}
fn main() {
let owned = String::from("hello world");
println!("{}", first_word(&owned));
println!("{}", first_word("literal here"));
}编译不过:error[E0308]
`&owned` 能传进 `&str` 是因为**解引用强制转换**(deref coercion):`String` 实现了 `Deref<Target = str>`,编译器会自动插一次转换。反过来不成立 —— 对照项里字面量根本不是 `String`,传不进去。
序列参数写 `&[T]`,`Vec` 和切片都能进来
fn sum(xs: &[i32]) -> i32 {
xs.iter().sum()
}
fn main() {
let v = vec![1, 2, 3, 4];
println!("{} {}", sum(&v), sum(&v[1..3]));
}编译通过 · 输出 "10 5\n"
对照:把参数类型改成 `&Vec<i32>`
fn sum(xs: &Vec<i32>) -> i32 {
xs.iter().sum()
}
fn main() {
let v = vec![1, 2, 3, 4];
println!("{} {}", sum(&v), sum(&v[1..3]));
}编译不过:error[E0308]
和上一条是同一件事的两个面:`&Vec<i32>` 只接受完整的 `Vec`,而 `&[i32]` 连数组、切片、`Vec` 全都接受。⇒ 记一条:**参数位置上,优先写那个「借来的」类型**。
字符串不能用数字下标
fn main() {
let s = String::from("hello");
let c = s[0];
println!("{c}");
}编译不过 · error[E0277]
rustc 原文
error[E0277]: the type `str` cannot be indexed by `{integer}`
--> slice-no-integer-index.rs:3:15
|
3 | let c = s[0];
| ^ string indices are ranges of `usize`
|
= help: the trait `SliceIndex<str>` is not implemented for `{integer}`
= note: you can use `.chars().nth()` or `.bytes().nth()`
for more information, see chapter 8 in The Book: <https://doc.rust-lang.org/book/ch08-02-strings.html#indexing-into-strings>
help: `usize` implements trait `SliceIndex<T>`
--> /rustc/48a229ceaefd4985c50990b14116b6d856af0985/library/core/src/slice/index.rs:179:0
|
= note: `SliceIndex<[T]>`
--> /rustc/48a229ceaefd4985c50990b14116b6d856af0985/library/core/src/bstr/traits.rs:197:0
|
= note: `SliceIndex<ByteStr>`
= note: required for `String` to implement `Index<{integer}>`对照:改成 `s.chars().next().unwrap()`
fn main() {
let s = String::from("hello");
let c = s.chars().next().unwrap();
println!("{c}");
}编译通过,输出:"h\n"
这条是**编译期**就被拦下的:`str` 压根没实现按整数索引。原因是 UTF-8 下「第 0 个」有歧义 —— 是第 0 个字节还是第 0 个字符?两者在中文上完全不同,而 Rust 拒绝替你猜。
🚨 但按字节切是**编译得过**的 —— 然后运行时炸
fn main() {
let s = String::from("中文");
println!("{}", &s[0..1]);
}编译通过,但运行时 panic · 退出码 101
运行时说了什么
thread 'main' panicked at slice-utf8-boundary-panic.rs:3:22:
end byte index 1 is not a char boundary; it is inside '中' (bytes 0..3 of string)对照:按字符取而不是按字节切
fn main() {
let s = String::from("中文");
println!("{}", s.chars().next().unwrap());
}编译通过,输出:"中\n"
和上一条对照着看:数字下标被编译期拦下,而**范围切片没有** —— `&s[0..1]` 是合法代码,编译器无从知道运行时那个 1 会不会落在字符中间。一个中文字在 UTF-8 下占 3 个字节,切在第 1 个字节上就是切进了字符内部。
下标越界同样是运行时的事
fn main() {
let v = vec![1, 2, 3];
println!("{}", v[10]);
}编译通过,但运行时 panic · 退出码 101
运行时说了什么
thread 'main' panicked at slice-index-out-of-bounds.rs:3:21:
index out of bounds: the len is 3 but the index is 10对照:用 `v.get(10)` 代替 `v[10]`
fn main() {
let v = vec![1, 2, 3];
println!("{:?}", v.get(10));
}编译通过,输出:"None\n"
`[]` 越界 panic,`.get()` 返回 `Option`。⇒ 判据很直接:**下标是你自己算出来的就用 `.get()`,是遍历给的才用 `[]`**。
切片是借用,所以它会挡住对原串的修改
fn main() {
let mut s = String::from("hello world");
let word = &s[..5];
s.clear();
println!("{word}");
}编译不过 · error[E0502]
rustc 原文
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
--> slice-borrow-blocks-mutation.rs:4:5
|
3 | let word = &s[..5];
| - immutable borrow occurs here
4 | s.clear();
| ^^^^^^^^^ mutable borrow occurs here
5 | println!("{word}");
| ---- immutable borrow later used here对照:把切片换成一份自己的拷贝 `s[..5].to_string()`
fn main() {
let mut s = String::from("hello world");
let word = s[..5].to_string();
s.clear();
println!("{word}");
}编译通过,输出:"hello\n"
这是「切片到底是什么」最有说服力的证据:它不是一小段复制出来的字符串,而是一个**指向原串某一段的借用** —— 所以借用规则原封不动地适用。在别的语言里 `s.clear()` 之后 `word` 会变成一个悬空的东西,而这里它编译不过。