Por que é desencorajado a aceitar uma referência a uma String (& String), Vec (& Vec) ou Box (& Box) como argumento de função?

126

Eu escrevi algum código Rust que leva a &Stringcomo argumento:

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

Também escrevi código que leva em uma referência a um Vecou Box:

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

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

No entanto, recebi alguns comentários de que fazer dessa maneira não é uma boa ideia. Por que não?

Shepmaster
fonte

Respostas:

161

TL; DR: Em vez disso &str, pode-se usar &[T]ou &Tpermitir código mais genérico.


  1. Um dos principais motivos para usar a Stringou a Vecé porque eles permitem aumentar ou diminuir a capacidade. No entanto, quando você aceita uma referência imutável, não pode usar nenhum desses métodos interessantes no Vecor String.

  2. Aceitar a &String, &Vecou &Boxtambém exige que o argumento seja alocado no heap antes que você possa chamar a função. Aceitar a &strpermite um literal de cadeia de caracteres (salvo nos dados do programa) e aceitar uma &[T]ou &Tpermite uma matriz ou variável alocada por pilha. Alocação desnecessária é uma perda de desempenho. Isso geralmente é exposto imediatamente quando você tenta chamar esses métodos em um teste ou mainmétodo:

    awesome_greeting(&String::from("Anna"));
    total_price(&vec![42, 13, 1337])
    is_even(&Box::new(42))
  3. Outra consideração de desempenho é que &String, &Vece &Boxintroduza uma camada desnecessária de indireção, pois você deve desreferenciar o &Stringpara obter um Stringe executar uma segunda desreferência para terminar &str.

Em vez disso, você deve aceitar uma fatia de string ( &str), uma fatia ( &[T]) ou apenas uma referência ( &T). A &String, &Vec<T>ou &Box<T>será automaticamente coagido a a &str, &[T]ou &T, respectivamente.

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
}

Agora você pode chamar esses métodos com um conjunto mais amplo de tipos. Por exemplo, awesome_greetingpode ser chamado com uma string literal ( "Anna") ou alocada String. total_pricepode ser chamado com uma referência a uma matriz ( &[1, 2, 3]) ou alocada Vec.


Se você deseja adicionar ou remover itens do Stringou Vec<T>, pode usar uma referência mutável ( &mut Stringou &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);
}

Especificamente para fatias, você também pode aceitar um &mut [T]ou &mut str. Isso permite que você modifique um valor específico dentro da fatia, mas não é possível alterar o número de itens dentro da fatia (o que significa que é muito restrito para cadeias):

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
fonte
5
Que tal um tl; dr no começo? Essa resposta já é um pouco longa. Algo como " &stré mais geral (como: impõe menos restrições) sem capacidade reduzida"? Além disso: o ponto 3 geralmente não é tão importante assim. Normalmente, Vecs e Strings ficam na pilha e geralmente até em algum lugar próximo ao quadro atual da pilha. A pilha geralmente está quente e a desreferência será servida a partir de um cache da CPU.
Lukas Kalbertodt 12/10
3
@ Shepmaster: Em relação ao custo de alocação, vale a pena mencionar a questão específica de substrings / slice ao falar sobre alocação obrigatória. total_price(&prices[0..4])não requer a alocação de um novo vetor para a fatia.
Matthieu M.
4
Esta é uma ótima resposta. Eu estou apenas começando no Rust e estava ficando empolgado para descobrir quando eu deveria usar um &stre por quê (vindo do Python, então eu geralmente não lido explicitamente com tipos). Limpou tudo isso perfeitamente
C.Nivs
2
Dicas impressionantes sobre parâmetros. Só preciso de uma dúvida: "Aceitar um & String, & Vec ou & Box também requer uma alocação antes que você possa chamar o método." ... Por que é isso? Você poderia apontar a parte dos documentos em que posso ler isso em detalhes? (Eu sou um iniciante). Além disso, podemos ter dicas semelhantes sobre os tipos de retorno?
Nawaz
2
Faltam informações sobre por que é necessária uma alocação extra. String é armazenada na pilha, ao aceitar & String como argumento, por que Rust não passa apenas um ponteiro armazenado na pilha que aponta para o espaço de pilha, não entendo por que passar uma & String precisaria de alocação adicional, passando uma string fatia também deve exigir o envio de um ponteiro armazenado na pilha que aponta para o espaço da pilha?
Cjpegsson
22

Além da resposta de Shepmaster , outro motivo para aceitar um &str(e similarmente &[T]etc) é por causa de todos os outros tipos além String e &strque também satisfazem Deref<Target = str>. Um dos exemplos mais notáveis ​​é o Cow<str>que permite que você seja muito flexível sobre se está lidando com dados próprios ou emprestados.

Se você tem:

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

Mas você precisa chamá-lo com a Cow<str>, precisará fazer isso:

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

Ao alterar o tipo de argumento para &str, você pode usá-lo Cowperfeitamente, sem nenhuma alocação desnecessária, assim como 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);

A aceitação &strtorna a chamada da sua função mais uniforme e conveniente, e a maneira "mais fácil" agora também é a mais eficiente. Esses exemplos também funcionarão com Cow<[T]>etc.

Peter Hall
fonte