Perché scegliere Struct Over Class?

Giocando con Swift, proveniente da uno sfondo Java, perché dovresti scegliere un Struct invece di una Classe? Sembra che siano la stessa cosa, con una Struct che offre meno funzionalità. Perché sceglierlo allora?

Secondo il popolare programma di programmazione orientata al protocollo di conversazione WWDC 2015 di Swift ( video , trascrizione ), Swift offre una serie di funzionalità che rendono le strutture migliori delle classi in molte circostanze.

Le strutture sono preferibili se sono relativamente piccole e copiabili perché copiare è molto più sicuro che avere più riferimenti alla stessa istanza che accade con le classi. Questo è particolarmente importante quando si passa da una variabile a molte classi e / o in un ambiente multithread. Se puoi sempre inviare una copia della tua variabile ad altri luoghi, non devi mai preoccuparti di quell’altro posto che cambia il valore della tua variabile sotto di te.

Con Structs, c’è molto meno bisogno di preoccuparsi di perdite di memoria o di più thread racing per accedere / modificare una singola istanza di una variabile. (Per chi ha una mentalità più tecnica, l’eccezione è quando si cattura una struttura all’interno di una chiusura perché in realtà sta acquisendo un riferimento all’istanza a meno che non lo si contrassegni esplicitamente per essere copiato).

Le classi possono anche diventare gonfie perché una class può ereditare solo da una singola superclass. Questo ci incoraggia a creare enormi superclassi che comprendono molte abilità diverse che sono solo vagamente correlate. L’utilizzo di protocolli, in particolare con estensioni di protocollo in cui è ansible fornire implementazioni ai protocolli, consente di eliminare la necessità per le classi di conseguire questo tipo di comportamento.

Il discorso espone questi scenari in cui le classi sono preferite:

  • Copiare o confrontare le istanze non ha senso (ad esempio, Finestra)
  • La durata dell’istanza è legata a effetti esterni (ad esempio, TemporaryFile)
  • Le istanze sono solo “sink” – conduttori di sola scrittura allo stato esterno (egCGContext)

Implica che le strutture dovrebbero essere l’impostazione predefinita e le classi dovrebbero essere un fallback.

D’altra parte, la documentazione di Swift Programming Language è in qualche modo contraddittoria:

Le istanze di struttura vengono sempre passate per valore e le istanze di class vengono sempre passate per riferimento. Ciò significa che sono adatti a diversi tipi di compiti. Considerando i costrutti e le funzionalità dei dati necessari per un progetto, decidere se ciascun costrutto di dati deve essere definito come una class o come una struttura.

Come linea guida generale, prendere in considerazione la creazione di una struttura quando si applicano una o più di queste condizioni:

  • Lo scopo principale della struttura è incapsulare alcuni valori di dati relativamente semplici.
  • È ragionevole aspettarsi che i valori incapsulati vengano copiati anziché riferiti quando si assegna o si passa un’istanza di tale struttura.
  • Tutte le proprietà memorizzate dalla struttura sono esse stesse dei tipi di valore, che dovrebbero anche essere copiati anziché riferiti.
  • La struttura non ha bisogno di ereditare proprietà o comportamento da un altro tipo esistente.

Esempi di buoni candidati per le strutture includono:

  • La dimensione di una forma geometrica, forse incapsulando una proprietà width e una proprietà height, entrambe di tipo Double.
  • Un modo per riferirsi a intervalli all’interno di una serie, forse incapsulare una proprietà di avvio e una proprietà di lunghezza, entrambi di tipo Int.
  • Un punto in un sistema di coordinate 3D, forse incapsulando le proprietà x, yez, ciascuna di tipo Double.

In tutti gli altri casi, definire una class e creare istanze di tale class da gestire e passare per riferimento. In pratica, ciò significa che la maggior parte dei costrutti di dati personalizzati dovrebbero essere classi, non strutture.

Qui si afferma che dovremmo utilizzare di default le classi e utilizzare le strutture solo in circostanze specifiche. In definitiva, è necessario comprendere l’implicazione del mondo reale dei tipi di valore rispetto ai tipi di riferimento e quindi è ansible prendere una decisione informata su quando utilizzare le strutture o le classi. Inoltre, tieni presente che questi concetti sono in continua evoluzione e la documentazione di Swift Programming Language è stata scritta prima che fosse data la presentazione della Programm Oriented Programming.

Poiché le istanze struct sono allocate nello stack e le istanze di class sono allocate nell’heap, le struct possono essere drasticamente più veloci.

Tuttavia, dovresti sempre misurarlo tu stesso e decidere in base al tuo caso d’uso unico.

Considera il seguente esempio, che dimostra 2 strategie di wrapping del tipo di dati Int utilizzando struct e class . Sto usando 10 valori ripetuti per riflettere meglio il mondo reale, dove hai più campi.

 class Int10Class { let value1, value2, value3, value4, value5, value6, value7, value8, value9, value10: Int init(_ val: Int) { self.value1 = val self.value2 = val self.value3 = val self.value4 = val self.value5 = val self.value6 = val self.value7 = val self.value8 = val self.value9 = val self.value10 = val } } struct Int10Struct { let value1, value2, value3, value4, value5, value6, value7, value8, value9, value10: Int init(_ val: Int) { self.value1 = val self.value2 = val self.value3 = val self.value4 = val self.value5 = val self.value6 = val self.value7 = val self.value8 = val self.value9 = val self.value10 = val } } func + (x: Int10Class, y: Int10Class) -> Int10Class { return IntClass(x.value + y.value) } func + (x: Int10Struct, y: Int10Struct) -> Int10Struct { return IntStruct(x.value + y.value) } 

Le prestazioni sono misurate usando

 // Measure Int10Class measure("class (10 fields)") { var x = Int10Class(0) for _ in 1...10000000 { x = x + Int10Class(1) } } // Measure Int10Struct measure("struct (10 fields)") { var y = Int10Struct(0) for _ in 1...10000000 { y = y + Int10Struct(1) } } func measure(name: String, @noescape block: () -> ()) { let t0 = CACurrentMediaTime() block() let dt = CACurrentMediaTime() - t0 print("\(name) -> \(dt)") } 

Il codice può essere trovato su https://github.com/knguyen2708/StructVsClassPerformance

AGGIORNAMENTO (27 marzo 2018) :

A partire da Swift 4.0, Xcode 9.2, in esecuzione versione di rilascio su iPhone 6S, iOS 11.2.6, l’impostazione di Swift Compiler è -O -whole-module-optimization :

  • class versione di class impiegato 2,06 secondi
  • struct versione di struct impiegava 4.17e-08 secondi (50.000.000 di volte più veloce)

(Non ho più mediamente più esecuzioni, poiché le varianze sono molto piccole, meno del 5%)

Nota : la differenza è molto meno drammatica senza l’ottimizzazione dell’intero modulo. Sarei felice se qualcuno potesse indicare cosa effettivamente fa la bandiera.


AGGIORNAMENTO (7 maggio 2016) :

A partire da Swift 2.2.1, Xcode 7.3, con versione di rilascio su iPhone 6S, iOS 9.3.1, con una media di 5 esecuzioni, l’impostazione di Swift Compiler è -O -whole-module-optimization :

  • class versione di class preso 2.159942142s
  • struct versione di struct preso 5.83E-08s (37.000.000 volte più veloce)

Nota : come qualcuno ha detto che negli scenari del mondo reale, ci sarà probabilmente più di 1 campo in una struttura, ho aggiunto test per structs / classs con 10 campi anziché 1. Sorprendentemente, i risultati non variano molto.


RISULTATI ORIGINALI (1 ° giugno 2014):

(Eseguito su struct / class con 1 campo, non 10)

A partire da Swift 1.2, Xcode 6.3.2, la versione di Release in esecuzione su iPhone 5S, iOS 8.3, ha una media di 5 esecuzioni

  • class versione di class preso 9.788332333s
  • struct versione della struct ha richiesto 0,010532942 (900 volte più veloce)

VECCHI RISULTATI (dal tempo sconosciuto)

(Eseguito su struct / class con 1 campo, non 10)

Con la versione build sul mio MacBook Pro:

  • La versione della class ha richiesto 1.10082 di secondo
  • La versione della struct impiegato 0.02324 sec (50 volte più veloce)

Somiglianze tra strutture e classi.

Ho creato Gist per questo con semplici esempi. https://github.com/objc-swift/swift-classs-vs-structures

E differenze

1. Ereditarietà.

le strutture non possono ereditare in modo rapido. Se vuoi

 class Vehicle{ } class Car : Vehicle{ } 

Vai per una lezione.

2. Passa

Le strutture Swift passano per valore e le istanze di class passano per riferimento.

Differenze contestuali

Struct costante e variabili

Esempio (utilizzato a WWDC 2014)

 struct Point{ var x = 0.0; var y = 0.0; } 

Definisce una struttura chiamata Punto.

 var point = Point(x:0.0,y:2.0) 

Ora se provo a cambiare la x. È un’espressione valida.

 point.x = 5 

Ma se definissi un punto come costante.

 let point = Point(x:0.0,y:2.0) point.x = 5 //This will give compile time error. 

In questo caso l’intero punto è costante immutabile.

Se ho usato un punto di class invece questa è un’espressione valida. Perché in una costante immutabile di class è il riferimento alla class stessa non alle sue variabili di istanza (A meno che quelle variabili siano definite come costanti)

Ecco alcuni altri motivi da considerare:

  1. le strutture ottengono un inizializzatore automatico che non è necessario mantenere nel codice.

     struct MorphProperty { var type : MorphPropertyValueType var key : String var value : AnyObject enum MorphPropertyValueType { case String, Int, Double } } var m = MorphProperty(type: .Int, key: "what", value: "blah") 

Per ottenere questo in una class, dovresti aggiungere l’inizializzatore e mantenere l’intializer …

  1. I tipi di raccolta di base come Array sono le strutture. Più li usi nel tuo codice, più ti abituerai a passare per valore anziché come riferimento. Per esempio:

     func removeLast(var array:[String]) { array.removeLast() println(array) // [one, two] } var someArray = ["one", "two", "three"] removeLast(someArray) println(someArray) // [one, two, three] 
  2. Apparentemente immutabilità vs. mutabilità è un argomento enorme, ma molte persone intelligenti pensano che l’immutabilità – in questo caso le strutture – sia preferibile. Mutevole vs oggetti immutabili

Alcuni vantaggi:

  • automaticamente thread-safe per non essere condivisibile
  • usa meno memoria a causa di no isa e refcount (e in effetti è lo stack allocato in genere)
  • i metodi vengono sempre inviati staticamente, quindi possono essere sottolineati (anche se @final può farlo per le classi)
  • più facile da ragionare (non c’è bisogno di “copiare in modo difensivo” come è tipico con NSArray, NSString, ecc …) per lo stesso motivo della sicurezza del thread

Supponendo che sappiamo che Struct è un tipo di valore e Class è un tipo di riferimento .

Se non sai che tipo di valore e tipo di riferimento vedi, qual è la differenza tra il passaggio per riferimento e il passaggio per valore?

Basato sul post di mikeash :

… Vediamo prima alcuni esempi estremi e ovvi. I numeri interi sono ovviamente riproducibili. Dovrebbero essere tipi di valore. Le prese di rete non possono essere copiate sensibilmente. Dovrebbero essere tipi di riferimento. I punti, come in x, y pair, sono copiabili. Dovrebbero essere tipi di valore. Un controller che rappresenta un disco non può essere copiato in modo sensibile. Questo dovrebbe essere un tipo di riferimento.

Alcuni tipi possono essere copiati, ma potrebbe non essere qualcosa che si vuole succedere in ogni momento. Questo suggerisce che dovrebbero essere tipi di riferimento. Ad esempio, un pulsante sullo schermo può essere copiato concettualmente. La copia non sarà del tutto identica all’originale. Un clic sulla copia non attiverà l’originale. La copia non occuperà la stessa posizione sullo schermo. Se passi il pulsante o lo metti in una nuova variabile, probabilmente vorrai fare riferimento al pulsante originale e vorresti solo fare una copia quando è richiesta esplicitamente. Ciò significa che il tuo tipo di pulsante dovrebbe essere un tipo di riferimento.

I controller di vista e finestra sono un esempio simile. Potrebbero essere riproducibili, in teoria, ma non è quasi mai quello che vorresti fare. Dovrebbero essere tipi di riferimento.

Che dire dei tipi di modello? Potresti avere un tipo di utente che rappresenta un utente sul tuo sistema o un tipo di crimine che rappresenta un’azione intrapresa da un utente. Questi sono abbastanza copiabili, quindi dovrebbero probabilmente essere tipi di valore. Tuttavia, probabilmente vorrai che gli aggiornamenti di Crime dell’Utente realizzati in un punto del tuo programma siano visibili ad altre parti del programma. Questo suggerisce che i tuoi utenti dovrebbero essere gestiti da una sorta di controller utente che sarebbe un tipo di riferimento .

Le collezioni sono un caso interessante. Questi includono cose come array e dizionari, oltre a stringhe. Sono copiabili? Ovviamente. Copiare qualcosa che vuoi accadere facilmente e spesso? Questo è meno chiaro.

La maggior parte delle lingue dice “no” a questo e rende i loro tipi di riferimento delle collezioni. Questo è vero in Objective-C e Java, Python e JavaScript e in quasi tutte le altre lingue a cui riesco a pensare. (Una delle principali eccezioni è C ++ con tipi di raccolta STL, ma C ++ è il pazzo delirante del mondo linguistico che fa tutto stranamente.)

Swift ha detto “sì”, il che significa che tipi come Array e Dictionary e String sono strutture piuttosto che classi. Vengono copiati sull’assegnazione e passandoli come parametri. Questa è una scelta del tutto ragionevole fintanto che la copia è a buon mercato, che Swift cerca molto duramente di realizzare. …

Inoltre, non utilizzare la class quando è necessario eseguire l’override di ogni singola istanza di una funzione, ovvero che non dispongono di alcuna funzionalità condivisa .

Quindi, invece di avere diverse sottoclassi di una class. Utilizzare diverse strutture conformi a un protocollo.

La struttura è molto più veloce di Class. Inoltre, se hai bisogno di ereditarietà, devi usare Class. Il punto più importante è che la class è un tipo di riferimento mentre la struttura è un tipo di valore. per esempio,

 class Flight { var id:Int? var description:String? var destination:String? var airlines:String? init(){ id = 100 description = "first ever flight of Virgin Airlines" destination = "london" airlines = "Virgin Airlines" } } struct Flight2 { var id:Int var description:String var destination:String var airlines:String } 

ora consente di creare l’istanza di entrambi.

 var flightA = Flight() var flightB = Flight2.init(id: 100, description:"first ever flight of Virgin Airlines", destination:"london" , airlines:"Virgin Airlines" ) 

ora passiamo queste istanze a due funzioni che modificano l’id, la descrizione, la destinazione ecc.

 func modifyFlight(flight:Flight) -> Void { flight.id = 200 flight.description = "second flight of Virgin Airlines" flight.destination = "new york" flight.airlines = "Virgin Airlines" } 

anche,

 func modifyFlight2(flight2: Flight2) -> Void { var passedFlight = flight2 passedFlight.id = 200 passedFlight.description = "second flight from virgin airlines" } 

così,

 modifyFlight(flight: flightA) modifyFlight2(flight2: flightB) 

ora se stampiamo l’ID e la descrizione del flightA, otteniamo

 id = 200 description = "second flight of Virgin Airlines" 

Qui, possiamo vedere l’id e la descrizione di FlightA è cambiato perché il parametro passato al metodo di modifica punta effettivamente all’indirizzo di memoria dell’object flightA (tipo di riferimento).

ora se stampiamo l’id e la descrizione dell’istanza FLightB che otteniamo,

 id = 100 description = "first ever flight of Virgin Airlines" 

Qui possiamo vedere che l’istanza FlightB non viene modificata perché nel metodo modifyFlight2, l’istanza effettiva di Flight2 è passata anziché di riferimento (tipo di valore).

Con le classi si ottiene l’ereditarietà e si passa per riferimento, le strutture non hanno ereditarietà e vengono passate per valore.

Ci sono grandi sessioni WWDC su Swift, a questa domanda specifica viene data una risposta dettagliata in uno di essi. Assicurati di guardarli, in quanto ti consentirà di accelerare molto più velocemente rispetto alla guida della lingua o all’iBook.

Rispondendo alla domanda dal punto di vista dei tipi di valore e dei tipi di riferimento, da questo post sul blog di Apple sembrerebbe molto semplice:

Utilizzare un tipo di valore [ad es. Struct, enum] quando:

  • Confrontare i dati di istanza con == ha senso
  • Vuoi che le copie abbiano uno stato indipendente
  • I dati verranno utilizzati nel codice attraverso più thread

Utilizzare un tipo di riferimento [ad esempio class] quando:

  • Confrontare l’id quadro dell’istanza con === ha senso
  • Vuoi creare uno stato condiviso e mutevole

Come accennato in questo articolo, una class senza proprietà scrivibili si comporterebbe in modo identico con una struttura, con (aggiungerò) un avvertimento: le strutture sono le migliori per i modelli thread-safe – un requisito sempre più imminente nella moderna architettura delle app.

Non direi che le strutture offrono meno funzionalità.

Certo, il sé è immutabile tranne che in una funzione mutante, ma questo è tutto.

L’ereditarietà funziona bene finché si mantiene la buona idea che ogni class dovrebbe essere astratta o definitiva.

Implementa classi astratte come protocolli e classi finali come strutture.

La cosa bella delle strutture è che puoi rendere i tuoi campi mutabili senza creare uno stato mutabile condiviso perché la copia su scrittura si occupa di questo 🙂

Ecco perché le proprietà / campi nel seguente esempio sono tutti mutabili, cosa che non farei in Java o C # o classi veloci.

Esempio di struttura di ereditarietà con un po ‘di sporco e semplice utilizzo in basso nella funzione denominata “esempio”:

 protocol EventVisitor { func visit(event: TimeEvent) func visit(event: StatusEvent) } protocol Event { var ts: Int64 { get set } func accept(visitor: EventVisitor) } struct TimeEvent : Event { var ts: Int64 var time: Int64 func accept(visitor: EventVisitor) { visitor.visit(self) } } protocol StatusEventVisitor { func visit(event: StatusLostStatusEvent) func visit(event: StatusChangedStatusEvent) } protocol StatusEvent : Event { var deviceId: Int64 { get set } func accept(visitor: StatusEventVisitor) } struct StatusLostStatusEvent : StatusEvent { var ts: Int64 var deviceId: Int64 var reason: String func accept(visitor: EventVisitor) { visitor.visit(self) } func accept(visitor: StatusEventVisitor) { visitor.visit(self) } } struct StatusChangedStatusEvent : StatusEvent { var ts: Int64 var deviceId: Int64 var newStatus: UInt32 var oldStatus: UInt32 func accept(visitor: EventVisitor) { visitor.visit(self) } func accept(visitor: StatusEventVisitor) { visitor.visit(self) } } func readEvent(fd: Int) -> Event { return TimeEvent(ts: 123, time: 56789) } func example() { class Visitor : EventVisitor { var status: UInt32 = 3; func visit(event: TimeEvent) { print("A time event: \(event)") } func visit(event: StatusEvent) { print("A status event: \(event)") if let change = event as? StatusChangedStatusEvent { status = change.newStatus } } } let visitor = Visitor() readEvent(1).accept(visitor) print("status: \(visitor.status)") } 

In Swift, è stato introdotto un nuovo modello di programmazione noto come programmazione orientata al protocollo.

Modello creativo:

In swift, Struct è un tipo di valore che viene automaticamente clonato. Pertanto otteniamo il comportamento richiesto per implementare il modello prototipo gratuitamente.

Mentre le classi sono il tipo di riferimento, che non viene automaticamente clonato durante l’assegnazione. Per implementare il modello prototipo, le classi devono adottare il protocollo NSCopying .


La copia poco profonda duplica solo il riferimento, che punta a quegli oggetti mentre la copia profonda duplica il riferimento dell’object.


Implementare una copia profonda per ogni tipo di riferimento è diventato un compito noioso. Se le classi includono un ulteriore tipo di riferimento, dobbiamo implementare un modello prototipo per ciascuna delle proprietà dei riferimenti. E quindi dobbiamo effettivamente copiare l’intero grafico dell’object implementando il protocollo NSCopying .

 class Contact{ var firstName:String var lastName:String var workAddress:Address // Reference type } class Address{ var street:String ... } 

Usando le strutture e le enumerazioni , abbiamo reso il nostro codice più semplice dal momento che non dobbiamo implementare la logica di copia.

Molte API Cocoa richiedono sottoclassi NSObject, che ti obbligano a utilizzare la class. Ma a parte questo, puoi utilizzare i seguenti casi del blog Swift di Apple per decidere se utilizzare un tipo di valore struct / enum o un tipo di riferimento di class.

https://developer.apple.com/swift/blog/?id=10

Un punto che non attira l’attenzione in queste risposte è che una variabile che tiene una class contro una struttura può essere let pur consentendo modifiche sulle proprietà dell’object, mentre non è ansible farlo con una struttura.

Questo è utile se non vuoi che la variabile punti mai a un altro object, ma devi comunque modificare l’object, cioè nel caso di avere molte variabili di istanza che desideri aggiornare una dopo l’altra. Se si tratta di una struct, è necessario consentire alla variabile di essere resettata su un altro object utilizzando var per fare ciò, poiché un tipo di valore costante in Swift consente correttamente la mutazione zero, mentre i tipi di riferimento (classi) non si comportano in questo modo .

Poiché struct sono tipi di valore e puoi creare facilmente la memoria che memorizza nello stack.Struct può essere facilmente accessibile e, dopo l’ambito del lavoro, è facilmente trasferito dalla memoria dello stack tramite pop dalla parte superiore dello stack. D’altra parte la class è un tipo di riferimento che memorizza nell’heap e le modifiche apportate in un object di class avranno un impatto sull’altro object poiché sono strettamente accoppiate e il tipo di riferimento. Tutti i membri di una struttura sono pubblici mentre tutti i membri di una class sono privati .

Gli svantaggi di struct è che non può essere ereditato.

  • Struttura e class sono tipi di dati sfidati dall’utente

  • Di default, la struttura è pubblica mentre la class è privata

  • La class implementa il principio dell’incapsulamento

  • Gli oggetti di una class vengono creati nella memoria heap

  • La class viene utilizzata per la riusabilità, mentre la struttura viene utilizzata per raggruppare i dati nella stessa struttura

  • I membri dei dati di struttura non possono essere inizializzati direttamente ma possono essere assegnati all’esterno della struttura

  • I membri di dati di class possono essere inizializzati direttamente dal parametro meno costruttore e assegnati dal costruttore parametrizzato