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?
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?
Risposte:
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 FooType
invece è 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 Base
e Derived
sono 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*=2
sarebbe in realtà una modifica values[0].B
!
Prova a eseguire il debug di QUESTO !
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?
Foo
ereditarietà di una struttura Bar
non dovrebbe consentire l' Foo
assegnazione 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 Bar
come primo elemento in Foo
e Foo
includere i nomi dei membri che sono alias di quei membri Bar
, permettendo al codice che era Bar
stato adattato di usare Foo
invece un , senza dover sostituire tutti i riferimenti thing.BarMember
con thing.theBar.BarMember
, e mantenendo la capacità di leggere e scrivere tutti Bar
i campi come gruppo; ...
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.
Foo
che ha un campo di tipo di struttura Bar
in grado di considerare Bar
i membri dei suoi come propri, in modo che una Point3d
classe potrebbe ad es. incapsulare a Point2d xy
ma fare riferimento a X
quel campo come xy.X
o X
.
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 struct
che 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.
Point3D
da un Point2D
; non si sarebbe in grado per usare un Point3D
invece di un Point2D
, ma non dovresti reimplementare il Point3D
tutto da zero. È così che l'ho interpretato comunque ...
class
sopra struct
al momento opportuno.
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.
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. :-)
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.
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ì.
Le strutture supportano le interfacce, quindi puoi fare alcune cose polimorfiche in quel modo.
IL è un linguaggio basato su stack, quindi chiamare un metodo con un argomento va in questo modo:
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.