为什么在Rust中使用&String、&Vec和&Box是不好的风格?

面向初学者的在线Python训练器

轻松学习Python,无需理论负担。在浏览器中直接解决带自动检查的实践任务,无需安装任何东西。

开始课程

引言:所有Rust新手都会掉入的陷阱

\n\n

你正在学习Rust。你已经理解了所有权和借用的概念。你正在编写第一个重要的函数,心想:“啊,为了不移动字符串,我要传递一个引用!”。于是你写下:

\n\n
fn print_string(s: &String) {\n    println!("{}", s);\n}
\n\n

编译器很开心。代码能运行。你也很开心。但问题来了:有经验的Rust开发者看到你的代码会皱眉头。为什么?明明一切都能运行!

\n\n

这个问题是StackOverflow上最热门的问题之一。成千上万的开发者都会在这个看似微不足道的细节上栽跟头。今天我们来分析一下,为什么函数签名中的&String、&Vec和&Box是一个危险信号,以及如何正确地编写代码。

\n\n

问题1:隐式的类型限制(类型强制转换)

\n\n

Rust拥有强大的自动类型转换系统,但仅限于单向转换。这被称为Deref强制转换(通过解引用进行转换)。

\n\n

原理很简单:如果你有一个指向String的引用,Rust自动可以将其转换为指向&str的引用。同样的情况也适用于&Vec<T> → &[T]和&Box<T> → &T。

\n\n

看看区别:

\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

实际示例:子串搜索

\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
  • 容量
  • \n
\n\n

当你传递&String时,你传递的是指向这个结构体的引用。为了访问数据,编译器需要:

\n
    \n
  1. 解引用&String → 获取String结构体
  2. \n
  3. 从结构体中获取指向堆上数据的指针
  4. \n
  5. 解引用这个指针 → 获取字符串的字节
  6. \n
\n\n

当你传递&str时,你传递的是一个胖指针,它只包含两个字段:

\n
    \n
  • 指向数据的指针
  • \n
  • 长度
  • \n
\n\n

编译器只需要做一步——解引用指针。间接引用越少,代码越快。虽然差异微乎其微,但在热点循环或传递到

博客

书籍推荐