À l'opposé du trait d'emprunt pour les types de copie?

Aug 18 2020

J'ai vu le Borrowtrait utilisé pour définir des fonctions qui acceptent à la fois un type possédé ou une référence, par exemple Tou &T. La borrow()méthode est alors appelée dans la fonction à obtenir &T.

Y a-t-il un trait qui permet le contraire (c'est-à-dire une fonction qui accepte Tou &Tet obtient T) pour les Copytypes?

Par exemple pour cet exemple:

use std::borrow::Borrow;

fn foo<T: Borrow<u32>>(value: T) -> u32 {
    *value.borrow()
}

fn main() {
    println!("{}", foo(&5));
    println!("{}", foo(5));
}

Cela appelle borrow()à obtenir une référence, qui est alors immédiatement déréférencée.

Existe-t-il une autre implémentation qui copie simplement la valeur si a Tété transmise et les déréférences si elles ont &Tété données? Ou est-ce que ce qui précède est la manière idiomatique d'écrire ce genre de chose?

Réponses

5 trentcl Aug 18 2020 at 14:09

Il n'y a pas vraiment de trait inverse pour Borrow, car ce n'est pas vraiment utile comme borne sur les fonctions de la même manière Borrow. La raison a à voir avec la propriété.

Pourquoi "inverse Borrow" est-il moins utile que Borrow?

Fonctions nécessitant des références

Considérez une fonction qui n'a besoin que de référencer son argument:

fn puts(arg: &str) {
    println!("{}", arg);
}

Accepter Stringserait idiot ici, car il putsn'est pas nécessaire de s'approprier les données, mais accepter &strsignifie que nous pouvons parfois forcer l'appelant à conserver les données plus longtemps que nécessaire:

{
    let output = create_some_string();
    output.push_str(some_other_string);
    puts(&output);
    // do some other stuff but never use `output` again
} // `output` isn't dropped until here

Le problème est que ce outputn'est pas nécessaire après son passage puts, et l'appelant le sait, mais putsnécessite une référence, il outputdoit donc rester en vie jusqu'à la fin du bloc. Évidemment, vous pouvez toujours résoudre ce problème dans l'appelant en ajoutant plus de blocs et parfois un let, mais putspeut également être rendu générique pour laisser l'appelant déléguer la responsabilité du nettoyage output:

fn puts<T: Borrow<str>>(arg: T) {
    println!("{}", arg.borrow());
}

Accepter T: Borrowpour putsdonne à l'appelant la flexibilité de décider de conserver l'argument ou de le déplacer dans la fonction.

Fonctions nécessitant des valeurs possédées

Considérons maintenant le cas d'une fonction qui doit réellement s'approprier:

struct Wrapper(String);
fn wrap(arg: String) -> Wrapper {
    Wrapper(arg)
}

Dans ce cas, accepter &strserait idiot, car il wrapfaudrait y faire appel to_owned(). Si l'appelant a un Stringqu'il n'utilise plus, cela copierait inutilement les données qui auraient pu juste être déplacées dans la fonction. Dans ce cas, accepter Stringest l'option la plus flexible, car elle permet à l'appelant de décider de faire un clonage ou de transmettre un existant String. Avoir un Borrowtrait «inverse » n'ajouterait aucune flexibilité qui arg: Stringne fournit pas déjà.

Mais Stringn'est pas toujours le plus ergonomique arguments, car il existe plusieurs types différents de chaîne: &str, Cow<str>, Box<str>... Nous pouvons faire wrapun peu plus ergonomique en disant qu'il accepte tout ce qui peut être converti intoun String.

fn wrap<T: Into<String>>(arg: T) -> Wrapper {
    Wrapper(arg.into())
}

Cela signifie que vous pouvez l'appeler comme wrap("hello, world")sans avoir à appeler .to_owned()le littéral. Ce qui n'est pas vraiment une victoire de flexibilité - l'appelant peut toujours appeler à la .into()place sans perte de généralité - mais c'est une victoire ergonomique .

Et les Copytypes?

Maintenant, vous avez posé des questions sur les Copytypes. Pour la plupart, les arguments ci-dessus s'appliquent toujours. Si vous écrivez une fonction qui, comme puts, n'a besoin que d'un &A, utiliser T: Borrow<A>peut être plus flexible pour l'appelant; pour une fonction comme wrapcelle-là a besoin du tout A, il est plus flexible de simplement accepter A. Mais pour les Copytypes, l' avantage ergonomique d'accepter T: Into<A>est beaucoup moins évident.

  • Pour les types entiers, parce que les génériques jouent avec l'inférence de type, leur utilisation rend généralement moins ergonomique l'utilisation de littéraux; vous devrez peut-être annoter explicitement les types.
  • Puisque &u32ne met pas en œuvre Into<u32>, cette astuce particulière ne fonctionnerait pas ici de toute façon.
  • Étant donné que les Copytypes sont facilement disponibles en tant que valeurs possédées, il est moins courant de les utiliser par référence en premier lieu.
  • Enfin, transformer un &Aen un Aquand A: Copyest aussi simple que d'ajouter *; pouvoir sauter cette étape n'est probablement pas une victoire suffisamment convaincante pour contrebalancer la complexité supplémentaire de l'utilisation de génériques dans la plupart des cas.

En conclusion, il foofaut presque certainement accepter value: u32et laisser l'appelant décider comment obtenir cette valeur.

Voir également

  • Est-il plus conventionnel de passer par valeur ou de passer par référence lorsque la méthode doit s'approprier la valeur?
2 Sunreef Aug 18 2020 at 11:00

Avec la fonction que vous avez, vous ne pouvez utiliser u32qu'un type ou un type qui peut être emprunté en tant que u32.

Vous pouvez rendre votre fonction plus générique en utilisant un deuxième argument de modèle.

fn foo<T: Copy, N: Borrow<T>>(value: N) -> T {
    *value.borrow()
}

Ce n'est cependant qu'une solution partielle car elle nécessitera des annotations de type dans certains cas pour fonctionner correctement.

Par exemple, cela fonctionne hors de la boîte avec usize:

let v = 0usize;
println!("{}", foo(v));

Il n'y a aucun problème ici pour que le compilateur devine qu'il foo(v)s'agit d'un fichier usize.

Cependant, si vous essayez foo(&v), le compilateur se plaindra qu'il ne peut pas trouver le bon type de sortie Tcar il &Tpourrait implémenter plusieurs Borrowtraits pour différents types. Vous devez spécifier explicitement celui que vous souhaitez utiliser comme sortie.

let output: usize = foo(&v);