引言:所有Rust新手都会掉入的陷阱
\n\n你正在学习Rust。你已经理解了所有权和借用的概念。你正在编写第一个重要的函数,心想:“啊,为了不移动字符串,我要传递一个引用!”。于是你写下:
\n\nfn print_string(s: &String) {\n println!("{}", s);\n}\n\n编译器很开心。代码能运行。你也很开心。但问题来了:有经验的Rust开发者看到你的代码会皱眉头。为什么?明明一切都能运行!
\n\n这个问题是StackOverflow上最热门的问题之一。成千上万的开发者都会在这个看似微不足道的细节上栽跟头。今天我们来分析一下,为什么函数签名中的&String、&Vec和&Box是一个危险信号,以及如何正确地编写代码。
问题1:隐式的类型限制(类型强制转换)
\n\nRust拥有强大的自动类型转换系统,但仅限于单向转换。这被称为Deref强制转换(通过解引用进行转换)。
\n\n原理很简单:如果你有一个指向String的引用,Rust自动可以将其转换为指向&str的引用。同样的情况也适用于&Vec<T> → &[T]和&Box<T> → &T。
看看区别:
\n\n// 不好:只接受 &String\nfn bad(s: &String) {\n println!("{}", s);\n}\n\n// 好:接受 &str\nfn good(s: &str) {\n println!("{}", s);\n}\n\nfn main() {\n let owned = String::from("你好,世界!");\n let slice = "你好,切片!";\n \n bad(&owned); // 可以运行\n // bad(slice); // 错误!slice是 &str,而不是 &String\n \n good(&owned); // 可以运行(自动转换)\n good(slice); // 可以运行(它已经是 &str)\n}\n\n看到了吗?good函数接受任何字符串:既可以是拥有所有权的String,也可以是字符串字面量&str,甚至可以是字符串的一部分。而bad函数只接受String。这是一个不必要的限制。
实际示例:子串搜索
\n\n// 受限的搜索\nfn find_in_string(haystack: &String, needle: &str) -> bool {\n haystack.contains(needle)\n}\n\n// 通用的搜索\nfn find_in_str(haystack: &str, needle: &str) -> bool {\n haystack.contains(needle)\n}\n\nfn main() {\n let my_string = String::from("Rust is awesome");\n let my_str = "Rust is awesome";\n \n // 只适用于 String\n find_in_string(&my_string, "Rust");\n // find_in_str(my_str, "Rust"); // 编译错误!\n \n // 两者都适用\n find_in_str(&my_string, "Rust");\n find_in_str(my_str, "Rust");\n}\n\n问题2:性能与多余间接引用
\n\n让我们看看“引擎盖”下面。在内存中String是什么?它是一个包含三个字段的结构体:
- \n
- 指向堆上数据的指针 \n
- 字符串长度 \n
- 容量 \n
当你传递&String时,你传递的是指向这个结构体的引用。为了访问数据,编译器需要:
- \n
- 解引用
&String→ 获取String结构体 \n - 从结构体中获取指向堆上数据的指针 \n
- 解引用这个指针 → 获取字符串的字节 \n
当你传递&str时,你传递的是一个胖指针,它只包含两个字段:
- \n
- 指向数据的指针 \n
- 长度 \n
编译器只需要做一步——解引用指针。间接引用越少,代码越快。虽然差异微乎其微,但在热点循环或传递到