Perché le strutture non supportano l'ereditarietà?


128

So che le strutture in .NET non supportano l'ereditarietà, ma non è esattamente chiaro perché siano limitate in questo modo.

Quale motivo tecnico impedisce alle strutture di ereditare da altre strutture?


6
Non sto morendo per questa funzionalità, ma posso pensare ad alcuni casi in cui l'ereditarietà della struttura sarebbe utile: potresti voler estendere una struttura Point2D a una struttura Point3D con ereditarietà, potresti voler ereditare da Int32 per vincolarne i valori tra 1 e 100, potresti voler creare un type-def che sia visibile su più file (il trucco Using typeA = typeB ha solo un ambito di file), ecc.
Juliet

4
Potresti voler leggere stackoverflow.com/questions/1082311/… , che spiega un po 'di più sulle strutture e sul perché dovrebbero essere limitate a una certa dimensione. Se vuoi usare l'ereditarietà in una struttura, probabilmente dovresti usare una classe.
Justin,

1
E potresti voler leggere stackoverflow.com/questions/1222935/… in quanto spiega perché non è stato possibile farlo nella piattaforma dotNet. Hanno fatto freddo in modo C ++, con gli stessi problemi che possono essere disastrosi per una piattaforma gestita.
Dykam,

Le classi @Justin hanno costi di prestazione che le strutture possono evitare. E nello sviluppo di giochi che conta davvero. Quindi in alcuni casi non dovresti usare una classe se puoi aiutarla.
Gavin Williams,

@Dykam Penso che possa essere fatto in C #. Disastroso è un'esagerazione. Posso scrivere codice disastroso oggi in C # quando non ho familiarità con una tecnica. Quindi non è davvero un problema. Se l'ereditarietà della struttura può risolvere alcuni problemi e offrire prestazioni migliori in determinati scenari, allora sono tutto per questo.
Gavin Williams,

Risposte:


121

Il motivo per cui i tipi di valore non possono supportare l'ereditarietà è a causa di array.

Il problema è che, per motivi di prestazioni e GC, le matrici di tipi di valore vengono archiviate "in linea". Ad esempio, dato che new FooType[10] {...}, se FooTypeè un tipo di riferimento, verranno creati 11 oggetti sull'heap gestito (uno per l'array e 10 per ogni istanza del tipo). Se FooTypeinvece è un tipo di valore, verrà creata solo un'istanza sull'heap gestito - per l'array stesso (poiché ciascun valore dell'array verrà memorizzato "in linea" con l'array).

Supponiamo ora di avere eredità con tipi di valore. Se combinato con il comportamento di "archiviazione inline" sopra descritto, si verificano cose errate, come si può vedere in C ++ .

Considera questo codice pseudo-C #:

struct Base
{
    public int A;
}

struct Derived : Base
{
    public int B;
}

void Square(Base[] values)
{
  for (int i = 0; i < values.Length; ++i)
      values [i].A *= 2;
}

Derived[] v = new Derived[2];
Square (v);

In base alle normali regole di conversione, a Derived[]è convertibile in a Base[](nel bene o nel male), quindi se s / struct / class / g per l'esempio sopra, verrà compilato ed eseguito come previsto, senza problemi. Ma se Basee Derivedsono tipi di valore e gli array archiviano i valori in linea, allora abbiamo un problema.

Abbiamo un problema perché Square()non ne sappiamo nulla Derived, utilizzerà solo l'aritmetica del puntatore per accedere a ciascun elemento dell'array, incrementando di una quantità costante ( sizeof(A)). L'assemblea sarebbe vagamente simile a:

for (int i = 0; i < values.Length; ++i)
{
    A* value = (A*) (((char*) values) + i * sizeof(A));
    value->A *= 2;
}

(Sì, è un assemblaggio abominevole, ma il punto è che aumenteremo attraverso l'array in corrispondenza di costanti di tempo di compilazione note, senza sapere che viene utilizzato un tipo derivato.)

Quindi, se ciò accadesse effettivamente, avremmo problemi di corruzione della memoria. Nello specifico, all'interno Square(), values[1].A*=2sarebbe in realtà una modifica values[0].B!

Prova a eseguire il debug di QUESTO !


11
La soluzione ragionevole a questo problema sarebbe quella di non consentire il cast di Base [] a Detived []. Proprio come il casting da short [] a int [] è proibito, anche se è possibile eseguire il casting da short a int.
Niki,

3
+ risposta: il problema con l'ereditarietà non ha fatto clic con me finché non lo hai inserito in termini di array. Un altro utente ha affermato che questo problema potrebbe essere mitigato "tagliando" le strutture alla dimensione appropriata, ma vedo che il taglio è la causa di più problemi di quanti ne risolva.
Juliet,

4
Sì, ma "ha senso" perché le conversioni di array sono per conversioni implicite, non conversioni esplicite. short in int è possibile, ma richiede un cast, quindi è ragionevole che short [] non possa essere convertito in int [] (short del codice di conversione, come 'a.Select (x => (int) x) .ToArray ( ) '). Se il runtime non consentisse il cast da Base a Derivato, si tratterebbe di una "verruca", in quanto tale IS consentiva i tipi di riferimento. Quindi abbiamo due diverse "verruche" possibili: vietare l'ereditarietà della struttura o proibire le conversioni di matrici di derivazione in matrici di base.
Jon

3
Almeno prevenendo l'ereditarietà di struct abbiamo una parola chiave separata e possiamo più facilmente dire "le strutture sono speciali", al contrario di avere una limitazione "casuale" in qualcosa che funziona per un insieme di cose (classi) ma non per un altro (strutture) . Immagino che la limitazione della struttura sia molto più semplice da spiegare ("sono diversi!").
Jon

2
è necessario cambiare il nome della funzione da 'square' a 'double'
John

69

Immagina che le strutture supportino l'ereditarietà. Quindi dichiarando:

BaseStruct a;
InheritedStruct b; //inherits from BaseStruct, added fields, etc.

a = b; //?? expand size during assignment?

significherebbe che le variabili struct non hanno dimensioni fisse ed è per questo che abbiamo tipi di riferimento.

Ancora meglio, considera questo:

BaseStruct[] baseArray = new BaseStruct[1000];

baseArray[500] = new InheritedStruct(); //?? morph/resize the array?

5
Il C ++ ha risposto a ciò introducendo il concetto di "slicing", quindi è un problema risolvibile. Quindi, perché non dovrebbe essere supportata la strutturazione dell'eredità?
Jon

1
Considera le matrici di strutture ereditabili e ricorda che C # è un linguaggio gestito (di memoria). Affettare o qualsiasi altra opzione simile causerebbe il caos sui fondamenti del CLR.
Kenan EK,

4
@jonp: risolvibile, sì. Auspicabile? Ecco un esperimento mentale: immagina di avere una classe base Vector2D (x, y) e una classe derivata Vector3D (x, y, z). Entrambe le classi hanno una proprietà Magnitude che calcola rispettivamente sqrt (x ^ 2 + y ^ 2) e sqrt (x ^ 2 + y ^ 2 + z ^ 2). Se scrivi 'Vector3D a = Vector3D (5, 10, 15); Vector2D b = a; ', cosa dovrebbe restituire' a.Magnitude == b.Magnitude '? Se poi scriviamo 'a = (Vector3D) b', a.Magnitude ha lo stesso valore prima dell'assegnazione come fa dopo? I progettisti di .NET probabilmente si sono detti "no, non avremo nulla di tutto ciò".
Juliet,

1
Solo perché un problema può essere risolto, non significa che dovrebbe essere risolto. A volte è meglio evitare situazioni in cui si presenta il problema.
Dan Diplo,

@ kek444: Avere l' Fooereditarietà di una struttura Barnon dovrebbe consentire l' Fooassegnazione di un a a Bar, ma dichiarare una struttura in questo modo potrebbe consentire un paio di effetti utili: (1) Creare un membro di tipo con nome speciale Barcome primo elemento in Fooe Fooincludere i nomi dei membri che sono alias di quei membri Bar, permettendo al codice che era Barstato adattato di usare Fooinvece un , senza dover sostituire tutti i riferimenti thing.BarMembercon thing.theBar.BarMember, e mantenendo la capacità di leggere e scrivere tutti Bari campi come gruppo; ...
supercat

14

Le strutture non usano i riferimenti (a meno che non siano inscatolati, ma dovresti cercare di evitarlo), quindi il polimorfismo non è significativo poiché non esiste un riferimento indiretto tramite un puntatore di riferimento. Gli oggetti normalmente vivono nell'heap e sono referenziati tramite puntatori di riferimento, ma le strutture sono allocate nello stack (a meno che non siano inscatolate) o sono allocate "all'interno" della memoria occupata da un tipo di riferimento sull'heap.


8
non è necessario usare il polimorfismo per trarre vantaggio dall'eredità
rmeador,

Quindi, avresti quanti tipi diversi di ereditarietà in .NET?
John Saunders,

Il polimorfismo esiste nelle strutture, basta considerare la differenza tra chiamare ToString () quando lo si implementa su una struttura personalizzata o quando non esiste un'implementazione personalizzata di ToString ().
Kenan EK,

Questo perché derivano tutti da System.Object. È più il polimorfismo del tipo System.Object che delle strutture.
John Saunders,

Il polimorfismo potrebbe essere significativo con strutture usate come parametri di tipo generico. Il polimorfismo funziona con strutture che implementano interfacce; il problema più grande con le interfacce è che non possono esporre i byrefs ai campi della struttura. Altrimenti, la cosa più grande che penso sarebbe utile per quanto riguarda "ereditare" le strutture sarebbe un mezzo per avere un tipo (struct o class) Fooche ha un campo di tipo di struttura Barin grado di considerare Bari membri dei suoi come propri, in modo che una Point3dclasse potrebbe ad es. incapsulare a Point2d xyma fare riferimento a Xquel campo come xy.Xo X.
supercat

8

Ecco cosa dicono i documenti :

Le strutture sono particolarmente utili per le piccole strutture di dati con semantica di valore. Numeri complessi, punti in un sistema di coordinate o coppie chiave-valore in un dizionario sono tutti buoni esempi di strutture. La chiave di queste strutture di dati è che hanno pochi membri di dati, che non richiedono l'uso dell'ereditarietà o dell'identità referenziale e che possono essere comodamente implementati usando la semantica del valore in cui l'assegnazione copia il valore anziché il riferimento.

Fondamentalmente, dovrebbero contenere dati semplici e quindi non hanno "funzionalità extra" come l'ereditarietà. Probabilmente sarebbe tecnicamente possibile per loro supportare qualche tipo limitato di ereditarietà (non polimorfismo, a causa del fatto che sono in pila), ma credo che sia anche una scelta progettuale non supportare l'ereditarietà (come molte altre cose in .NET le lingue sono.)

D'altra parte, sono d'accordo con i benefici dell'eredità e penso che tutti abbiamo raggiunto il punto in cui vogliamo structche ereditiamo da un altro e ci rendiamo conto che non è possibile. Ma a quel punto, la struttura dei dati è probabilmente così avanzata che dovrebbe essere comunque una classe.


4
Non è questo il motivo per cui non esiste eredità.
Dykam,

Credo che l'eredità di cui stiamo parlando qui non sia in grado di usare due strutture in cui una eredita dall'altra in modo intercambiabile, ma riutilizzando e aggiungendo l'implementazione di una struttura all'altra (cioè creando un Point3Dda un Point2D; non si sarebbe in grado per usare un Point3Dinvece di un Point2D, ma non dovresti reimplementare il Point3Dtutto da zero. È così che l'ho interpretato comunque ...
Blixt

In breve: potrebbe supportare l'eredità senza polimorfismo. Non Credo che sia una scelta di progettazione per aiutare una persona sceglie classsopra structal momento opportuno.
Blixt

3

L'ereditarietà di classe come non è possibile, poiché una struttura viene posta direttamente nello stack. Una struttura ereditaria sarebbe più grande di quella genitore, ma la JIT non lo sa e cerca di dedicare troppo spazio a troppo poco. Sembra un po 'poco chiaro, scriviamo un esempio:

struct A {
    int property;
} // sizeof A == sizeof int

struct B : A {
    int childproperty;
} // sizeof B == sizeof int * 2

Se ciò fosse possibile, si arresterebbe in modo anomalo sul seguente frammento:

void DoSomething(A arg){};

...

B b;
DoSomething(b);

Lo spazio è assegnato per la dimensione di A, non per la dimensione di B.


2
C ++ gestisce bene questo caso, IIRC. L'istanza di B è suddivisa per adattarsi alle dimensioni di una A. Se si tratta di un tipo di dati puro, come lo sono le strutture .NET, non accadrà nulla di male. Si riscontra un piccolo problema con un metodo che restituisce una A e si memorizza quel valore restituito in una B, ma ciò non dovrebbe essere consentito. In breve, i progettisti .NET avrebbero potuto occuparsene se avessero voluto, ma per qualche motivo non lo hanno fatto.
dal

1
Per il tuo DoSomething (), non è probabile che ci sia un problema in quanto (supponendo che la semantica C ++) 'b' verrebbe "suddiviso" per creare un'istanza A. Il problema è con <i> array </i>. Considera le tue strutture A e B esistenti e un metodo <c> DoSomething (A [] arg) {arg [1] .property = 1;} </c>. Poiché le matrici di tipi di valore memorizzano i valori "inline", DoSomething (actual = new B [2] {}) causerà l'impostazione della proprietà [0] .child effettiva, non dell'attuale [1] .property. Questo non va bene.
Jon

2
@John: non stavo affermando che lo fosse, e non credo nemmeno @jonp. Stavamo semplicemente menzionando che questo problema è vecchio ed è stato risolto, quindi i progettisti di .NET hanno scelto di non supportarlo per qualche motivo diverso dall'inaccessibilità tecnica.
dal

Va notato che il problema "matrici di tipi derivati" non è nuovo in C ++; vedi parashift.com/c++-faq-lite/proper-inheritance.html#faq-21.4 (Le matrici in C ++ sono cattive! ;-)
jonp

@Giovanni: la soluzione al problema "matrici di tipi derivati ​​e tipi di base non si mescolano" è, come al solito, non farlo. Questo è il motivo per cui le matrici in C ++ sono dannose (consente più facilmente il danneggiamento della memoria) e perché .NET non supporta l'ereditarietà con tipi di valore (compilatore e JIT assicurano che ciò non accada).
Jon

3

C'è un punto che vorrei correggere. Anche se il motivo per cui le strutture non possono essere ereditate è perché vivono nello stack è quello giusto, è allo stesso tempo una spiegazione metà corretta. Le strutture, come qualsiasi altro tipo di valore, possono vivere nello stack. Poiché dipenderà da dove viene dichiarata la variabile, vivranno nello stack o nell'heap . Questo avverrà quando saranno rispettivamente variabili locali o campi di istanza.

Nel dire ciò, Cecil Has a Name l'ha inchiodato correttamente.

Vorrei sottolineare questo, i tipi di valore possono vivere nello stack. Questo non significa che lo facciano sempre. Le variabili locali, inclusi i parametri del metodo, lo faranno. Tutti gli altri no. Tuttavia, rimane ancora il motivo per cui non possono essere ereditati. :-)


3

Le strutture sono allocate in pila. Ciò significa che la semantica di valore è praticamente gratuita e l'accesso ai membri di struct è molto economico. Questo non impedisce il polimorfismo.

È possibile che ogni struttura inizi con un puntatore alla sua tabella delle funzioni virtuali. Questo sarebbe un problema di prestazioni (ogni struttura avrebbe almeno le dimensioni di un puntatore), ma è fattibile. Ciò consentirebbe funzioni virtuali.

Che dire dell'aggiunta di campi?

Bene, quando si assegna una struttura in pila, si assegna una certa quantità di spazio. Lo spazio richiesto viene determinato al momento della compilazione (sia in anticipo che durante JITting). Se aggiungi campi e poi assegni a un tipo di base:

struct A
{
    public int Integer1;
}

struct B : A
{
    public int Integer2;
}

A a = new B();

Ciò sovrascriverà alcune parti sconosciute dello stack.

L'alternativa è che il runtime lo impedisca scrivendo solo sizeof (A) byte su qualsiasi variabile A.

Cosa succede se B sovrascrive un metodo in A e fa riferimento al suo campo Intero2? Il runtime genera un MemberAccessException o il metodo accede invece ad alcuni dati casuali nello stack. Nessuno di questi è consentito.

È perfettamente sicuro avere l'ereditarietà della struttura, a condizione che non si utilizzino le strutture polimorficamente o finché non si aggiungono campi durante l'ereditarietà. Ma questi non sono tremendamente utili.


1
Quasi. Nessun altro ha menzionato il problema del taglio in riferimento alla pila, solo in riferimento alle matrici. E nessun altro ha menzionato le soluzioni disponibili.
user38001,

1
Tutti i tipi di valore in .net vengono riempiti per zero al momento della creazione, indipendentemente dal loro tipo o dai campi che contengono. L'aggiunta di qualcosa come un puntatore vtable a una struttura richiederebbe un mezzo per inizializzare i tipi con valori predefiniti diversi da zero. Una tale funzionalità potrebbe essere utile per una varietà di scopi, e implementare una cosa del genere nella maggior parte dei casi potrebbe non essere troppo difficile, ma non esiste nulla di simile in .net.
supercat

@ user38001 "Le strutture sono allocate nello stack" - a meno che non siano campi di istanza nel qual caso sono allocate nell'heap.
David Klempfner,

2

Questa sembra una domanda molto frequente. Mi sento di aggiungere che i tipi di valore sono memorizzati "sul posto" in cui si dichiara la variabile; a parte i dettagli dell'implementazione, ciò significa che non esiste un'intestazione dell'oggetto che dica qualcosa sull'oggetto, solo la variabile sa quale tipo di dati risiede lì.


Il compilatore sa cosa c'è. Riferendosi al C ++ questa non può essere la risposta.
Henk Holterman,

Da dove hai dedotto il C ++? Vorrei dire sul posto perché è quello che corrisponde meglio al comportamento, lo stack è un dettaglio di implementazione, per citare un articolo del blog MSDN.
Cecil ha un nome il

Sì, menzionare il C ++ era male, solo il mio treno di pensieri. Ma a parte la domanda se sono necessarie informazioni di runtime, perché le strutture non dovrebbero avere un '"intestazione oggetto"? Il compilatore può mescolarli in ogni modo. Potrebbe persino nascondere un'intestazione su una struttura [Structlayout].
Henk Holterman,

Poiché le strutture sono tipi di valore, non è necessario con un'intestazione di oggetto poiché il runtime copia sempre il contenuto come per altri tipi di valore (un vincolo). Non avrebbe senso con un'intestazione, perché è a questo che servono le classi del tipo di riferimento: P
Cecil ha un nome il

1

Le strutture supportano le interfacce, quindi puoi fare alcune cose polimorfiche in quel modo.


0

IL è un linguaggio basato su stack, quindi chiamare un metodo con un argomento va in questo modo:

  1. Spingi l'argomento sulla pila
  2. Chiama il metodo

Quando il metodo viene eseguito, espelle alcuni byte dallo stack per ottenere il suo argomento. Sa esattamente quanti byte espellere perché l'argomento è un puntatore del tipo di riferimento (sempre 4 byte su 32 bit) oppure è un tipo di valore per il quale la dimensione è sempre nota esattamente.

Se si tratta di un puntatore del tipo di riferimento, il metodo cerca l'oggetto nell'heap e ottiene il relativo handle di tipo, che punta a una tabella dei metodi che gestisce quel particolare metodo per quel tipo esatto. Se si tratta di un tipo di valore, non è necessaria alcuna ricerca in una tabella dei metodi poiché i tipi di valore non supportano l'ereditarietà, pertanto esiste solo una possibile combinazione metodo / tipo.

Se i tipi di valore supportano l'ereditarietà, allora ci sarebbe un sovraccarico in più in quanto il particolare tipo di struct dovrebbe essere posizionato nello stack e il suo valore, il che significherebbe una sorta di ricerca nella tabella dei metodi per la particolare istanza concreta del tipo. Ciò eliminerebbe i vantaggi di velocità ed efficienza dei tipi di valore.


1
C ++ che ha risolto, leggere questa risposta per il problema vero: stackoverflow.com/questions/1222935/...
Dykam
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.