【问题标题】:Why is it discouraged to accept a reference to a String (&String), Vec (&Vec), or Box (&Box) as a function argument?为什么不鼓励接受对 String (&String)、Vec (&Vec) 或 Box (&Box) 的引用作为函数参数?
【发布时间】:2017-02-21 16:55:25
【问题描述】:

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

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
}

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

【问题讨论】:

    标签: string reference rust borrowing


    【解决方案1】:

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


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

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

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

    相反,您应该接受一个字符串切片 (&amp;str)、一个切片 (&amp;[T]),或者只是一个引用(&amp;T)。 &amp;String&amp;Vec&lt;T&gt;&amp;Box&lt;T&gt; 将被自动强制(通过 deref coercion)分别为 &amp;str&amp;[T]&amp;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") 分配的 String 调用。 total_price 可以通过引用数组 (&amp;[1, 2, 3]) 分配的 Vec 来调用。


    如果您想在StringVec&lt;T&gt; 中添加或删除项目,您可以采用可变引用&amp;mut String&amp;mut Vec&lt;T&gt;):

    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);
    }
    

    对于切片,您还可以接受&amp;mut [T]&amp;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();
        }
    }
    

    【讨论】:

    • 一开始的tl;dr怎么样?这个答案已经有点长了。像“&amp;str”这样的东西更通用(如:施加较少的限制)而不会降低功能”?另外:我认为第 3 点通常并不重要。通常Vecs 和Strings 将存在于堆栈中,甚至通常位于当前堆栈帧附近的某个地方。堆栈通常是热的,取消引用将从 CPU 缓存中提供。
    • @Shepmaster:关于分配成本,在谈到强制分配时,可能值得一提的是子字符串/切片的特殊问题。 total_price(&amp;prices[0..4]) 不需要为切片分配新向量。
    • 这是一个很好的答案。我刚刚开始使用 Rust,并且一直在搞清楚什么时候应该使用 &amp;strwhy (来自 Python,所以我通常不明确处理类型)。完美地清除了所有这些
    • 我缺少有关为什么需要额外分配的信息。字符串存储在堆上,当接受 &String 作为参数时,为什么 Rust 不只是传递存储在堆栈上的指向堆空间的指针,我不明白为什么传递 &String 需要额外分配,传递一个字符串slice 还应该要求发送一个存储在堆栈上的指针,该指针指向堆空间?
    • 为了完整起见,应该注意的是,接受&amp;String&amp;Vec 的唯一时间是当您需要访问仅存在于这些上的&amp;self 方法时,即capacity。此外,这些都不适用于 mutable 借用,即 &amp;mut Vec&amp;mut String,当您想要扩大或缩小集合时,它们是合法需要的。
    【解决方案2】:

    除了Shepmaster's answer,接受&amp;str(以及类似的&amp;[T]等)的另一个原因是因为所有其他类型除了String&amp;str也满足Deref&lt;Target = str&gt;。最著名的例子之一是Cow&lt;str&gt;,它让您可以非常灵活地处理自己的数据还是借用的数据。

    如果你有:

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

    但您需要使用Cow&lt;str&gt; 调用它,您必须这样做:

    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());
    

    当你将参数类型更改为&amp;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);
    

    接受&amp;str 使调用你的函数更加统一和方便,“最简单”的方式现在也是最高效的。这些示例也适用于 Cow&lt;[T]&gt; 等。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-03-16
      • 1970-01-01
      • 2023-03-29
      • 2015-05-29
      相关资源
      最近更新 更多