为什么不鼓励接受对String (&String)、Vec (&Vec)或Box (&Box)的引用作为函数参数?

我写了一些Rust代码,以&String作为参数:

fn awesome_greeting(name: &String) {
println!("Wow, you are awesome, {}!", name);
}

我还编写了接受VecBox引用的代码:

fn total_price(prices: &Vec<i32>) -> i32 {
prices.iter().sum()
}


fn is_even(value: &Box<i32>) -> bool {
**value % 2 == 0
}

然而,我收到了一些反馈,说这样做不是一个好主意。为什么不呢?

19991 次浏览

TL;DR:可以使用&str&[T]&T来允许更通用的代码。


  1. 使用StringVec的一个主要原因是它们允许增加或减少容量。然而,当你接受一个不可变引用时,你不能在VecString上使用任何这些有趣的方法。

  2. 在调用函数之前,接受&String&Vec&Box需要分配到堆上的参数。接受&str可以使用字符串字面值(保存在程序数据中),接受&[T]&T可以使用堆栈分配的数组或变量。不必要的分配是性能损失。当你试图在测试或main方法中调用这些方法时,这通常会立即暴露:

    awesome_greeting(&String::from("Anna"));
    
    total_price(&vec![42, 13, 1337])
    
    is_even(&Box::new(42))
    
  3. 另一个性能考虑因素是&String&Vec&Box引入了一个不必要的间接层,因为你必须解引用&String来获得String,然后执行第二次解引用以结束在&str

相反,你应该接受一个字符串切 (&str),一个&[T]0 (&[T]),或者只是一个引用(&T)。&String&Vec<T>&Box<T>将分别被自动强制(通过&[T]1)转换为&str&[T]&T

fn awesome_greeting(name: &str) {
println!("Wow, you are awesome, {}!", name);
}
fn total_price(prices: &[i32]) -> i32 {
prices.iter().sum()
}
fn is_even(value: &i32) -> bool {
*value % 2 == 0
}

现在您可以使用更广泛的类型集调用这些方法。例如,awesome_greeting可以用一个字符串字面值("Anna") 调用一个已分配的Stringtotal_price可以通过引用数组(&[1, 2, 3]) 和已分配的Vec来调用。


如果你想从StringVec<T>中添加或删除项,你可以使用可变参考 (&mut String&mut Vec<T>):

fn add_greeting_target(greeting: &mut String) {
greeting.push_str("world!");
}
fn add_candy_prices(prices: &mut Vec<i32>) {
prices.push(5);
prices.push(25);
}

特别是对于切片,你也可以接受&mut [T]&mut str。这允许你在切片中改变一个特定的值,但你不能改变切片中项目的数量(这意味着它对字符串非常有限):

fn reset_first_price(prices: &mut [i32]) {
prices[0] = 0;
}
fn lowercase_first_ascii_character(s: &mut str) {
if let Some(f) = s.get_mut(0..1) {
f.make_ascii_lowercase();
}
}

除了Shepmaster的回答之外,接受&str(以及类似的&[T]等)的另一个原因是,所有其他类型除了String&str也满足Deref<Target = str>。最著名的例子之一是Cow<str>,它允许你非常灵活地处理自己拥有的还是借来的数据。

如果你有:

fn awesome_greeting(name: &String) {
println!("Wow, you are awesome, {}!", name);
}

但是你需要用Cow<str>来调用它,你必须这样做:

let c: Cow<str> = Cow::from("hello");
// Allocate an owned String from a str reference and then makes a reference to it anyway!
awesome_greeting(&c.to_string());

当你将参数类型更改为&str时,你可以无缝地使用Cow,而不需要任何不必要的分配,就像使用String一样:

let c: Cow<str> = Cow::from("hello");
// Just pass the same reference along
awesome_greeting(&c);


let c: Cow<str> = Cow::from(String::from("hello"));
// Pass a reference to the owned string that you already have
awesome_greeting(&c);

接受&str使函数调用更加统一和方便,而且“最简单”的方法现在也是最有效的方法。这些例子也适用于Cow<[T]>等。

建议使用&str而不是&String,因为&str也满足&String,它可以用于拥有的字符串和字符串切片,但不能用于其他方式:

use std::borrow::Cow;


fn greeting_one(name: &String) {
println!("Wow, you are awesome, {}!", name);
}


fn greeting_two(name: &str) {
println!("Wow, you are awesome, {}!", name);
}


fn main() {
let s1 = "John Doe".to_string();
let s2 = "Jenny Doe";
let s3 = Cow::Borrowed("Sally Doe");
let s4 = Cow::Owned("Sally Doe".to_string());


greeting_one(&s1);
// greeting_one(&s2);  // Does not compile
// greeting_one(&s3);  // Does not compile
greeting_one(&s4);
    

greeting_two(&s1);
greeting_two(s2);
greeting_two(&s3);
greeting_two(&s4);
}

使用向量来操作文本从来都不是一个好主意,甚至不值得讨论,因为您将失去所有的健全性检查和性能优化。字符串类型在内部使用vector。记住,为了提高存储效率,Rust对字符串使用UTF-8。如果你用向量,你必须重复所有艰难的工作。除此之外,借用向量或盒装值应该是可以的。