Opposto del tratto in prestito per i tipi di copia?

Aug 18 2020

Ho visto il Borrowtratto utilizzato per definire le funzioni che accettano sia un tipo di proprietà che un riferimento, ad esempio To &T. Il borrow()metodo viene quindi chiamato nella funzione da ottenere &T.

C'è qualche tratto che consente l'opposto (cioè una funzione che accetta To &Te ottiene T) per i Copytipi?

Ad esempio, per questo esempio:

use std::borrow::Borrow;

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

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

Ciò richiede borrow()di ottenere un riferimento, che viene quindi immediatamente dereferenziato.

C'è un'altra implementazione che copia semplicemente il valore se è Tstato passato e dereferenzia se è &Tstato fornito? O quanto sopra è il modo idiomatico di scrivere questo genere di cose?

Risposte

5 trentcl Aug 18 2020 at 14:09

Non c'è davvero un tratto inverso per Borrow, perché non è veramente utile come limite alle funzioni allo stesso modo Borrow. Il motivo ha a che fare con la proprietà.

Perché "inverso Borrow" è meno utile di Borrow?

Funzioni che richiedono riferimenti

Considera una funzione che deve solo fare riferimento al suo argomento:

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

Accettare Stringsarebbe sciocco qui, perché putsnon è necessario assumere la proprietà dei dati, ma accettare &strsignifica che a volte potremmo costringere il chiamante a conservare i dati più a lungo del necessario:

{
    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

Il problema è che outputnon è necessario dopo che è passato a puts, e il chiamante lo sa, ma putsrichiede un riferimento, quindi outputdeve rimanere in vita fino alla fine del blocco. Ovviamente puoi sempre risolvere questo problema nel chiamante aggiungendo più blocchi e talvolta un let, ma putspuò anche essere reso generico per lasciare che il chiamante deleghi la responsabilità della pulizia output:

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

Accettare T: Borrowper putsdà al chiamante la flessibilità di decidere se mantenere l'argomento in giro o spostarlo nella funzione.

Funzioni che richiedono valori di proprietà

Consideriamo ora il caso di una funzione che deve effettivamente assumere la proprietà:

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

In questo caso accettare &strsarebbe una sciocchezza, perché wrapbisognerebbe invocarlo to_owned(). Se il chiamante ha un Stringche non sta più usando, ciò copierebbe inutilmente i dati che avrebbero potuto essere appena spostati nella funzione. In questo caso, accettare Stringè l'opzione più flessibile, perché consente al chiamante di decidere se fare un clone o passare un esistente String. Avere un Borrowtratto "inverso " non aggiungerebbe alcuna flessibilità che arg: Stringgià non fornisce.

Ma Stringnon è sempre l'argomento più ergonomico, perché ci sono diversi tipi di stringhe: &str, Cow<str>, Box<str>... Possiamo fare wrapun po 'più ergonomica dicendo accetta tutto ciò che può essere convertito intouna String.

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

Ciò significa che puoi chiamarlo come wrap("hello, world")senza dover ricorrere .to_owned()al letterale. Che non è davvero una vittoria in termini di flessibilità - il chiamante può sempre chiamare .into()invece senza perdere la generalità - ma è una vittoria ergonomica .

E i Copytipi?

Ora, hai chiesto dei Copytipi. Per la maggior parte si applicano ancora gli argomenti di cui sopra. Se stai scrivendo una funzione che, come puts, necessita solo di un &A, l'utilizzo T: Borrow<A>potrebbe essere più flessibile per il chiamante; poiché una funzione del genere ha wrapbisogno del tutto A, è più flessibile semplicemente accettare A. Ma per i Copytipi il vantaggio ergonomico dell'accettazione T: Into<A>è molto meno evidente.

  • Per i tipi interi, poiché i generici pasticciano con l'inferenza del tipo, usarli di solito rende meno ergonomico l'uso dei letterali; potresti finire per dover annotare esplicitamente i tipi.
  • Dal momento &u32che non implementa Into<u32>, quel particolare trucco non funzionerebbe comunque qui.
  • Poiché i Copytipi sono prontamente disponibili come valori di proprietà, è meno comune utilizzarli per riferimento in primo luogo.
  • Infine, trasformare un &Ain un Aquando A: Copyè semplice come aggiungere *; essere in grado di saltare questo passaggio probabilmente non è una vittoria abbastanza convincente da controbilanciare la complessità aggiuntiva dell'uso di farmaci generici nella maggior parte dei casi.

In conclusione, fooquasi certamente dovrebbe solo accettare value: u32e lasciare che il chiamante decida come ottenere quel valore.

Guarda anche

  • È più convenzionale passare per valore o passaggio per riferimento quando il metodo richiede la proprietà del valore?
2 Sunreef Aug 18 2020 at 11:00

Con la funzione che hai puoi usare solo a u32o un tipo che può essere preso in prestito come u32.

È possibile rendere la funzione più generica utilizzando un secondo argomento del modello.

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

Questa è tuttavia solo una soluzione parziale in quanto richiederà annotazioni di tipo in alcuni casi per funzionare correttamente.

Ad esempio, funziona fuori dagli schemi con usize:

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

Non c'è nessun problema qui per il compilatore di indovinare che foo(v)è un file usize.

Tuttavia, se ci provi foo(&v), il compilatore si lamenterà di non riuscire a trovare il tipo di output corretto Tperché &Tpotrebbe implementare diversi Borrowtratti per tipi diversi. È necessario specificare esplicitamente quale si desidera utilizzare come output.

let output: usize = foo(&v);