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

127

J'ai écrit du code Rust qui prend &Stringcomme 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 Vecou 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?

Shepmaster
la source

Réponses:

162

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


  1. L'une des principales raisons d'utiliser a Stringou a Vecest 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 le Vecfichier ou String.

  2. L'acceptation d'un &String, &Vecou nécessite&Box également que l'argument soit alloué sur le tas avant de pouvoir appeler la fonction. L'acceptation de a &strautorise un littéral de chaîne (enregistré dans les données du programme) et l'acceptation de &[T]ou &Tautorise un tableau ou une variable alloué à la pile. Une allocation inutile est une perte de performance. Ceci est généralement exposé immédiatement lorsque vous essayez d'appeler ces méthodes dans un test ou une mainméthode:

    awesome_greeting(&String::from("Anna"));
    total_price(&vec![42, 13, 1337])
    is_even(&Box::new(42))
  3. Une autre considération pour les performances est que &String, &Vecet &Boxintroduisez une couche d'indirection inutile car vous devez déréférencer le &Stringpour obtenir un String, puis effectuer une deuxième déréférence pour aboutir à &str.

Au lieu de cela, vous devriez accepter une chaîne slice ( &str), une slice ( &[T]) ou simplement une référence ( &T). A &String, &Vec<T>ou &Box<T>sera automatiquement forcé à a &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 désormais appeler ces méthodes avec un ensemble plus large de types. Par exemple, awesome_greetingpeut être appelé avec une chaîne littérale ( "Anna") ou allouée String. total_pricepeut être appelé avec une référence à un tableau ( &[1, 2, 3]) ou un fichier alloué Vec.


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

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'elle est très limitée 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();
    }
}
Shepmaster
la source
5
Que diriez-vous d'un tl; dr au début? Cette réponse est déjà un peu longue. Quelque chose comme " &strest plus général (comme dans: impose moins de restrictions) sans capacités réduites"? Aussi: le point 3 n'est souvent pas si important que je pense. Habituellement, Vecs et Strings vivront sur la pile et souvent même quelque part près du cadre de pile actuel. La pile est généralement chaude et le déréférencement sera servi à partir d'un cache CPU.
Lukas Kalbertodt
3
@Shepmaster: En ce qui concerne le coût d'allocation, il peut être intéressant de mentionner le problème particulier des sous-chaînes / tranches quand on parle d'allocation obligatoire. total_price(&prices[0..4])ne nécessite pas d'allouer un nouveau vecteur pour la tranche.
Matthieu M.
4
C'est une excellente réponse. Je ne fais que commencer à Rust et je me demandais quand je devrais utiliser a &stret pourquoi (venant de Python, donc je ne traite généralement pas explicitement des types). Tout cela a été parfaitement
réglé
2
Super conseils sur les paramètres. Juste besoin d'un doute: "Accepter une & String, & Vec ou & Box nécessite également une allocation avant de pouvoir appeler la méthode." ... Pourquoi? Pourriez-vous indiquer la partie de la documentation où je peux lire ceci en détail? (Je suis un débutant). En outre, pouvons-nous avoir des conseils similaires sur les types de retour?
Nawaz
2
Il me manque des informations sur les raisons pour lesquelles une allocation supplémentaire est nécessaire. La chaîne est stockée sur le tas, lors de l'acceptation de & String comme argument, pourquoi Rust ne passe-t-il pas simplement un pointeur stocké sur la pile qui pointe vers l'espace du tas, je ne comprends pas pourquoi le passage d'un & String nécessiterait une allocation supplémentaire, en passant une chaîne slice devrait également exiger l'envoi d'un pointeur stocké sur la pile qui pointe vers l'espace du tas?
cjohansson
22

En plus de la réponse de Shepmaster , une autre raison d'accepter un &str(et de même , &[T]etc.) est à cause de tous les autres types d' ailleurs String et &strqui satisfont aussi Deref<Target = str>. L'un des exemples les plus notables estCow<str> 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 le faire:

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 modifiez le type d'argument en &str, vous pouvez l'utiliser de Cowmanière 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 &strrend l'appel de votre fonction plus uniforme et plus pratique, et le moyen le plus "simple" est désormais aussi le plus efficace. Ces exemples fonctionneront également avec Cow<[T]>etc.

Peter Hall
la source