Conosco relativamente bene Go, dopo aver scritto un numero di piccoli programmi. Ruggine, ovviamente, mi è meno familiare ma mi tengo d'occhio.
Avendo letto di recente http://yager.io/programming/go.html , ho pensato di esaminare personalmente i due modi in cui i generici sono gestiti perché l'articolo sembrava criticare ingiustamente Go quando, in pratica, non c'erano molte interfacce non poteva realizzare elegantemente. Continuavo a sentire l'hype su quanto potenti fossero i tratti di Rust e nient'altro che le critiche della gente su Go. Avendo un po 'di esperienza in Go, mi chiedevo quanto fosse vero e quali fossero le differenze alla fine. Quello che ho scoperto è che i tratti e le interfacce sono abbastanza simili! Alla fine, non sono sicuro che mi manchi qualcosa, quindi ecco un rapido riassunto educativo delle loro somiglianze, così puoi dirmi cosa mi sono perso!
Ora diamo un'occhiata a Go Interfaces dalla loro documentazione :
Le interfacce in Go forniscono un modo per specificare il comportamento di un oggetto: se qualcosa può farlo, allora può essere usato qui.
Di gran lunga l'interfaccia più comune è quella Stringer
che restituisce una stringa che rappresenta l'oggetto.
type Stringer interface {
String() string
}
Quindi, qualsiasi oggetto che ha String()
definito su di esso è un Stringer
oggetto. Questo può essere usato nelle firme dei tipi in modo tale da func (s Stringer) print()
prendere quasi tutti gli oggetti e stamparli.
Abbiamo anche interface{}
che prende qualsiasi oggetto. Dobbiamo quindi determinare il tipo in fase di esecuzione attraverso la riflessione.
Ora diamo un'occhiata a Rust Traits dalla loro documentazione :
Nella sua forma più semplice, un tratto è un insieme di zero o più firme di metodi. Ad esempio, potremmo dichiarare il tratto stampabile per cose che possono essere stampate sulla console, con una firma con un solo metodo:
trait Printable {
fn print(&self);
}
Questo sembra immediatamente abbastanza simile alle nostre interfacce Go. L'unica differenza che vedo è che definiamo 'Implementations' of Traits piuttosto che semplicemente definire i metodi. Quindi lo facciamo
impl Printable for int {
fn print(&self) { println!("{}", *self) }
}
invece di
fn print(a: int) { ... }
Domanda bonus: cosa succede in Rust se si definisce una funzione che implementa un tratto ma non si utilizza impl
? Semplicemente non funziona?
A differenza di Go's Interfaces, il sistema di tipi di Rust ha parametri di tipo che ti permettono di fare generici e cose come interface{}
mentre il compilatore e il runtime conoscono effettivamente il tipo. Per esempio,
trait Seq<T> {
fn length(&self) -> uint;
}
funziona su qualsiasi tipo e il compilatore sa che il tipo degli elementi Sequence al momento della compilazione piuttosto che usare la riflessione.
Ora, la vera domanda: mi sto perdendo qualche differenza qui? Sono davvero così simili? Non c'è qualche differenza fondamentale che mi manca qui? (In uso. I dettagli di implementazione sono interessanti, ma alla fine non importanti se funzionano allo stesso modo.)
Oltre alle differenze sintattiche, le differenze effettive che vedo sono:
- Go ha un metodo di invio automatico rispetto a Rust che richiede (?)
impl
S per implementare un tratto- Elegante contro esplicito
- La ruggine ha parametri di tipo che consentono generici adeguati senza riflessione.
- Go non ha davvero alcuna risposta qui. Questa è l'unica cosa che è significativamente più potente ed è in definitiva solo una sostituzione per i metodi di copia e incolla con firme di tipo diverso.
Sono queste le uniche differenze non banali? In tal caso, sembrerebbe che il sistema di interfaccia / tipo di Go non sia, in pratica, debole come percepito.
AnyMap
è una buona dimostrazione dei punti di forza di Rust, che combina oggetti tratti con generici per fornire un'astrazione sicura ed espressiva della cosa fragile che in Go verrebbe necessariamente scrittamap[string]interface{}
.