Quando dovrei usare uno struct invece di una classe?


302

MSDN dice che dovresti usare le strutture quando hai bisogno di oggetti leggeri. Ci sono altri scenari in cui una struttura è preferibile rispetto a una classe?

Alcune persone potrebbero aver dimenticato che:

  1. le strutture possono avere metodi.
  2. le strutture non possono essere ereditate.

Comprendo le differenze tecniche tra strutture e classi, semplicemente non ho una buona idea di quando usare una struttura.


Solo un promemoria: ciò che la maggior parte delle persone tende a dimenticare in questo contesto è che anche le strutture in C # possono avere metodi.
petr k.

Risposte:


295

MSDN ha la risposta: scegliere tra classi e strutture .

Fondamentalmente, quella pagina ti dà un elenco di controllo di 4 elementi e dice di usare una classe a meno che il tuo tipo non soddisfi tutti i criteri.

Non definire una struttura a meno che il tipo non abbia tutte le seguenti caratteristiche:

  • Rappresenta logicamente un singolo valore, simile ai tipi primitivi (intero, doppio e così via).
  • Ha una dimensione dell'istanza inferiore a 16 byte.
  • È immutabile
  • Non dovrà essere inscatolato frequentemente.

1
Forse mi manca qualcosa di ovvio ma non riesco proprio a capire il ragionamento dietro la parte "immutabile". Perché è necessario? Qualcuno potrebbe spiegarlo?
Tamas Czinege,

3
Probabilmente lo hanno raccomandato perché se la struttura è immutabile, non importa che abbia valore semantica piuttosto che riferimento semantica. La distinzione conta solo se muti l'oggetto / struttura dopo aver fatto una copia.
Stephen C. Steel,

@DrJokepu: In alcune situazioni, il sistema eseguirà una copia temporanea di una struttura e quindi consentirà che quella copia venga passata facendo riferimento al codice che la modifica; poiché la copia temporanea verrà eliminata, la modifica andrà persa. Questo problema è particolarmente grave se una struttura ha metodi che la mutano. Non sono assolutamente d'accordo con l'idea che la mutabilità sia una ragione per fare qualcosa di una classe, poiché - nonostante alcune carenze in c # e vb.net, le strutture mutabili forniscono una semantica utile che non può essere raggiunta in nessun altro modo; non c'è motivo semantico per preferire una struttura immutabile a una classe.
supercat

4
@Chuu: nel progettare il compilatore JIT, Microsoft ha deciso di ottimizzare il codice per la copia di strutture di dimensioni pari o inferiori a 16 byte; ciò significa che la copia di una struttura a 17 byte può essere significativamente più lenta della copia di una struttura a 16 byte. Non vedo alcun motivo particolare per aspettarsi che Microsoft estenda tali ottimizzazioni a strutture più grandi, ma è importante notare che mentre le strutture a 17 byte possono essere più lente da copiare rispetto alle strutture a 16 byte, ci sono molti casi in cui le grandi strutture possono essere più efficienti di oggetti di grande classe e dove il vantaggio relativo delle strutture cresce con la dimensione della struttura.
supercat

3
@Chuu: applicare gli stessi schemi di utilizzo a grandi strutture come si farebbe per le classi è suscettibile di generare codice inefficiente, ma la soluzione corretta spesso non sostituisce le strutture con le classi, ma invece utilizza le strutture in modo più efficiente; in particolare, si dovrebbe evitare di passare o restituire strutture per valore. Passali come refparametri ogni volta che è ragionevole farlo. Passare una struttura con 4.000 campi come parametro ref a un metodo che ne modifica uno sarà più economico rispetto al passaggio di una struttura con 4 campi per valore a un metodo che restituisce la versione modificata.
supercat

53

Sono sorpreso di non aver letto nessuna delle risposte precedenti, che considero l'aspetto più cruciale:

Uso le strutture quando voglio un tipo senza identità. Ad esempio un punto 3D:

public struct ThreeDimensionalPoint
{
    public readonly int X, Y, Z;
    public ThreeDimensionalPoint(int x, int y, int z)
    {
        this.X = x;
        this.Y = y;
        this.Z = z;
    }

    public override string ToString()
    {
        return "(X=" + this.X + ", Y=" + this.Y + ", Z=" + this.Z + ")";
    }

    public override int GetHashCode()
    {
        return (this.X + 2) ^ (this.Y + 2) ^ (this.Z + 2);
    }

    public override bool Equals(object obj)
    {
        if (!(obj is ThreeDimensionalPoint))
            return false;
        ThreeDimensionalPoint other = (ThreeDimensionalPoint)obj;
        return this == other;
    }

    public static bool operator ==(ThreeDimensionalPoint p1, ThreeDimensionalPoint p2)
    {
        return p1.X == p2.X && p1.Y == p2.Y && p1.Z == p2.Z;
    }

    public static bool operator !=(ThreeDimensionalPoint p1, ThreeDimensionalPoint p2)
    {
        return !(p1 == p2);
    }
}

Se hai due istanze di questa struttura, non ti importa se sono un singolo pezzo di dati in memoria o due. Ti preoccupi solo dei valori che detengono.


4
Un po 'fuori tema, ma perché lanci ArgumentException quando obj non è ThreeDimensionalPoint? Non dovresti semplicemente restituire false in quel caso?
Svish,

4
Esatto, ero troppo desideroso. return falseè quello che avrebbe dovuto essere lì, correggendo ora.
Andrei Rînea,

Un motivo interessante per usare una struttura. Ho creato classi con GetHashCode ed Equals definite simili a ciò che mostri qui, ma poi ho sempre dovuto fare attenzione a non mutare quelle istanze se le usassi come chiavi del dizionario. Probabilmente sarebbe stato più sicuro se li avessi definiti come strutture. (Perché allora la chiave sarebbe una copia dei campi nel momento in cui la struttura diventasse una chiave del dizionario , quindi la chiave rimarrebbe invariata se in seguito cambiassi l'originale.)
ToolmakerSteve

Nel tuo esempio va bene perché hai solo 12 byte ma tieni presente che se hai molti campi in quella struttura che superano 16 byte, devi considerare di usare una classe e sovrascrivere i metodi GetHashCode e Equals.
Daniel Botero Correa,

Un tipo di valore in DDD non significa che dovresti necessariamente utilizzare un tipo di valore in C #
Darragh

26

Bill Wagner ha un capitolo su questo nel suo libro "efficace c #" ( http://www.amazon.com/Effective-Specific-Ways-Improve-Your/dp/0321245660 ). Conclude usando il seguente principio:

  1. La responsabilità principale del tipo è l'archiviazione dei dati?
  2. La sua interfaccia pubblica è definita interamente da proprietà che accedono o modificano i suoi membri di dati?
  3. Sei sicuro che il tuo tipo non avrà mai sottoclassi?
  4. Sei sicuro che il tuo tipo non verrà mai trattato polimorficamente?

Se rispondi "sì" a tutte e 4 le domande: usa una struttura. Altrimenti, usa una classe.


1
quindi ... gli oggetti di trasferimento dati (DTO) dovrebbero essere strutture?
Incrociatore

1
Direi di sì, se soddisfa i 4 criteri sopra descritti. Perché un oggetto di trasferimento dati dovrebbe essere trattato in un modo specifico?
Bart Gijssens,

2
@cruizer dipende dalla tua situazione. Su un progetto, avevamo nei nostri DTO campi di audit comuni e quindi abbiamo scritto un DTO di base da cui altri hanno ereditato.
Andrew Grothe,

1
Tutti tranne (2) sembrano principi eccellenti. Dovrebbe vedere il suo ragionamento per sapere esattamente cosa intende per (2) e perché.
ToolmakerSteve

1
@ToolmakerSteve: per questo dovrai leggere il libro. Non pensare che sia giusto copiare / incollare grandi parti di un libro.
Bart Gijssens,


12

Vorrei usare le strutture quando:

  1. si suppone che un oggetto sia di sola lettura (ogni volta che si passa / si assegna una struttura viene copiato). Gli oggetti di sola lettura sono ottimi quando si tratta di elaborazione multithread in quanto non richiedono il blocco nella maggior parte dei casi.

  2. un oggetto è piccolo e di breve durata. In tal caso, esiste una buona probabilità che l'oggetto venga allocato nello stack, il che è molto più efficiente rispetto all'archiviazione dell'heap gestito. Inoltre, la memoria allocata dall'oggetto verrà liberata non appena esce dal suo ambito. In altre parole è meno lavoro per Garbage Collector e la memoria viene utilizzata in modo più efficiente.


10

Utilizzare una classe se:

  • La sua identità è importante. Le strutture vengono copiate implicitamente quando vengono passate per valore in un metodo.
  • Avrà un ingombro di memoria elevato.
  • I suoi campi hanno bisogno di inizializzatori.
  • Devi ereditare da una classe base.
  • Hai bisogno di un comportamento polimorfico;

Utilizzare una struttura se:

  • Funzionerà come un tipo primitivo (int, long, byte, ecc.).
  • Deve avere un footprint di memoria ridotto.
  • Stai chiamando un metodo P / Invoke che richiede il passaggio di una struttura per valore.
  • È necessario ridurre l'impatto della garbage collection sulle prestazioni dell'applicazione.
  • I suoi campi devono essere inizializzati solo ai loro valori predefiniti. Questo valore sarebbe zero per i tipi numerici, false per i tipi booleani e null per i tipi di riferimento.
    • Si noti che in C # 6.0 le strutture possono avere un costruttore predefinito che può essere usato per inizializzare i campi della struttura a valori non predefiniti.
  • Non è necessario ereditare da una classe base (diversa da ValueType, da cui ereditano tutte le strutture).
  • Non è necessario un comportamento polimorfico.

5

Ho sempre usato uno struct quando volevo raggruppare alcuni valori per passare le cose da una chiamata di metodo, ma non avrò bisogno di usarlo per nulla dopo aver letto quei valori. Proprio come un modo per mantenere le cose pulite. Tendo a vedere le cose in una struttura come "usa e getta" e le cose in una classe come più utili e "funzionali"


4

Se un'entità sarà immutabile, la questione se usare una struttura o una classe sarà generalmente una prestazione piuttosto che una semantica. Su un sistema a 32/64 bit, i riferimenti di classe richiedono 4/8 byte per l'archiviazione, indipendentemente dalla quantità di informazioni nella classe; la copia di un riferimento di classe richiederà la copia di 4/8 byte. D'altra parte, ogni distintol'istanza di classe avrà 8/16 byte di overhead oltre alle informazioni in suo possesso e al costo della memoria dei riferimenti ad essa. Supponiamo che uno voglia un array di 500 entità, ognuna contenente quattro numeri interi a 32 bit. Se l'entità è un tipo di struttura, l'array richiederà 8.000 byte indipendentemente dal fatto che tutte e 500 le entità siano tutte identiche, tutte diverse o in qualche punto intermedio. Se l'entità è un tipo di classe, l'array di 500 riferimenti richiederà 4.000 byte. Se quei riferimenti puntano tutti a oggetti diversi, gli oggetti richiederebbero 24 byte ciascuno ciascuno (12.000 byte per tutti i 500), per un totale di 16.000 byte, il doppio del costo di archiviazione di un tipo di struttura. D'altra parte, del codice creato un'istanza di oggetto e quindi copiato un riferimento a tutti i 500 slot di array, il costo totale sarebbe di 24 byte per quell'istanza e 4, 000 per l'array: un totale di 4.024 byte. Un grande risparmio. Poche situazioni funzionerebbero così come l'ultima, ma in alcuni casi potrebbe essere possibile copiare alcuni riferimenti in sufficienti slot di array per rendere utile tale condivisione.

Se si suppone che l'entità sia mutabile, la questione se utilizzare una classe o una struttura è in qualche modo più semplice. Supponiamo che "Thing" sia una struttura o una classe che ha un campo intero chiamato x, e si fa il seguente codice:

  Cosa t1, t2;
  ...
  t2 = t1;
  t2.x = 5;

Si vuole che quest'ultima affermazione influisca su t1.x?

Se Thing è un tipo di classe, t1 e t2 saranno equivalenti, nel senso che anche t1.xe t2.x saranno equivalenti. Pertanto, la seconda istruzione influenzerà t1.x. Se Thing è un tipo di struttura, t1 e t2 saranno istanze diverse, il che significa t1.xe t2.x si riferiranno a numeri interi diversi. Pertanto, la seconda istruzione non influirà su t1.x.

Le strutture mutabili e le classi mutabili hanno comportamenti sostanzialmente diversi, sebbene .net abbia alcune stranezze nella gestione delle mutazioni di struttura. Se si desidera un comportamento di tipo valore (nel senso che "t2 = t1" copierà i dati da t1 a t2 lasciando t1 e t2 come istanze distinte), e se si può vivere con le stranezze nella gestione dei tipi di valore di .net, usare una struttura. Se si desidera una semantica di tipo valore, ma le stranezze di .net porterebbero a una semantica di tipo valore errata nella propria applicazione, utilizzare una classe e borbottare.


3

Inoltre le eccellenti risposte sopra:

Le strutture sono tipi di valore.

Non possono mai essere impostati su Nothing .

Impostando una struttura = Nothing, imposterai tutti i tipi di valori sui valori predefiniti.


2

quando non hai davvero bisogno di comportamento, ma hai bisogno di più struttura di un semplice array o dizionario.

Follow up Ecco come penso alle strutture in generale. So che possono avere metodi, ma mi piace mantenere quella distinzione mentale generale.


Perché dici questo? Le strutture possono avere metodi.
Esteban Araya,

2

Come ha detto @Simon, le strutture forniscono una semantica di tipo "valore", quindi se hai bisogno di un comportamento simile a un tipo di dati incorporato, usa una struttura. Poiché le strutture vengono passate per copia, si desidera assicurarsi che siano di piccole dimensioni, circa 16 byte.


2

È un argomento vecchio, ma voleva fornire un semplice test di riferimento.

Ho creato due file .cs:

public class TestClass
{
    public long ID { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

e

public struct TestStruct
{
    public long ID { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

Esegui benchmark:

  • Crea 1 TestClass
  • Crea 1 TestStruct
  • Crea 100 TestClass
  • Crea 100 TestStruct
  • Crea 10000 TestClass
  • Crea 10000 TestStruct

risultati:

BenchmarkDotNet=v0.12.0, OS=Windows 10.0.18362
Intel Core i5-8250U CPU 1.60GHz (Kaby Lake R), 1 CPU, 8 logical and 4 physical cores
.NET Core SDK=3.1.101
[Host]     : .NET Core 3.1.1 (CoreCLR 4.700.19.60701, CoreFX 4.700.19.60801), X64 RyuJIT  [AttachedDebugger]
DefaultJob : .NET Core 3.1.1 (CoreCLR 4.700.19.60701, CoreFX 4.700.19.60801), X64 RyuJIT


|         Method |           Mean |         Error |        StdDev |     Ratio | RatioSD | Rank |    Gen 0 | Gen 1 | Gen 2 | Allocated |
|--------------- |---------------:|--------------:|--------------:|----------:|--------:|-----:|---------:|------:|------:|----------:|

|      UseStruct |      0.0000 ns |     0.0000 ns |     0.0000 ns |     0.000 |    0.00 |    1 |        - |     - |     - |         - |
|       UseClass |      8.1425 ns |     0.1873 ns |     0.1839 ns |     1.000 |    0.00 |    2 |   0.0127 |     - |     - |      40 B |
|   Use100Struct |     36.9359 ns |     0.4026 ns |     0.3569 ns |     4.548 |    0.12 |    3 |        - |     - |     - |         - |
|    Use100Class |    759.3495 ns |    14.8029 ns |    17.0471 ns |    93.144 |    3.24 |    4 |   1.2751 |     - |     - |    4000 B |
| Use10000Struct |  3,002.1976 ns |    25.4853 ns |    22.5920 ns |   369.664 |    8.91 |    5 |        - |     - |     - |         - |
|  Use10000Class | 76,529.2751 ns | 1,570.9425 ns | 2,667.5795 ns | 9,440.182 |  346.76 |    6 | 127.4414 |     - |     - |  400000 B |

1

Hmm ...

Non userei la garbage collection come argomento a favore / contro l'uso di struct vs class. L'heap gestito funziona in modo simile a uno stack: la creazione di un oggetto lo posiziona in cima all'heap, che è quasi altrettanto veloce dell'allocazione sullo stack. Inoltre, se un oggetto è di breve durata e non sopravvive a un ciclo GC, la deallocazione è gratuita in quanto il GC funziona solo con memoria ancora accessibile. (Cerca MSDN, c'è una serie di articoli sulla gestione della memoria .NET, sono troppo pigro per andare a cercarli).

La maggior parte delle volte uso una struttura, finisco per prendermi a calci per farlo, perché in seguito scopro che avere una semantica di riferimento avrebbe reso le cose un po 'più semplici.

Ad ogni modo, quei quattro punti nell'articolo MSDN pubblicato sopra sembrano una buona linea guida.


1
Se a volte hai bisogno di una semantica di riferimento con una struttura, devi semplicemente dichiarare class MutableHolder<T> { public T Value; MutableHolder(T value) {Value = value;} }, e quindi un MutableHolder<T>sarà un oggetto con una semantica di classe mutabile (funziona altrettanto bene se Tè una struttura o un tipo di classe immutabile).
supercat

1

Le strutture sono sullo Stack, non sull'Heap, quindi sono thread-safe e dovrebbero essere utilizzate durante l'implementazione del modello di oggetti di trasferimento, non si desidera mai usare oggetti sull'Heap ma sono volatili, in questo caso si desidera utilizzare lo Stack di chiamate, questo è un caso di base per l'utilizzo di una struttura che mi sorprende da tutte le risposte di uscita qui,


-3

Penso che la risposta migliore sia semplicemente usare struct quando ciò di cui hai bisogno è una raccolta di proprietà, classe quando è una raccolta di proprietà E comportamenti.


anche le strutture possono avere metodi
Nithin Chandran,

certo, ma se hai bisogno di metodi le probabilità sono del 99% stai usando impropriamente struct invece della classe. Le uniche eccezioni che ho riscontrato quando è giusto avere metodi nella struttura sono i callback
Lucian Gabriel Popescu,
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.