¿Lo contrario del rasgo de préstamo para los tipos de copia?
He visto el Borrow
rasgo utilizado para definir funciones que aceptan tanto un tipo propio como una referencia, por ejemplo, T
o &T
. El borrow()
método se llama a continuación, en la función de obtener &T
.
¿Existe algún rasgo que permita lo contrario (es decir, una función que acepta T
u &T
obtiene T
) para Copy
tipos?
Por ejemplo, para este ejemplo:
use std::borrow::Borrow;
fn foo<T: Borrow<u32>>(value: T) -> u32 {
*value.borrow()
}
fn main() {
println!("{}", foo(&5));
println!("{}", foo(5));
}
Esto llama borrow()
a obtener una referencia, que luego se desreferencia inmediatamente.
¿Existe otra implementación que simplemente copia el valor si T
se pasó y elimina las referencias si &T
se proporcionó? ¿O es lo anterior la forma idiomática de escribir este tipo de cosas?
Respuestas
Realmente no hay un rasgo inverso para Borrow
, porque no es realmente útil como un límite en funciones de la misma manera Borrow
. La razón tiene que ver con la propiedad.
¿Por qué "inverso Borrow
" es menos útil que Borrow
?
Funciones que necesitan referencias
Considere una función que solo necesita hacer referencia a su argumento:
fn puts(arg: &str) {
println!("{}", arg);
}
Aceptar String
sería una tontería aquí, porque puts
no necesita tomar posesión de los datos, pero aceptar &str
significa que a veces podemos obligar a la persona que llama a mantener los datos más tiempo del necesario:
{
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
El problema es que output
no es necesario después de que se pasa a puts
, y la persona que llama lo sabe, pero puts
requiere una referencia, por lo que output
debe permanecer vivo hasta el final del bloque. Obviamente, siempre puede solucionar esto en la persona que llama agregando más bloques y, a veces let
, un , pero puts
también puede hacerse genérico para permitir que la persona que llama delegue la responsabilidad de limpiar output
:
fn puts<T: Borrow<str>>(arg: T) {
println!("{}", arg.borrow());
}
Aceptar T: Borrow
para puts
le da a la persona que llama la flexibilidad de decidir si mantener el argumento o moverlo a la función.
Funciones que necesitan valores propios
Ahora considere el caso de una función que realmente necesita tomar posesión:
struct Wrapper(String);
fn wrap(arg: String) -> Wrapper {
Wrapper(arg)
}
En este caso aceptar &str
sería una tontería, porque wrap
tendría que recurrir to_owned()
a él. Si la persona String
que llama tiene un que ya no está usando, eso copiaría innecesariamente los datos que podrían haberse movido a la función. En este caso, aceptar String
es la opción más flexible, ya que permite a la persona que llama decidir si hacer un clon o pasar uno existente String
. Tener un Borrow
rasgo "inverso " no agregaría ninguna flexibilidad que arg: String
no brinda.
Pero String
no siempre es el argumento más ergonómico, porque hay varios tipos diferentes de cadena: &str
, Cow<str>
, Box<str>
... Podemos hacer wrap
un poco más ergonómica diciendo que acepta cualquier cosa que se puede convertir into
una String
.
fn wrap<T: Into<String>>(arg: T) -> Wrapper {
Wrapper(arg.into())
}
Esto significa que puede llamarlo como wrap("hello, world")
sin tener que llamar .to_owned()
al literal. Lo cual no es realmente una ventaja de flexibilidad , la persona que llama siempre puede llamar .into()
sin perder la generalidad, pero es una ventaja ergonómica .
¿Qué pasa con los Copy
tipos?
Ahora, preguntaste sobre Copy
tipos. En su mayor parte, los argumentos anteriores todavía se aplican. Si está escribiendo una función que, como puts
, solo necesita un &A
, el uso T: Borrow<A>
podría ser más flexible para la persona que llama; para una función como wrap
esa que necesita el todo A
, es más flexible simplemente aceptar A
. Pero para los Copy
tipos, la ventaja ergonómica de aceptar T: Into<A>
es mucho menos clara.
- Para los tipos enteros, debido a que los genéricos interfieren con la inferencia de tipos, usarlos generalmente hace que sea menos ergonómico usar literales; puede terminar teniendo que anotar explícitamente los tipos.
- Como
&u32
no se implementaInto<u32>
, ese truco en particular no funcionaría aquí de todos modos. - Dado que los
Copy
tipos están disponibles como valores propios, es menos común usarlos como referencia en primer lugar. - Finalmente, convertir a
&A
en unA
whenA: Copy
es tan simple como sumar*
; ser capaz de omitir ese paso probablemente no sea una victoria lo suficientemente convincente como para contrarrestar la complejidad adicional del uso de genéricos en la mayoría de los casos.
En conclusión, es foo
casi seguro que simplemente acepte value: u32
y deje que la persona que llama decida cómo obtener ese valor.
Ver también
- ¿Es más convencional pasar por valor o pasar por referencia cuando el método necesita la propiedad del valor?
Con la función que tiene, solo puede usar u32
un tipo o un tipo que se puede tomar prestado como u32
.
Puede hacer que su función sea más genérica utilizando un segundo argumento de plantilla.
fn foo<T: Copy, N: Borrow<T>>(value: N) -> T {
*value.borrow()
}
Sin embargo, esta es solo una solución parcial, ya que requerirá anotaciones de tipo en algunos casos para funcionar correctamente.
Por ejemplo, funciona de inmediato con usize
:
let v = 0usize;
println!("{}", foo(v));
Aquí no hay problema para que el compilador adivine que foo(v)
es un usize
.
Sin embargo, si lo intenta foo(&v)
, el compilador se quejará de que no puede encontrar el tipo de salida correcto T
porque &T
podría implementar varios Borrow
rasgos para diferentes tipos. Debe especificar explícitamente cuál desea usar como salida.
let output: usize = foo(&v);