Quando usi uno struct invece di una classe? [chiuso]


174

Quali sono le tue regole empiriche per quando usare le strutture rispetto alle classi? Sto pensando alla definizione C # di quei termini, ma se la tua lingua ha concetti simili mi piacerebbe anche sentire la tua opinione.

Tendo a usare le classi per quasi tutto, e uso le strutture solo quando qualcosa è molto semplicistico e dovrebbe essere un tipo di valore, come un PhoneNumber o qualcosa del genere. Ma questo sembra un uso relativamente minore e spero che ci siano casi d'uso più interessanti.



7
@Frustrated Non è possibile contrassegnare una domanda come duplicata di qualcosa su un altro sito, sebbene sia sicuramente correlata.
Adam Lear

@Anna Lear: lo so, ho votato per migrare non per chiudere come duplicato. Una volta migrato, può essere chiuso correttamente come duplicato .
FrustratedWithFormsDesigner,

5
@Frustrato Non penso che debba essere migrato.
Adam Lear

15
Questa sembra una domanda molto più adatta a P.SE vs. SO. Ecco perché l'ho chiesto qui.
RationalGeek,

Risposte:


169

La regola generale da seguire è che le strutture dovrebbero essere raccolte piccole (semplici) di proprietà correlate, che sono immutabili una volta create; per qualsiasi altra cosa, usa una classe.

C # è carino in quanto le strutture e le classi non hanno differenze esplicite nella dichiarazione oltre alla parola chiave di definizione; quindi, se ritieni di dover "aggiornare" una struttura in una classe o viceversa "declassare" una classe in una struttura, è principalmente una semplice questione di cambiare la parola chiave (ci sono alcuni altri gotcha; le strutture non possono derivare da qualsiasi altra classe o tipo di struttura e non possono definire esplicitamente un costruttore predefinito senza parametri).

Dico "principalmente", perché la cosa più importante da sapere sulle strutture è che, poiché sono tipi di valore, trattarli come classi (tipi di riferimento) può finire con un dolore e mezzo. In particolare, rendere mutabili le proprietà di una struttura può causare comportamenti imprevisti.

Ad esempio, supponiamo di avere una classe SimpleClass con due proprietà, A e B. È possibile creare un'istanza di una copia di questa classe, inizializzare A e B e quindi passare l'istanza a un altro metodo. Tale metodo modifica ulteriormente A e B. Tornando alla funzione chiamante (quella che ha creato l'istanza), A e B dell'istanza avranno i valori dati loro dal metodo chiamato.

Ora, lo rendi una struttura. Le proprietà sono ancora modificabili. Esegui le stesse operazioni con la stessa sintassi di prima, ma ora i nuovi valori di A e B non sono nell'istanza dopo aver chiamato il metodo. Quello che è successo? Bene, la tua classe ora è una struttura, il che significa che è un tipo di valore. Se si passa un tipo di valore a un metodo, l'impostazione predefinita (senza una parola chiave out o ref) è passare "per valore"; una copia superficiale dell'istanza viene creata per essere utilizzata dal metodo e quindi distrutta quando il metodo viene eseguito lasciando intatta l'istanza iniziale.

Questo diventa ancora più confuso se si dovesse avere un tipo di riferimento come membro della propria struttura (non vietato, ma pratica estremamente negativa in quasi tutti i casi); la classe non verrebbe clonata (solo il riferimento della struttura ad essa), quindi le modifiche alla struttura non influirebbero sull'oggetto originale, ma le modifiche alla sottoclasse della struttura influenzeranno l'istanza dal codice chiamante. Questo può facilmente mettere le strutture mutabili in stati molto incoerenti che possono causare errori molto lontani da dove si trova il vero problema.

Per questo motivo, praticamente ogni autorità su C # dice di rendere sempre immutabili le tue strutture; consentire al consumatore di specificare i valori delle proprietà solo sulla costruzione di un oggetto e non fornire mai alcun mezzo per modificare i valori di tale istanza. I campi di sola lettura o le proprietà di sola lettura sono la regola. Se il consumatore desidera modificare il valore, può creare un nuovo oggetto basato sui valori di quello precedente, con le modifiche che desidera, oppure può chiamare un metodo che farà lo stesso. Questo li costringe a trattare una singola istanza della tua struttura come un "valore" concettuale, indivisibile e distinto da (ma probabilmente equiparabile a) tutti gli altri. Se eseguono un'operazione su un "valore" memorizzato dal tuo tipo, ottengono un nuovo "valore" diverso dal loro valore iniziale,

Per un buon esempio, guarda il tipo DateTime. Non è possibile assegnare direttamente nessuno dei campi di un'istanza DateTime; devi crearne uno nuovo o chiamare un metodo su quello esistente che produrrà una nuova istanza. Questo perché una data e un'ora sono un "valore", come il numero 5, e una modifica al numero 5 comporta un nuovo valore che non è 5. Solo perché 5 + 1 = 6 non significa che 5 è ora 6 perché ne hai aggiunto 1. DateTimes funzionano allo stesso modo; 12:00 non "diventa" 12:01 se aggiungi un minuto, ottieni invece un nuovo valore 12:01 diverso da 12:00. Se questo è uno stato logico per il tuo tipo (buoni esempi concettuali che non sono integrati in .NET sono Money, Distance, Weight e altre quantità di un UOM in cui le operazioni devono tenere conto di tutte le parti del valore), quindi utilizzare una struttura e progettarla di conseguenza. Nella maggior parte degli altri casi in cui le voci secondarie di un oggetto devono essere modificabili indipendentemente, utilizzare una classe.


1
Buona risposta! Vorrei indicarne uno leggendolo alla metodologia e al libro "Domain Driven Development", che sembra in qualche modo correlato alla tua opinione sul perché si dovrebbero usare classi o strutture basate sul concetto che si sta effettivamente cercando di implementare
Maxim Popravko

1
Solo un piccolo dettaglio degno di nota: non è possibile definire il proprio costruttore predefinito su una struttura. Devi accontentarti di quello che inizializza tutto al suo valore predefinito. Di solito, è piuttosto secondario, specialmente in classe che sono buoni candidati per essere una struttura. Ciò significa che cambiare una classe in una struttura può implicare qualcosa di più del semplice cambiare una parola chiave.
Laurent Bourgault-Roy,

1
@ LaurentBourgault-Roy - Vero. Ci sono anche altri gotcha; le strutture non possono avere una gerarchia ereditaria, per esempio. Possono implementare interfacce, ma non possono derivare da qualcosa di diverso da System.ValueType (implicitamente dal fatto che sono strutture).
KeithS

Come sviluppatore JavaScript devo chiedere due cose. In che modo gli oggetti letterali differiscono dagli usi tipici delle strutture e perché è importante che le strutture siano immutabili?
Erik Reppen,

1
@ErikReppen: in risposta a entrambe le tue domande: quando una struttura viene passata in una funzione, viene passata per valore. Con una classe, stai passando un riferimento alla classe (sempre per valore). Eric Lippert discute perché questo è un problema: blogs.msdn.com/b/ericlippert/archive/2008/05/14/… . L'ultimo paragrafo di KeithS copre anche queste domande.
Brian,

14

La risposta: "Utilizzare una struttura per costrutti di dati puri e una classe per oggetti con operazioni" è sicuramente IMO errata. Se una struttura contiene un numero elevato di proprietà, una classe è quasi sempre più appropriata. Microsoft dice spesso, dal punto di vista dell'efficienza, se il tuo tipo è più grande di 16 byte dovrebbe essere una classe.

https://stackoverflow.com/questions/1082311/why-should-a-net-struct-be-less-than-16-bytes


1
Assolutamente. Vorrei votare se potessi. Non capisco da dove sia venuta l'idea che le strutture siano per i dati e gli oggetti no. Una struttura in C # è solo un meccanismo per l'allocazione nello stack. Vengono copiate copie dell'intera struttura anziché solo una copia di un puntatore oggetto.
mike30,

2
@mike - Le strutture C # non sono necessariamente allocate nello stack.
Lee,

1
@mike L'idea "Le strutture sono per i dati" deriva probabilmente dall'abitudine acquisita da molti anni di programmazione in C. C non ha classi e le sue strutture sono solo contenitori di dati.
Konamiman,

Ovviamente ci sono almeno 3 programmatori C (sono stati votati) che hanno letto la risposta di Michael K ;-)
bytedev

10

La differenza più importante tra a classe a structè ciò che accade nella seguente situazione:

  Thing thing1 = new Thing ();
  thing1.somePropertyOrField = 5;
  Thing thing2 = thing1;
  thing2.somePropertyOrField = 9;

Quale dovrebbe essere l'effetto dell'ultima affermazione su thing1.somePropertyOrField? Se Thinguna struttura, ed somePropertyOrFieldè un campo pubblico esposto, gli oggetti thing1e thing2saranno "staccati" l'uno dall'altro, quindi quest'ultima affermazione non avrà alcun effetto thing1. Se Thingè una classe, allora thing1e thing2sarà collegata l'una all'altra, e quindi quest'ultima istruzione scriverà thing1.somePropertyOrField. Si dovrebbe usare una struttura nei casi in cui la prima semantica avrebbe più senso, e si dovrebbe usare una classe nei casi in cui la seconda semantica avrebbe più senso.

Nota che mentre alcune persone consigliano che il desiderio di rendere qualcosa di mutevole è un argomento a favore del fatto che sia la classe, suggerirei che è vero il contrario: se qualcosa che esiste allo scopo di conservare alcuni dati sarà mutabile, e se non sarà ovvio se le istanze sono associate a qualcosa, la cosa dovrebbe essere una struttura (probabilmente con campi esposti) in modo da chiarire che le istanze non sono collegate a nient'altro.

Ad esempio, considera le dichiarazioni:

  Persona somePerson = myPeople.GetPerson ("123-45-6789");
  somePerson.Name = "Mary Johnson"; // Era stato "Mary Smith"

La seconda affermazione modificherà le informazioni archiviate myPeople? Se Personè una struttura a campo esposto, non lo farà, e il fatto che non lo sarà sarà una conseguenza ovvia del suo essere una struttura a campo esposto ; se Personè una struttura e si desidera aggiornare myPeople, si dovrebbe chiaramente fare qualcosa di simile myPeople.UpdatePerson("123-45-6789", somePerson). Se Personè una classe, tuttavia, potrebbe essere molto più difficile determinare se il codice sopra non aggiornerà mai il contenuto MyPeople, lo aggiornerà sempre o talvolta lo aggiornerà.

Per quanto riguarda l'idea che le strutture debbano essere "immutabili", non sono d'accordo in generale. Esistono casi di utilizzo validi per strutture "immutabili" (in cui gli invarianti sono fatti valere in un costruttore) ma richiedono che una intera struttura debba essere riscritta ogni volta che una parte di essa cambia è scomoda, dispendiosa e più adatta a causare bug rispetto alla semplice esposizione campi direttamente. Ad esempio, considera una PhoneNumberstruttura i cui campi includono, tra gli altri, AreaCodee Exchange, e supponiamo che uno abbia un List<PhoneNumber>. L'effetto di quanto segue dovrebbe essere abbastanza chiaro:

  per (int i = 0; i <myList.Count; i ++)
  {
    PhoneNumber theNumber = myList [i];
    if (theNumber.AreaCode == "312")
    {
      stringa newExchange = "";
      if (new312to708Exchanges.TryGetValue (theNumber.Exchange), out newExchange)
      {
        theNumber.AreaCode = "708";
        theNumber.Exchange = newExchange;
        myList [i] = theNumber;
      }
    }
  }

Si noti che nulla nel codice sopra è a conoscenza o si preoccupa per qualsiasi campo PhoneNumberdiverso da AreaCodee Exchange. Se PhoneNumberfosse una cosiddetta struttura "immutabile", sarebbe necessario che fornisse un withXXmetodo per ciascun campo, che restituirebbe una nuova istanza di struttura che contenesse il valore passato nel campo indicato, altrimenti sarebbe necessario per il codice come sopra per conoscere ogni campo nella struttura. Non proprio attraente.

A proposito, ci sono almeno due casi in cui le strutture possono ragionevolmente contenere riferimenti a tipi mutabili.

  1. La semantica della struttura indica che sta memorizzando l'identità dell'oggetto in questione, piuttosto che come scorciatoia per contenere le proprietà dell'oggetto. Ad esempio, un `KeyValuePair` conterrebbe le identità di alcuni pulsanti, ma non ci si aspetterebbe che contenga informazioni persistenti sulle posizioni di tali pulsanti, gli stati di evidenziazione, ecc.
  2. La struttura sa che contiene l'unico riferimento a quell'oggetto, nessun altro otterrà un riferimento e qualsiasi mutazione che verrà mai eseguita su quell'oggetto sarà stata fatta prima che un riferimento ad esso venga archiviato ovunque.

Nel primo scenario, l'IDENTITÀ dell'oggetto sarà immutabile; nel secondo, la classe dell'oggetto nidificato potrebbe non imporre l'immutabilità, ma la struttura che detiene il riferimento lo farà.


7

Tendo a usare le strutture solo quando è necessario un metodo, quindi una classe sembra troppo "pesante". Quindi a volte tratto le strutture come oggetti leggeri. ATTENZIONE: questo non sempre funziona e non è sempre la cosa migliore da fare, quindi dipende davvero dalla situazione!

RISORSE EXTRA

Per una spiegazione più formale, MSDN dice: http://msdn.microsoft.com/en-us/library/ms229017.aspx Questo link verifica ciò che ho menzionato sopra, le strutture dovrebbero essere usate solo per ospitare una piccola quantità di dati.


5
Le domande su un altro sito (anche se quel sito è Stack Overflow ) non contano come duplicati. Espandi la tua risposta per includere una risposta effettiva e non solo collegamenti ad altri luoghi. Se i collegamenti dovessero cambiare, la tua risposta così come sarà scritta sarà inutile e ci piace conservare le informazioni e rendere i programmatori il più autosufficienti possibile. Grazie!
Adam Lear

2
@Anna Lear Grazie per avermelo fatto notare, lo terrò a mente d'ora in poi!
rrazd,

2
-1 "Tendo a usare le strutture solo quando è necessario un metodo, quindi una classe sembra troppo" pesante "." ... pesante? cosa significa veramente? ... e stai davvero dicendo che solo perché esiste un metodo, allora dovrebbe probabilmente essere una struttura?
bytedev,

2

Utilizzare una struttura per costrutti di dati puri e una classe per oggetti con operazioni.

Se la struttura dei dati non necessita di controllo di accesso e non ha operazioni speciali oltre a get / set, utilizzare una struttura. Ciò rende ovvio che tutta quella struttura è un contenitore per i dati.

Se la tua struttura ha operazioni che modificano la struttura in un modo più complesso, usa una classe. Una classe può anche interagire con altre classi tramite argomenti ai metodi.

Pensala come la differenza tra un mattone e un aeroplano. Un mattone è una struttura; ha lunghezza, larghezza e altezza. Non può fare molto. Un aeroplano potrebbe avere più operazioni che lo mutano in qualche modo, come spostare le superfici di controllo e cambiare la spinta del motore. Sarebbe meglio come una classe.


2
"Usa una struttura per costrutti di dati puri e una classe per oggetti con operazioni" in C # non ha senso. Vedi la mia risposta qui sotto.
bytedev,

1
"Usa una struttura per costrutti di dati puri e una classe per oggetti con operazioni." Questo è vero per C / C ++, non per C #
taktak004,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.