Perché le struct e le classi separano i concetti in C #?

44

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

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

Questa configurazione suona bene all'inizio, ma poi incappi in un metodo che assume 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 per il problema sembra dichiarare tutto struct s come "immutabile" (impostando i loro campi su readonly ) per evitare possibili errori, limitando l'utilità di struct s.

C ++, ad esempio, utilizza un modello molto più utilizzabile: consente di creare un'istanza di oggetto nello stack o sull'heap e passarlo per valore o per riferimento (o per puntatore). Continuo a sentire che C # è stato ispirato dal C ++, e non riesco proprio a capire perché non ha assunto questa tecnica. Combinare class e struct in un costrutto con due diverse opzioni di allocazione (heap e stack) e passandoli come valori o (esplicitamente) come riferimenti tramite le parole chiave ref e out sembra una cosa carina.

La domanda è: perché class e struct diventano concetti separati in C # e CLR invece di un tipo aggregato con due opzioni di allocazione?

    
posta Mints97 26.02.2015 - 21:39
fonte

4 risposte

58

Il motivo per cui C # (e Java e essenzialmente ogni altro linguaggio OO sviluppato dopo C ++) non copiava il modello di C ++ sotto questo aspetto è perché il modo in cui C ++ lo fa è un pasticcio orribile

Hai identificato correttamente i punti rilevanti sopra: struct : tipo di valore, nessuna ereditarietà. class : tipo di riferimento, ha ereditarietà. I tipi di ereditarietà e valore (o più specificamente, polimorfismo e pass-per-valore) non si mescolano; se si passa un oggetto di tipo Derived a un argomento del metodo di tipo Base e 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 problemi che si verificano in C ++ avendo oggetti ereditabili come tipi di valore (costruttori di copie e affettare gli oggetti venite in mente!) la soluzione migliore è Just Say No.

Il buon design del linguaggio non è solo l'implementazione delle funzionalità, ma anche la conoscenza delle funzionalità da non implementare e uno dei modi migliori per farlo è quello di imparare dagli errori di coloro che sono venuti prima di te.

    
risposta data 26.02.2015 - 21:49
fonte
19

Per analogia, C # è fondamentalmente come un insieme di strumenti meccanici in cui qualcuno ha letto che in genere si dovrebbero evitare pinze e chiavi regolabili, quindi non include affatto le chiavi regolabili e le pinze sono bloccate in uno speciale cassetto contrassegnato "non sicuro" e può essere utilizzato solo con l'approvazione di un supervisore, dopo aver firmato una dichiarazione di non responsabilità che ha assolto il datore di lavoro da qualsiasi responsabilità per la propria salute.

C ++, al confronto, non include solo le chiavi e le pinze regolabili, ma alcuni strumenti per scopi speciali a sfera dispari il cui scopo non è immediatamente evidente, e se non si conosce il modo giusto per tenerli, potrebbero facilmente taglia il pollice (ma una volta capito come usarli, puoi fare cose che sono essenzialmente impossibili con gli strumenti di base nella toolbox C #). Inoltre, ha un tornio, una fresatrice, una smerigliatrice di superficie, una sega a nastro per il taglio di metalli ecc., Per permetterti di progettare e creare strumenti completamente nuovi ogni volta che ne senti il bisogno (ma sì, gli strumenti di questi macchinisti possono e causeranno ferite gravi se non sai cosa stai facendo con loro - o anche se sei semplicemente incauto).

Ciò riflette la differenza fondamentale in filosofia: C ++ tenta di darti tutti gli strumenti di cui potresti avere bisogno per qualsiasi progetto tu voglia. Non fa quasi nessun tentativo di controllare come si usano questi strumenti, quindi è anche facile usarli per produrre disegni che funzionano bene solo in rare situazioni, così come progetti che sono probabilmente solo un'idea pessima 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'è un'enorme differenza tra la semplice scrittura di C ++ e la scrittura di C ++. Per scrivere bene in C ++, è necessario conoscere molti idiomi e regole empiriche (comprese le regole pratiche su come seriamente riconsiderare prima di infrangere altre regole empiriche). Di conseguenza, il C ++ è orientato molto più verso la facilità d'uso (da parte degli esperti) rispetto alla facilità di apprendimento. Ci sono anche (troppe) circostanze in cui non è nemmeno terribilmente facile da usare.

C # fa molto di più per provare a forzare (o almeno estremamente suggerisce strongmente) ciò che i progettisti di linguaggio hanno considerato buone pratiche di progettazione. Molte cose che sono disaccoppiate in C ++ (ma di solito vanno insieme nella pratica) sono direttamente accoppiate in C #. Permette al codice "non sicuro" di spingere un po 'i limiti, 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, è un lotto tutto più facile da imparare in C #, e le possibilità di produrre un disegno davvero orribile che non funzioni per la tua situazione (o probabilmente altre) sono drasticamente ridotto. In molti casi (probabilmente anche nella maggior parte dei casi), è possibile ottenere un design solido e funzionale semplicemente "andando con il flusso", per così dire. Oppure, come uno dei miei amici (almeno mi piace pensare a lui come ad un amico - non sono sicuro che sia d'accordo) a lui piace, C # rende facile cadere nella fossa del successo.

Quindi, esaminando in modo più specifico la questione di come class e struct hanno ottenuto come sono nei due linguaggi: oggetti creati in una gerarchia ereditaria in cui si potrebbe usare un oggetto di una classe derivata sotto le spoglie della sua base classe / interfaccia, sei praticamente bloccato dal fatto che normalmente devi 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 di memoria.

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

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 prendere alcuna iniziativa per passare un'istanza di una classe per riferimento, perché ciò avverrà per impostazione predefinita.

    
risposta data 27.02.2015 - 17:32
fonte
7

Si tratta di "C #: Perché abbiamo bisogno di un'altra lingua?" - Gunnerson, Eric:

Simplicity was an important design goal for C#.

It's possible to go overboard on simplicity and language purity but purity for purity's sake is of little use to the professional programmer. We therefore tried to balance our desire to have a simple and concise language with solving the real-world problems that programmers face.

[...]

Value types, operator overloading and user-defined conversions all add complexity to the language, but allow an important user scenario to be tremendously simplified.

La semantica di riferimento per gli oggetti è un modo per evitare molti problemi (ovviamente e non solo l'affettamento degli oggetti) ma i problemi del mondo reale a volte richiedono oggetti con valore semantico (ad esempio, dai un'occhiata a I suoni come non dovrei mai usare la semantica di riferimento, giusto? per un diverso punto di vista).

Quale approccio migliore prendere, quindi, di separare quegli oggetti sporchi, brutti e cattivi con valore-semantico sotto il tag di struct ?

    
risposta data 27.02.2015 - 16:09
fonte
4

Piuttosto che pensare a tipi di valore derivanti da 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 oggetto heap corrispondente genere. 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 in base a un modello 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 una dichiarazione del tipo:     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 come boxed_Int32 ; dal momento che il sistema dovrebbe sapere quali contesti richiedono l'istanza di oggetti heap e quali richiedono un percorso di archiviazione.

Alcune persone pensano che qualsiasi tipo di valore che non si comporta come un oggetto dovrebbe essere considerato "cattivo". Prendo la tesi opposta: poiché le posizioni di memorizzazione dei tipi di valore non sono né oggetti né riferimenti agli oggetti, l'aspettativa che si comportino come oggetti dovrebbe essere considerata inutile. Nei casi in cui una struct può comportarsi in modo utile come un oggetto, non c'è niente di sbagliato nell'avere uno così, ma ogni struct è al suo centro nient'altro che un'aggregazione di campi pubblici e privati incollati insieme con del nastro adesivo.

    
risposta data 27.02.2015 - 19:18
fonte

Leggi altre domande sui tag