Giocando con Swift, proveniente da un background Java, perché dovresti scegliere un Struct anziché un Class? Sembra che siano la stessa cosa, con un Struct che offre meno funzionalità. Perché sceglierlo allora?
Giocando con Swift, proveniente da un background Java, perché dovresti scegliere un Struct anziché un Class? Sembra che siano la stessa cosa, con un Struct che offre meno funzionalità. Perché sceglierlo allora?
Risposte:
Secondo il molto popolare talk del WWDC 2015, Protocol Oriented Programming in 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é la copia è molto più sicura che avere più riferimenti alla stessa istanza che accade con le classi. Ciò è 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 in altri luoghi, non devi mai preoccuparti che quell'altro luogo cambi il valore della tua variabile sotto di te.
Con Structs, è molto meno necessario preoccuparsi di perdite di memoria o di più thread in competizione per accedere / modificare una singola istanza di una variabile. (Per i più tecnicamente preparati, l'eccezione è quando si acquisisce una struttura all'interno di una chiusura perché in realtà sta catturando un riferimento all'istanza a meno che non si contrassegni esplicitamente per essere copiato).
Le classi possono anche gonfiarsi perché una classe può ereditare solo da una singola superclasse. Questo ci incoraggia a creare enormi superclassi che racchiudono molte abilità diverse che sono solo vagamente correlate. L'uso dei protocolli, in particolare con le estensioni di protocollo in cui è possibile fornire implementazioni ai protocolli, consente di eliminare la necessità per le classi di ottenere questo tipo di comportamento.
Il discorso illustra questi scenari in cui le classi sono preferite:
- La copia o il confronto di istanze non ha senso (ad es. Window)
- La durata dell'istanza è legata ad effetti esterni (ad esempio, File temporaneo)
- Le istanze sono solo "pozzi" - condotte di sola scrittura verso uno stato esterno (ad es. CGContext)
Implica che le strutture dovrebbero essere predefinite e che le classi dovrebbero essere un fallback.
D'altra parte, la documentazione di The Swift Programming Language è alquanto contraddittoria:
Le istanze della struttura vengono sempre passate per valore e le istanze di classe vengono sempre passate per riferimento. Ciò significa che sono adatti a diversi tipi di attività. Considerando i costrutti e le funzionalità di dati necessari per un progetto, decidere se ciascun costrutto di dati deve essere definito come una classe o una struttura.
Come linea guida generale, considerare 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é referenziati quando si assegna o si passa attorno a un'istanza di quella struttura.
- Qualsiasi proprietà memorizzata dalla struttura è a sua volta un tipo di valore, che dovrebbe essere copiato piuttosto che referenziato.
- Non è necessario che la struttura erediti le proprietà o il 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 fare riferimento a intervalli all'interno di una serie, magari incapsulando una proprietà start e una proprietà length, entrambe di tipo Int.
- Un punto in un sistema di coordinate 3D, forse incapsulando le proprietà x, ye z, ciascuna di tipo Double.
In tutti gli altri casi, definire una classe e creare istanze di quella classe 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 sta sostenendo che dovremmo usare le classi per impostazione predefinita e usare le strutture solo in circostanze specifiche. Alla fine, è necessario comprendere le implicazioni del mondo reale dei tipi di valore rispetto ai tipi di riferimento e quindi è possibile prendere una decisione informata su quando utilizzare le strutture o le classi. Inoltre, tieni presente che questi concetti sono in continua evoluzione e che la documentazione del linguaggio di programmazione rapida è stata scritta prima che fosse pronunciata la conferenza sulla programmazione orientata al protocollo.
In practice, this means that most custom data constructs should be classes, not structures.
Puoi spiegarmi come, dopo averlo letto, ottieni che la maggior parte dei set di dati dovrebbe essere una struttura e non una classe? Hanno dato un insieme specifico di regole quando qualcosa dovrebbe essere una struttura e praticamente hanno detto "tutti gli altri scenari in cui una classe è migliore".
Poiché le istanze struct sono allocate in pila e le istanze di classe sono allocate in heap, le strutture possono talvolta essere drasticamente più veloci.
Tuttavia, dovresti sempre misurarlo da solo e decidere in base al tuo caso d'uso unico.
Si consideri il seguente esempio, che dimostra 2 strategie di wrapping del Int
tipo di dati usando 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 è disponibile all'indirizzo https://github.com/knguyen2708/StructVsClassPerformance
AGGIORNAMENTO (27 marzo 2018) :
A partire da Swift 4.0, Xcode 9.2, con Release build su iPhone 6S, iOS 11.2.6, l'impostazione del compilatore Swift è -O -whole-module-optimization
:
class
la versione impiegava 2,06 secondistruct
la versione impiegava 4,17e-08 secondi (50.000.000 di volte più veloce)(Non eseguo più la media di più corse, poiché le varianze sono molto piccole, inferiori al 5%)
Nota : la differenza è molto meno drammatica senza l'ottimizzazione dell'intero modulo. Sarei felice se qualcuno potesse indicare cosa fa realmente la bandiera.
AGGIORNAMENTO (7 maggio 2016) :
A partire da Swift 2.2.1, Xcode 7.3, con Release build su iPhone 6S, iOS 9.3.1, media su 5 run, l'impostazione del compilatore Swift è -O -whole-module-optimization
:
class
la versione ha richiesto 2.159942142sstruct
versione impiegata 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 strutture / classi con 10 campi invece di 1. Sorprendentemente, i risultati non variano molto.
RISULTATI ORIGINALI (1 giugno 2014):
(Ha funzionato su struct / class con 1 campo, non 10)
A partire da Swift 1.2, Xcode 6.3.2, con Release build su iPhone 5S, iOS 8.3, media su 5 run
class
la versione impiegava 9.788332333sstruct
la versione impiegava 0,010532942s (900 volte più veloce)VECCHI RISULTATI (da tempi sconosciuti)
(Ha funzionato su struct / class con 1 campo, non 10)
Con il rilascio sviluppato sul mio MacBook Pro:
class
versione impiegò 1.10082 secstruct
versione ha impiegato 0,02324 sec (50 volte più veloce)Ho creato l'essenza per questo con semplici esempi. https://github.com/objc-swift/swift-classes-vs-structures
le strutture non possono ereditare rapidamente. Se vuoi
class Vehicle{
}
class Car : Vehicle{
}
Andare per una lezione.
Le strutture veloci passano per valore e le istanze di classe passano per riferimento.
Costante strutturale e variabili
Esempio (usato al 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 classe invece questa è un'espressione valida. Perché in una classe la costante immutabile è il riferimento alla classe stessa non alle sue variabili di istanza (A meno che quelle variabili non siano definite costanti)
Ecco alcuni altri motivi da considerare:
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 classe, dovresti aggiungere l'inizializzatore e mantenere l'inizializzatore ...
I tipi di raccolta di base come Array
sono strutture. Più li usi nel tuo codice, più ti abituerai a passare per valore anziché a 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]
Apparentemente l'immutabilità rispetto alla mutabilità è un argomento enorme, ma molte persone intelligenti pensano che l'immutabilità - le strutture in questo caso - sia preferibile. Oggetti mutabili vs immutabili
internal
dell'ambito.
mutating
modo da essere esplicito su quali funzioni cambiano il loro stato. Ma la loro natura come tipi di valore è ciò che è importante. Se si dichiara uno struct con let
non è possibile chiamare alcuna funzione mutante su di esso. Il video del WWDC 15 su Una migliore programmazione attraverso tipi di valore è una risorsa eccellente su questo.
Supponendo che sappiamo che Struct è un tipo di valore e Class è un tipo di riferimento .
Se non sai quale sia un tipo di valore e un tipo di riferimento, vedi Qual è la differenza tra passaggio per riferimento e passaggio per valore?
Basato sul post di mikeash :
... Vediamo prima alcuni esempi estremi, ovvi. I numeri interi sono ovviamente copiabili. Dovrebbero essere tipi di valore. I socket di rete non possono essere copiati in modo ragionevole. Dovrebbero essere tipi di riferimento. I punti, come nelle coppie x, y, sono copiabili. Dovrebbero essere tipi di valore. Un controller che rappresenta un disco non può essere copiato in modo ragionevole. Questo dovrebbe essere un tipo di riferimento.
Alcuni tipi possono essere copiati ma potrebbe non essere qualcosa che vuoi che accada sempre. Ciò suggerisce che dovrebbero essere tipi di riferimento. Ad esempio, è possibile copiare concettualmente un pulsante sullo schermo. 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 si passa il pulsante o lo si inserisce in una nuova variabile, probabilmente si farà riferimento al pulsante originale e si vorrebbe fare una copia solo quando viene esplicitamente richiesto. Ciò significa che il tipo di pulsante deve essere un tipo di riferimento.
I controller di vista e finestra sono un esempio simile. Potrebbero essere copiabili, presumibilmente, ma non è quasi mai quello che vorresti fare. Dovrebbero essere tipi di riferimento.
E i 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 desideri che gli aggiornamenti al crimine di un utente effettuati in un punto del programma siano visibili ad altre parti del programma. Ciò suggerisce che i tuoi utenti dovrebbero essere gestiti da una sorta di controller utente che sarebbe un tipo di riferimento . per esempio
struct User {} class UserController { var users: [User] func add(user: User) { ... } func remove(userNamed: String) { ... } func ... }
Le collezioni sono un caso interessante. Questi includono cose come array e dizionari, nonché stringhe. Sono copiabili? Ovviamente. Copiare è qualcosa che vuoi che accada facilmente e spesso? Questo è meno chiaro.
La maggior parte delle lingue dice "no" a questo e rende i loro tipi di riferimento di raccolte. Questo è vero in Objective-C, Java, Python e JavaScript e in quasi tutte le altre lingue che mi vengono in mente. (Un'eccezione importante è il C ++ con i tipi di raccolta STL, ma C ++ è il folle delirante del mondo linguistico che fa tutto in modo strano.)
Swift ha detto "sì", il che significa che tipi come Array, Dictionary e String sono strutture piuttosto che classi. Vengono copiati al momento dell'assegnazione e al loro passaggio come parametri. Questa è una scelta del tutto ragionevole a condizione che la copia sia economica, cosa che Swift si impegna a fondo. ...
Personalmente non nomina le mie lezioni in quel modo. Di solito il mio UserManager viene chiamato invece UserController ma l'idea è la stessa
Inoltre, non usare la classe quando devi sovrascrivere ogni singola istanza di una funzione, cioè non avere alcuna funzionalità condivisa .
Quindi invece di avere diverse sottoclassi di una classe. Utilizzare diverse strutture conformi a un protocollo.
Un altro caso ragionevole per le strutture è quando vuoi fare un delta / diff del tuo vecchio e nuovo modello. Con i tipi di riferimenti non puoi farlo immediatamente. Con i tipi di valore le mutazioni non sono condivise.
Alcuni vantaggi:
La struttura è molto più veloce di Class. Inoltre, se hai bisogno di ereditarietà, devi usare Class. Il punto più importante è che la classe è 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 volo A, otteniamo
id = 200
description = "second flight of Virgin Airlines"
Qui, possiamo vedere l'id e la descrizione di FlightA è cambiata perché il parametro passato al metodo di modifica in realtà punta all'indirizzo di memoria dell'oggetto flightA (tipo di riferimento).
ora se stampiamo l'id e la descrizione dell'istanza di FLightB che otteniamo,
id = 100
description = "first ever flight of Virgin Airlines"
Qui possiamo vedere che l'istanza FlightB non è cambiata perché nel metodo modiflight2, l'istanza effettiva di Flight2 è passata anziché riferimento (tipo di valore).
Here we can see that the FlightB instance is not changed
Structs
sono value type
e Classes
sonoreference type
Utilizzare un value
tipo quando:
Utilizzare un reference
tipo quando:
Ulteriori informazioni sono disponibili anche nella documentazione di Apple
https://docs.swift.org/swift-book/LanguageGuide/ClassesAndStructures.html
Informazioni aggiuntive
I tipi di valore rapidi vengono mantenuti nello stack. In un processo, ogni thread ha il proprio spazio di stack, quindi nessun altro thread sarà in grado di accedere direttamente al tipo di valore. Quindi nessuna condizione di competizione, blocco, deadlock o qualsiasi complessità di sincronizzazione del thread correlata.
I tipi di valore non richiedono allocazione dinamica della memoria o conteggio dei riferimenti, entrambi operazioni costose. Allo stesso tempo, i metodi sui tipi di valore vengono inviati staticamente. Questi creano un enorme vantaggio a favore di tipi di valore in termini di prestazioni.
Come promemoria ecco un elenco di Swift
Tipi di valore:
Tipi di riferimento:
Rispondere alla domanda dal punto di vista dei tipi di valore rispetto ai tipi di riferimento, da questo post sul blog di Apple sembrerebbe molto semplice:
Utilizzare un tipo di valore [es. Struct, enum] quando:
- Il confronto dei dati dell'istanza con == ha senso
- Volete che le copie abbiano uno stato indipendente
- I dati verranno utilizzati nel codice su più thread
Utilizzare un tipo di riferimento [ad es. Classe] quando:
- Confrontare l'identità dell'istanza con === ha senso
- Volete creare uno stato condiviso e mutabile
Come menzionato in quell'articolo, una classe senza proprietà scrivibili si comporterà in modo identico a una struttura, con (aggiungerò) un avvertimento: le strutture sono le migliori per i modelli thread-safe - un requisito sempre più imminente nell'architettura delle app moderne.
Con le classi si ottiene l'ereditarietà e si passa per riferimento, le strutture non hanno ereditarietà e vengono passate per valore.
Ci sono ottime sessioni WWDC su Swift, a questa domanda specifica viene data risposta in dettaglio in una di esse. Assicurati di guardarli, perché ti consentiranno di accelerare molto più rapidamente rispetto alla Guida linguistica o all'iBook.
Non direi che le strutture offrano meno funzionalità.
Certo, il sé è immutabile se non in una funzione mutante, ma questo è tutto.
L'ereditarietà funziona bene fintanto che rimani fedele alla buona vecchia idea che ogni classe dovrebbe essere astratta o finale.
Implementare le classi astratte come protocolli e le 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 se ne occupa :)
Ecco perché le proprietà / i campi nel seguente esempio sono tutti mutabili, cosa che non farei in Java o C # o in classi veloci .
Esempio di struttura ereditaria con un po 'di utilizzo sporco e semplice nella parte inferiore 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.
Motivo creazionale:
In breve, Struct è un tipo di valore che viene clonato automaticamente. Pertanto otteniamo il comportamento richiesto per implementare gratuitamente il modello prototipo.
Considerando che 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 NSCopying
protocollo.
La copia superficiale duplica solo il riferimento, che punta a quegli oggetti mentre la copia profonda duplica il riferimento dell'oggetto.
L'implementazione della copia profonda per ciascun tipo di riferimento è diventata un'attività noiosa. 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'oggetto implementando il NSCopying
protocollo.
class Contact{
var firstName:String
var lastName:String
var workAddress:Address // Reference type
}
class Address{
var street:String
...
}
Usando le strutture e gli enum , abbiamo semplificato il nostro codice poiché non dobbiamo implementare la logica di copia.
Molte API Cocoa richiedono sottoclassi NSObject, che ti costringono a utilizzare la classe. Oltre a ciò, puoi utilizzare i seguenti casi dal blog Swift di Apple per decidere se utilizzare un tipo di valore struct / enum o un tipo di riferimento di classe.
Un punto che non attira l'attenzione su queste risposte è che una variabile che detiene una classe rispetto a una struttura può essere un let
po 'che consente comunque di modificare le proprietà dell'oggetto, mentre non è possibile farlo con una struttura.
Ciò è utile se non si desidera che la variabile punti mai verso un altro oggetto, ma è comunque necessario modificarlo, ovvero nel caso in cui si disponga di molte variabili di istanza che si desidera aggiornare una dopo l'altra. Se si tratta di una struttura, è necessario consentire la reimpostazione totale della variabile su un altro oggetto var
per poterlo fare, 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.
Dato che struct sono tipi di valore e puoi creare molto facilmente la memoria che viene archiviata nello stack. La struttura può essere facilmente accessibile e dopo l'ambito del lavoro viene facilmente allocata dalla memoria dello stack attraverso il pop dalla parte superiore dello stack. D'altra parte la classe è un tipo di riferimento che memorizza nell'heap e le modifiche apportate in un oggetto di classe avranno un impatto su un altro oggetto poiché sono strettamente accoppiate e di tipo di riferimento. Tutti i membri di una struttura sono pubblici mentre tutti i membri di una classe sono privati .
Lo svantaggio di struct è che non può essere ereditato.
La struttura e la classe sono tipi di dati sfidati dall'utente
Per impostazione predefinita, la struttura è pubblica mentre la classe è privata
La classe implementa il principio dell'incapsulamento
Gli oggetti di una classe vengono creati nella memoria dell'heap
La classe viene utilizzata per la riutilizzabilità, mentre la struttura viene utilizzata per raggruppare i dati nella stessa struttura
I membri dei dati della struttura non possono essere inizializzati direttamente ma possono essere assegnati dall'esterno della struttura
I membri dei dati di classe possono essere inizializzati direttamente dal parametro less constructor e assegnati dal costruttore con parametri