¿Lo contrario del rasgo de préstamo para los tipos de copia?

Aug 18 2020

He visto el Borrowrasgo utilizado para definir funciones que aceptan tanto un tipo propio como una referencia, por ejemplo, To &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 Tu &Tobtiene T) para Copytipos?

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 Tse pasó y elimina las referencias si &Tse proporcionó? ¿O es lo anterior la forma idiomática de escribir este tipo de cosas?

Respuestas

5 trentcl Aug 18 2020 at 14:09

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 Stringsería una tontería aquí, porque putsno necesita tomar posesión de los datos, pero aceptar &strsignifica 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 outputno es necesario después de que se pasa a puts, y la persona que llama lo sabe, pero putsrequiere una referencia, por lo que outputdebe 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 putstambié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: Borrowpara putsle 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 &strsería una tontería, porque wraptendría que recurrir to_owned()a él. Si la persona Stringque 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 Stringes 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 Borrowrasgo "inverso " no agregaría ninguna flexibilidad que arg: Stringno brinda.

Pero Stringno siempre es el argumento más ergonómico, porque hay varios tipos diferentes de cadena: &str, Cow<str>, Box<str>... Podemos hacer wrapun poco más ergonómica diciendo que acepta cualquier cosa que se puede convertir intouna 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 Copytipos?

Ahora, preguntaste sobre Copytipos. 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 wrapesa que necesita el todo A, es más flexible simplemente aceptar A. Pero para los Copytipos, 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 &u32no se implementa Into<u32>, ese truco en particular no funcionaría aquí de todos modos.
  • Dado que los Copytipos están disponibles como valores propios, es menos común usarlos como referencia en primer lugar.
  • Finalmente, convertir a &Aen un Awhen A: Copyes 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 foocasi seguro que simplemente acepte value: u32y 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?
2 Sunreef Aug 18 2020 at 11:00

Con la función que tiene, solo puede usar u32un 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 Tporque &Tpodría implementar varios Borrowrasgos para diferentes tipos. Debe especificar explícitamente cuál desea usar como salida.

let output: usize = foo(&v);