Perché le strutture e le classi hanno concetti separati in C #?


44

Durante la programmazione in C #, mi sono imbattuto in una strana decisione di progettazione del linguaggio che non riesco proprio a capire.

Quindi, C # (e CLR) ha due tipi di dati aggregati: struct(tipo-valore, archiviato nello stack, nessuna eredità) e class(tipo di riferimento, memorizzato nell'heap, ha ereditarietà).

All'inizio questa configurazione suona bene, ma poi ti imbatti in un metodo che prende un parametro di un tipo aggregato e per capire se è effettivamente di un tipo di valore o di un tipo di riferimento, devi trovare la dichiarazione del suo tipo. A volte può diventare davvero confuso.

La soluzione generalmente accettata al problema sembra dichiarare tutti struct"immutabili" (impostando i loro campi su readonly) per prevenire possibili errori, limitando l struct'utilità.

Il C ++, ad esempio, utilizza un modello molto più utilizzabile: consente di creare un'istanza di oggetto nello stack o nell'heap e passarla per valore o per riferimento (o per puntatore). Continuo a sentire che C # è stato ispirato da C ++ e non riesco proprio a capire perché non abbia preso questa tecnica. Combinare classe structin un costrutto con due diverse opzioni di allocazione (heap e stack) e passarle come valori o (esplicitamente) come riferimenti tramite le parole chiave refe outsembra una cosa carina.

La domanda è: perché classe structdiventano concetti distinti in C # e il CLR, invece di un tipo di aggregazione con due opzioni di allocazione?


34
"La soluzione generalmente accettata al problema sembra dichiarare tutte le strutture" immutabili "... limitando l'utilità delle strutture" Molte persone sostengono che rendere qualsiasi cosa immutabile in generale lo rende più utile ogni volta che non è la causa di un collo di bottiglia delle prestazioni. Inoltre, structi messaggi di posta elettronica non sono sempre memorizzati nello stack; considera un oggetto con un structcampo. A parte questo, come menzionato da Mason Wheeler, il problema del taglio è probabilmente il motivo principale.
Doval,

7
Non è vero che C # sia stato ispirato da C ++; piuttosto C # è stato ispirato da tutti gli errori (ben intenzionati e di buon suono in quel momento) nella progettazione di C ++ e Java.
Pieter Geerkens,

18
Nota: stack e heap sono dettagli di implementazione. Non c'è nulla che dica che le istanze di struct devono essere allocate nello stack e che le istanze di classe debbano essere allocate nell'heap. Ed in realtà non è nemmeno vero. Ad esempio, è molto probabile che un compilatore possa determinare usando Escape Analysis che una variabile non può sfuggire all'ambito locale e quindi allocarla nello stack anche se si tratta di un'istanza di classe. Non dice nemmeno che ci deve essere uno stack o un heap. È possibile allocare frame di ambiente come un elenco collegato sull'heap e non avere nemmeno uno stack.
Jörg W Mittag,


3
"ma poi ti imbatti in un metodo che prende un parametro di un tipo aggregato e per capire se è effettivamente di un tipo di valore o di un tipo di riferimento, devi trovare la dichiarazione del suo tipo" Umm, perché è importante, esattamente ? Il metodo ti chiede di passare un valore. Si passa un valore. A che punto devi preoccuparti se si tratta di un tipo di riferimento o di un valore?
Luaan,

Risposte:


58

La ragione per cui C # (e Java ed essenzialmente ogni altro linguaggio OO sviluppato dopo C ++) non ha copiato il modello di C ++ in questo aspetto è perché il modo in cui C ++ lo fa è un casino orrendo.

Hai identificato correttamente i punti rilevanti sopra:: structtipo di valore, nessuna eredità. class: tipo di riferimento, ha ereditarietà. I tipi di ereditarietà e di valore (o più specificamente, polimorfismo e pass-by-value) non si mescolano; se si passa un oggetto di tipo Deriveda un argomento di metodo di tipo Basee quindi si chiama un metodo virtuale su di esso, l'unico modo per ottenere un comportamento corretto è assicurarsi che ciò che è stato passato sia un riferimento.

Tra questo e tutti gli altri casini in cui ti imbatti in C ++ avendo oggetti ereditabili come tipi di valore (ti vengono in mente costruttori di copie e divisione di oggetti !) La soluzione migliore è solo dire No.

Una buona progettazione del linguaggio non è solo l'implementazione di funzionalità, ma anche la conoscenza di quali funzionalità non implementare e uno dei modi migliori per farlo è quello di imparare dagli errori di coloro che ti hanno preceduto.


29
Questo è solo un altro inutile rant soggettivo sul C ++. Non posso sottovalutare, ma lo farei se potessi.
Bartek Banachewicz,

17
@MasonWheeler: " è un casino orrendo " sembra abbastanza soggettivo. Questo è già stato discusso in un lungo thread di commenti a un'altra tua risposta ; il thread è stato rovinato (purtroppo perché conteneva commenti utili anche se in salsa di guerra di fiamma). Non penso che valga la pena ripetere tutto qui, ma "C # ha capito bene e C ++ ha sbagliato" (che sembra essere il messaggio che stai cercando di trasmettere) è davvero un'affermazione soggettiva.
Andy Prowl,

15
@MasonWheeler: l'ho fatto nel thread che è stato rovinato, e così anche molte altre persone - motivo per cui penso sia un peccato che sia stato cancellato. Non credo sia una buona idea replicare quel thread qui, ma la versione breve è: in C ++ l' utente del tipo, non il suo progettista , decide con quale semantica un tipo dovrebbe essere usato (riferimento semantica o valore semantica). Questo ha vantaggi e svantaggi: ti arrabbi contro i contro senza considerare (o sapere?) I pro. Ecco perché l'analisi è soggettiva.
Andy Prowl,

7
sospiro Credo che la discussione sulla violazione di LSP sia già avvenuta. E credo che la maggior parte delle persone sia d'accordo sul fatto che la menzione di LSP sia piuttosto strana e non correlata, ma non posso controllare perché una mod ha rovinato il thread dei commenti .
Bartek Banachewicz,

9
Se sposti l'ultimo paragrafo in alto e cancelli l'attuale primo paragrafo, penso che tu abbia l'argomento perfetto. Ma l'attuale primo paragrafo è solo soggettivo.
Martin York,

19

Per analogia, C # è fondamentalmente come un set di strumenti per meccanici in cui qualcuno ha letto che dovresti generalmente evitare pinze e chiavi regolabili, quindi non include affatto chiavi regolabili e le pinze sono bloccate in un apposito cassetto contrassegnato "non sicuro" e può essere utilizzato solo con l'approvazione di un supervisore, dopo aver firmato un disclaimer che assolve il datore di lavoro da qualsiasi responsabilità per la propria salute.

Il C ++, al confronto, include non solo chiavi e pinze regolabili, ma alcuni strumenti piuttosto speciali a sfera speciale il cui scopo non è immediatamente evidente e se non conosci il modo giusto di tenerli, potrebbero facilmente tagliare il tuo pollice (ma una volta che hai capito come usarli, puoi fare cose che sono essenzialmente impossibili con gli strumenti di base nella casella degli strumenti di C #). Inoltre, ha un tornio, una fresatrice, una smerigliatrice di superficie, una sega a nastro per il taglio di metalli, ecc., Per consentirti di progettare e creare strumenti completamente nuovi ogni volta che ne senti la necessità (ma sì, quegli strumenti del macchinista possono e causeranno lesioni gravi se non sai cosa stai facendo con loro - o anche solo se ti trascuri).

Ciò riflette la differenza fondamentale nella filosofia: il C ++ tenta di fornirti tutti gli strumenti di cui potresti aver bisogno essenzialmente per qualsiasi progetto tu voglia. Non fa quasi alcun tentativo di controllare il modo in cui usi questi strumenti, quindi è anche facile usarli per produrre progetti che funzionano bene solo in situazioni rare, così come progetti che sono probabilmente solo un'idea schifosa e nessuno sa di una situazione in cui probabilmente funzioneranno bene. In particolare, gran parte di questo viene fatto disaccoppiando le decisioni di progettazione - anche quelle che in pratica sono quasi sempre accoppiate. Di conseguenza, c'è una grande differenza tra scrivere semplicemente C ++ e scrivere bene C ++. Per scrivere bene C ++, devi conoscere molti modi di dire e regole empiriche (comprese le regole empiriche su quanto seriamente riconsiderare prima di infrangere altre regole empiriche). Di conseguenza, Il C ++ è molto più orientato alla facilità d'uso (da parte degli esperti) che alla facilità di apprendimento. Ci sono anche (troppe) circostanze in cui non è nemmeno terribilmente facile da usare.

C # fa molto di più per cercare di forzare (o almeno suggerire in modo estremamente forte) ciò che i progettisti del linguaggio hanno considerato buone pratiche di progettazione. Molte cose che sono disaccoppiate in C ++ (ma di solito vanno insieme in pratica) sono direttamente accoppiate in C #. Permette al codice "non sicuro" di spingere un po 'i confini, ma onestamente, non molto.

Il risultato è che da un lato ci sono alcuni progetti che possono essere espressi in modo abbastanza diretto in C ++ che sono sostanzialmente più goffi da esprimere in C #. D'altra parte, si tratta di un intero molto più facile da imparare il C #, e le possibilità di produzione di un design davvero orribile che non funzionerà per la vostra situazione (o probabilmente qualsiasi altro) sono drasticamente ridotti. In molti (probabilmente anche nella maggior parte dei casi), è possibile ottenere un design solido e realizzabile semplicemente "seguendo il flusso", per così dire. Oppure, come uno dei miei amici (almeno mi piace pensare a lui come un amico - non sono sicuro che sia davvero d'accordo) piace metterlo, C # rende facile cadere nella fossa del successo.

Quindi, osservando più specificamente la domanda su come classe structottenuto come sono nelle due lingue: oggetti creati in una gerarchia di ereditarietà in cui potresti usare un oggetto di una classe derivata nelle vesti della sua classe / interfaccia di base, sei praticamente bloccato dal fatto che normalmente è necessario farlo tramite una sorta di puntatore o riferimento - a livello concreto, ciò che accade è che l'oggetto della classe derivata contiene qualcosa di memoria che può essere trattato come un'istanza della classe base / interfaccia e l'oggetto derivato viene manipolato tramite l'indirizzo di quella parte della memoria.

In C ++, spetta al programmatore farlo correttamente - quando usa l'ereditarietà, spetta a lui assicurarsi che (ad esempio) una funzione che funzioni con le classi polimorfiche in una gerarchia lo faccia tramite un puntatore o un riferimento alla base classe.

In C #, ciò che è fondamentalmente la stessa separazione tra i tipi è molto più esplicito e applicato dal linguaggio stesso. Il programmatore non ha bisogno di fare alcun passo per passare un'istanza di una classe per riferimento, perché ciò accadrà di default.


2
Come fan del C ++, penso che questo sia un eccellente riassunto delle differenze tra C # e Swiss Army Chainsaw.
David Thornley,

1
@DavidThornley: ho almeno tentato di scrivere quello che pensavo sarebbe stato un paragone in qualche modo equilibrato. Non puntare le dita, ma parte di ciò che ho visto quando l'ho scritto mi ha colpito come ... in qualche modo impreciso (per dirla bene).
Jerry Coffin,

7

Questo è tratto da "C #: perché abbiamo bisogno di un'altra lingua?" - Gunnerson, Eric:

La semplicità era un importante obiettivo di progettazione per C #.

È possibile esagerare con la semplicità e la purezza del linguaggio, ma la purezza per il bene della purezza è di scarsa utilità per il programmatore professionista. Abbiamo quindi cercato di bilanciare il nostro desiderio di avere un linguaggio semplice e conciso con la soluzione dei problemi del mondo reale che i programmatori devono affrontare.

[...]

I tipi di valore , il sovraccarico dell'operatore e le conversioni definite dall'utente aggiungono complessità al linguaggio , ma consentono di semplificare enormemente uno scenario utente importante.

La semantica di riferimento per gli oggetti è un modo per evitare molti problemi (ovviamente e non solo la suddivisione degli oggetti) ma a volte i problemi del mondo reale possono richiedere oggetti con valore semantico (ad esempio dare un'occhiata a Suoni come se non dovessi mai usare la semantica di riferimento, giusto? per un diverso punto di vista).

Quale approccio migliore da adottare, quindi, che segregare quegli oggetti sporchi, brutti e cattivi con valore semantico sotto l'etichetta di struct?


1
Non lo so, forse non usi quegli oggetti sporchi, brutti e cattivi con la semantica di riferimento?
Bartek Banachewicz,

Forse ... sono una causa persa.
manlio,

2
IMHO, uno dei maggiori difetti nella progettazione di Java è la mancanza di qualsiasi mezzo per dichiarare se una variabile viene utilizzata per incapsulare identità o proprietà e uno dei maggiori difetti in C # è la mancanza di un mezzo per distinguere le operazioni su una variabile dalle operazioni su un oggetto a cui una variabile contiene un riferimento. Anche se il runtime non si preoccupasse di tali distinzioni, essere in grado di specificare in una lingua se una variabile di tipo int[]dovrebbe essere condivisibile o modificabile (gli array possono essere entrambi, ma generalmente non entrambi) contribuirebbe a far apparire sbagliato il codice sbagliato.
supercat,

4

Anziché pensare ai tipi di valore derivanti Object, sarebbe più utile pensare ai tipi di posizione di archiviazione esistenti in un universo completamente separato dai tipi di istanza di classe, ma per ogni tipo di valore avere un tipo di oggetto heap corrispondente. Una posizione di archiviazione del tipo di struttura contiene semplicemente una concatenazione dei campi pubblici e privati ​​del tipo e il tipo di heap viene generato automaticamente secondo uno schema come:

// Defined structure
struct Point : IEquatable<Point>
{
  public int X,Y;
  public Point(int x, int y) { X=x; Y=y; }
  public bool Equals(Point other) { return X==other.X && y==other.Y; }
  public bool Equals(Object other)
  { return other != null && other.GetType()==typeof(this) && Equals(Point(other)); }
  public bool ToString() { return String.Format("[{0},{1}", x, y); }
  public bool GetHashCode() { return unchecked(x+y*65531); }
}        
// Auto-generated class
class boxed_Point: IEquatable<Point>
{
  public Point value; // Fake name; C++/CLI, though not C#, allow full access
  public boxed_Point(Point v) { value=v; }
  // Members chain to each member of the original
  public bool Equals(Point other) { return value.Equals(other); }
  public bool Equals(Object other) { return value.Equals(other); }
  public String ToString() { return value.ToString(); }
  public Int32 GetHashCode() { return value.GetHashCode(); }
}

e per un'istruzione come: Console.WriteLine ("Il valore è {0}", somePoint);

da tradurre come: boxed_Point box1 = new boxed_Point (somePoint); Console.WriteLine ("Il valore è {0}", box1);

In pratica, poiché i tipi di posizione di archiviazione e i tipi di istanza heap esistono in universi separati, non è necessario chiamare i tipi di istanza heap come cose simili boxed_Int32; poiché il sistema saprebbe quali contesti richiedono l'istanza di heap-object e quali richiedono un percorso di archiviazione.

Alcune persone pensano che qualsiasi tipo di valore che non si comporti come un oggetto debba essere considerato "malvagio". Sono dell'opinione opposta: poiché le posizioni di archiviazione dei tipi di valore non sono né oggetti né riferimenti ad oggetti, l'aspettativa che debbano comportarsi come oggetti dovrebbe essere considerata inutile. Nei casi in cui una struttura può comportarsi utilmente come un oggetto, non c'è nulla di sbagliato nel farlo, ma ognuno structè nel suo cuore nient'altro che un'aggregazione di campi pubblici e privati ​​attaccati insieme al nastro adesivo.

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.