为什么不建议将&String、&Vec或&Box作为函数参数接受?

248

我写了一些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
}

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

4个回答

319

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


  1. One of the main reasons to use a String or a Vec is because they allow increasing or decreasing the capacity. However, when you accept an immutable reference, you cannot use any of those interesting methods on the Vec or String.

  2. Accepting a &String, &Vec or &Box also requires the argument to be allocated on the heap before you can call the function. Accepting a &str allows a string literal (saved in the program data) and accepting a &[T] or &T allows a stack-allocated array or variable. Unnecessary allocation is a performance loss. This is usually exposed right away when you try to call these methods in a test or a main method:

    awesome_greeting(&String::from("Anna"));
    
    total_price(&vec![42, 13, 1337])
    
    is_even(&Box::new(42))
    
  3. Another performance consideration is that &String, &Vec and &Box introduce an unnecessary layer of indirection as you have to dereference the &String to get a String and then perform a second dereference to end up at &str.

相反,您应该接受一个字符串切片 (&str)、一个切片 (&[T]) 或者只是一个引用 (&T)。一个 &String&Vec<T> 或者 &Box<T> 将会自动强制转换 (通过deref coercion) 分别为 &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")或分配的String进行调用。total_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();
    }
}

16
开头可以加上一个“tl;dr”吗?这个答案已经有点长了。可以写成“&str更普遍(即:不限制使用范围)而且没有降低功能”。另外,我认为第三点通常并不那么重要。通常VecString会在栈上存储,甚至会在当前的栈框架附近存储。栈通常是热的,并且解引用将从CPU缓存中提供服务。 - Lukas Kalbertodt
5
关于分配成本,当谈到必须分配时,提到子字符串/切片的特定问题可能是值得一提的。使用total_price(&prices[0..4])不需要为切片分配新的向量。 - Matthieu M.
10
这是一个很好的回答。我刚开始学Rust,一直在琢磨什么时候应该使用&str以及为什么要这样做(因为我以前学的是Python,通常不需要明确处理类型)。这个回答完美地解决了我的困惑。 - C.Nivs
6
我缺少有关为什么需要额外分配内存的信息。当将&String作为参数传递时,字符串被存储在堆上,为什么Rust不只是传递一个存储在堆空间中指向该字符串的栈上指针?我不明白为什么传递一个&String需要额外的分配,传递一个字符串切片也应该需要发送一个存储在栈上的指向堆空间的指针吧? - cjohansson
9
为了完整起见,需要指出仅在需要访问仅存在于&String&Vec上的&self方法(即capacity)时,接受&String&Vec才有意义。此外,所有这些内容都不适用于可变借用,即&mut Vec&mut String,当您想要增加或缩小集合时,这些是合法需要的。 - Arnavion
显示剩余4条评论

54
除了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时,你可以像使用String一样无缝地使用Cow,而不会有任何不必要的分配:
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]>等。

0
为了减少代码重复并允许重用。
建议使用&str而不是&String,因为&str也满足&String或其他借用底层utf-8值的类型。这使得函数可用于拥有字符串和字符串切片,但反过来则不行:
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);
}

使用向量来操作文本从来都不是一个好主意,甚至不值得讨论,因为你将失去编译器提供的所有合理性检查和性能优化。

字符串类型在内部已经使用了向量。请记住,Rust 使用 UTF-8 作为字符串的存储方式以提高效率。如果您使用向量,则必须重复所有的努力。除此之外,借用向量或盒装值应该是可以的。

如果您了解不同字符串类型之间的区别,所有这些解释都会变得更加清晰。

Rust 的 `String` 和 `str` 有什么区别?


0

因为这些类型可以被强制转换,所以如果我们使用这些类型,函数将接受更少的类型:

1- 对字符串的引用可以被强制转换为字符串切片。例如创建一个函数:

fn count_wovels(words:&String)->usize{
    let wovels_count=words.chars().into_iter().filter(|x|(*x=='a') | (*x=='e')| (*x=='i')| (*x=='o')|(*x=='u')).count();
    wovels_count
}

如果你传递&str,它将不会被接受:

let name="yilmaz".to_string();
println!("{}",count_wovels(&name));
// this is not allowed because argument should be &String but we are passing str
// println!("{}",wovels("yilmaz"))

但是如果该函数接受 &str 作为参数

// words:&str
fn count_wovels(words:&str)->usize{ ... }

我们可以将这两种类型都传递给函数

let name="yilmaz".to_string();
println!("{}",count_wovels(&name));
println!("{}",wovels("yilmaz"))

通过这样,我们的函数可以接受更多类型

2- 同样地,对于 Box 的引用 &Box[T],将被强制转换为 Box 内部值的引用 Box[&T]。例如

fn length(name:&Box<&str>){
    println!("lenght  {}",name.len())
}

这个只接受 &Box<&str> 类型

let boxed_str=Box::new("Hello");
length(&boxed_str);

// expected reference `&Box<&str>` found reference `&'static str`
// length("hello")

如果我们将&str作为类型传递,就可以同时传递这两种类型

3- 引用Vec和数组之间也存在类似的关系

fn square(nums:&Vec<i32>){
    for num in nums{
        println!("square of {} is {}",num,num*num)
    }
}
fn main(){
    let nums=vec![1,2,3,4,5];
    let nums_array=[1,2,3,4,5];
    // only &Vec<i32> is accepted
    square(&nums);
    // mismatched types: mismatched types expected reference `&Vec<i32>` found reference `&[{integer}; 5]`
    //square(&nums_array)
}

这适用于两种类型

fn square(nums:&[i32]){..}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接