À l'opposé du trait d'emprunt pour les types de copie?
J'ai vu le Borrow
trait utilisé pour définir des fonctions qui acceptent à la fois un type possédé ou une référence, par exemple T
ou &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 T
ou &T
et obtient T
) pour les Copy
types?
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
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 String
serait idiot ici, car il puts
n'est pas nécessaire de s'approprier les données, mais accepter &str
signifie 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 output
n'est pas nécessaire après son passage puts
, et l'appelant le sait, mais puts
nécessite une référence, il output
doit 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 puts
peut é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: Borrow
pour puts
donne à 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 &str
serait idiot, car il wrap
faudrait y faire appel to_owned()
. Si l'appelant a un String
qu'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 String
est 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 Borrow
trait «inverse » n'ajouterait aucune flexibilité qui arg: String
ne fournit pas déjà.
Mais String
n'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 wrap
un peu plus ergonomique en disant qu'il accepte tout ce qui peut être converti into
un 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 Copy
types?
Maintenant, vous avez posé des questions sur les Copy
types. 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 wrap
celle-là a besoin du tout A
, il est plus flexible de simplement accepter A
. Mais pour les Copy
types, 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
&u32
ne met pas en œuvreInto<u32>
, cette astuce particulière ne fonctionnerait pas ici de toute façon. - Étant donné que les
Copy
types 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
&A
en unA
quandA: Copy
est 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 foo
faut presque certainement accepter value: u32
et 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?
Avec la fonction que vous avez, vous ne pouvez utiliser u32
qu'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 T
car il &T
pourrait implémenter plusieurs Borrow
traits pour différents types. Vous devez spécifier explicitement celui que vous souhaitez utiliser comme sortie.
let output: usize = foo(&v);