所有权:编译器为什么拒绝我

切片:借用一段,而不是整个

它在解决什么

这一章一路在用 &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]);   // 编译通过

它 panic 了:

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` 会变成一个悬空的东西,而这里它编译不过。