web-dev-qa-db-fra.com

Pourquoi est-il déconseillé d'accepter une référence à une chaîne (& String), Vec (& Vec) ou Box (& Box) comme argument de fonction?

J'ai écrit du code Rust qui prend un &String comme argument:

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

J'ai également écrit du code qui prend une référence à un Vec ou Box:

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

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

Cependant, j'ai reçu des commentaires selon lesquels le faire comme ça n'est pas une bonne idée. Pourquoi pas?

96
Shepmaster

TL; DR: On peut utiliser à la place &str, &[T] ou &T pour permettre un code plus générique.


  1. L'une des principales raisons d'utiliser un String ou un Vec est qu'ils permettent d'augmenter ou de diminuer la capacité. Cependant, lorsque vous acceptez une référence immuable, vous ne pouvez utiliser aucune de ces méthodes intéressantes sur Vec ou String.

  2. Accepter un &String, &Vec ou &Box aussi nécessite l'argument à allouer sur le tas avant d'appeler la fonction. Accepter un &str autorise un littéral de chaîne (enregistré dans les données du programme) et accepte un &[T] ou &T autorise un tableau ou une variable allouée à la pile. Une allocation inutile est une perte de performance. Cela est généralement exposé immédiatement lorsque vous essayez d'appeler ces méthodes dans un test ou une méthode main:

    awesome_greeting(&String::from("Anna"));
    
    total_price(&vec![42, 13, 1337])
    
    is_even(&Box::new(42))
    
  3. Une autre considération de performance est que &String, &Vec et &Box introduire une couche d'indirection inutile car vous devez déréférencer le &String pour obtenir un String, puis effectuez un deuxième déréférencement pour finir à &str.

Au lieu de cela, vous devez accepter une tranche de chaîne (&str), une tranche (&[T]), ou simplement une référence (&T). UNE &String, &Vec<T> ou &Box<T> sera automatiquement contraint à un &str, &[T] ou &T, respectivement.

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
}

Vous pouvez maintenant appeler ces méthodes avec un ensemble de types plus large. Par exemple, awesome_greeting peut être appelé avec un littéral de chaîne ("Anna") ou un String alloué. total_price peut être appelé avec une référence à un tableau (&[1, 2, 3]) ou un Vec alloué.


Si vous souhaitez ajouter ou supprimer des éléments de String ou Vec<T>, vous pouvez prendre une référence mutable (&mut String ou &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);
}

Spécifiquement pour les tranches, vous pouvez également accepter un &mut [T] ou &mut str. Cela vous permet de muter une valeur spécifique à l'intérieur de la tranche, mais vous ne pouvez pas modifier le nombre d'éléments à l'intérieur de la tranche (ce qui signifie qu'il est très restreint pour les chaînes):

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();
    }
}
124
Shepmaster

En plus de réponse de Shepmaster , une autre raison d'accepter un &str (et de même &[T] etc) est dû à tous les autres types en plus String et &str qui satisfont également Deref<Target = str>. L'un des exemples les plus notables est Cow<str>, ce qui vous permet d'être très flexible quant à savoir si vous traitez des données détenues ou empruntées.

Si tu as:

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

Mais vous devez l'appeler avec un Cow<str>, vous devrez faire ceci:

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

Lorsque vous changez le type d'argument en &str, vous pouvez utiliser Cow de façon transparente, sans allocation inutile, tout comme avec 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);

Accepter &str rend l'appel de votre fonction plus uniforme et plus pratique, et la méthode la plus "simple" est désormais la plus efficace. Ces exemples fonctionneront également avec Cow<[T]> etc.

14
Peter Hall