Opposto del tratto in prestito per i tipi di copia?
Ho visto il Borrow
tratto utilizzato per definire le funzioni che accettano sia un tipo di proprietà che un riferimento, ad esempio T
o &T
. Il borrow()
metodo viene quindi chiamato nella funzione da ottenere &T
.
C'è qualche tratto che consente l'opposto (cioè una funzione che accetta T
o &T
e ottiene T
) per i Copy
tipi?
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 è T
stato passato e dereferenzia se è &T
stato fornito? O quanto sopra è il modo idiomatico di scrivere questo genere di cose?
Risposte
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 String
sarebbe sciocco qui, perché puts
non è necessario assumere la proprietà dei dati, ma accettare &str
significa 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 output
non è necessario dopo che è passato a puts
, e il chiamante lo sa, ma puts
richiede un riferimento, quindi output
deve rimanere in vita fino alla fine del blocco. Ovviamente puoi sempre risolvere questo problema nel chiamante aggiungendo più blocchi e talvolta un let
, ma puts
può 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: Borrow
per puts
dà 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 &str
sarebbe una sciocchezza, perché wrap
bisognerebbe invocarlo to_owned()
. Se il chiamante ha un String
che 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 Borrow
tratto "inverso " non aggiungerebbe alcuna flessibilità che arg: String
già non fornisce.
Ma String
non è sempre l'argomento più ergonomico, perché ci sono diversi tipi di stringhe: &str
, Cow<str>
, Box<str>
... Possiamo fare wrap
un po 'più ergonomica dicendo accetta tutto ciò che può essere convertito into
una 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 Copy
tipi?
Ora, hai chiesto dei Copy
tipi. 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 wrap
bisogno del tutto A
, è più flessibile semplicemente accettare A
. Ma per i Copy
tipi 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
&u32
che non implementaInto<u32>
, quel particolare trucco non funzionerebbe comunque qui. - Poiché i
Copy
tipi sono prontamente disponibili come valori di proprietà, è meno comune utilizzarli per riferimento in primo luogo. - Infine, trasformare un
&A
in unA
quandoA: 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, foo
quasi certamente dovrebbe solo accettare value: u32
e 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?
Con la funzione che hai puoi usare solo a u32
o 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 T
perché &T
potrebbe implementare diversi Borrow
tratti per tipi diversi. È necessario specificare esplicitamente quale si desidera utilizzare come output.
let output: usize = foo(&v);