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


127

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

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

我还编写了引用Vec或的代码Box

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

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

但是,我收到一些反馈,认为这样做不是一个好主意。为什么不?

Answers:


162

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


  1. 使用a String或a的主要原因之一Vec是因为它们允许增加或减少容量。但是,当您接受不可变的引用时,就不能在Vec或上使用任何这些有趣的方法String

  2. 接受&String&Vec&Box需要在堆上进行分配后,才能调用函数的参数。接受a &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以获得a String,然后执行第二次取消以最终到达&str

相反,您应该接受字符串slice&str),slice&[T])或仅引用(&T)。甲&String&Vec<T>&Box<T>将被自动强制转换为&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


如果您想从String或添加或删除项目,则Vec<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();
    }
}

5
一开始的tl; dr怎么样?这个答案已经有些长了。诸如“ &str更通用(例如:施加更少的限制)而不降低功能”之类的东西?另外:我认为第3点通常并不那么重要。通常Vecs和Strings将存在于堆栈中,甚至经常位于当前堆栈帧附近。堆栈通常很热,并且将从CPU缓存中进行取消引用。
卢卡斯·卡尔伯托德

3
@Shepmaster:关于分配成本,在谈论强制分配时,可能值得一提的是子字符串/切片的特定问题。total_price(&prices[0..4])不需要为切片分配新的向量。
Matthieu M.

4
这是一个很好的答案。我才刚开始使用Rust,并且被搞清楚了什么时候应该使用a &str为什么(来自Python,所以我通常不明确处理类型)。清除所有这一切完美妆容
C.Nivs

2
很棒的参数提示。只是需要一个疑问:“接受&String,&Vec或&Box也需要分配才能调用该方法。” ...为什么会这样?您能否指出文档中我可以详细阅读的部分?(我是初学者)。另外,我们可以对返回类型有类似的提示吗?
纳瓦兹

2
我缺少有关为什么需要额外分配的信息。字符串存储在堆上,当接受&String作为参数时,为什么Rust不只是传递存储在堆栈上的指向堆空间的指针,我不明白为什么传递&String会需要额外的分配,传递字符串slice还应该要求发送存储在堆栈上的指向堆空间的指针吗?
cjohansson

22

除了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]>etc 一起使用。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.