Perché tutte le classi in .NET ereditano globalmente dalla classe Object?


16

È molto interessante per me quali vantaggi offre un approccio di "classe radice globale" per il framework. In parole semplici, quali sono le ragioni per cui il framework .NET è stato progettato per avere una classe di oggetti radice con funzionalità generale adatta a tutte le classi.

Oggi stiamo progettando un nuovo framework per uso interno (il framework sotto la piattaforma SAP) e tutti ci siamo divisi in due campi - il primo a pensare che quel framework dovrebbe avere una radice globale, e il secondo - che pensa al contrario.

Sono al campo "radice globale". E i miei motivi per cui un simile approccio produrrebbe una buona flessibilità e una riduzione dei costi di sviluppo perché non svilupperemo più funzionalità generali.

Quindi, sono molto interessato a sapere quali ragioni spingono davvero gli architetti .NET a progettare il framework in questo modo.


6
Ricorda che .NET Framework fa parte di un ambiente di sviluppo software generalizzato. Frame più specifici, come il tuo, potrebbero non aver bisogno di un oggetto "root". C'è una radice objectnel framework .NET in parte perché fornisce alcune funzionalità di base su tutti gli oggetti, come ToString()eGetHashCode()
Robert Harvey,

10
"Sono molto interessante sapere quali ragioni spingono davvero gli architetti .NET a progettare il framework in questo modo." Ci sono tre ottime ragioni per farlo: 1. Java ha fatto in quel modo, 2. Java ha fatto in quel modo e 3. Java ha fatto in quel modo.
dasblinkenlight,

2
@vcsjones: e Java per esempio già inquinati con wait()/ notify()/ notifyAll()e clone().
Joachim Sauer,

3
@daskblinkenlight Quello è cacca. Java non ha fatto così.
Dante,

2
@dasblinkenlight: Cordiali saluti, Java è quello che attualmente sta copiando C #, con lambdas, funzioni LINQ, ecc ...
user541686

Risposte:


18

La causa più urgente per Objectè i contenitori (prima dei generici) che potrebbero contenere qualsiasi cosa invece di dover andare in stile C "Scrivi di nuovo per tutto ciò di cui hai bisogno". Naturalmente, probabilmente, l'idea che tutto dovrebbe ereditare da una classe specifica e quindi abusare di questo fatto per perdere completamente ogni granello di sicurezza del tipo è così terribile che avrebbe dovuto essere una luce di avvertimento gigante sulla spedizione della lingua senza generici, e significa anche che Objectviene accuratamente ridondanti per il nuovo codice.


6
Questa è la risposta esatta. La mancanza di supporto generico era l'unica ragione. Altri argomenti "metodi comuni" non sono argomenti reali perché non è necessario che i metodi comuni siano comuni - Esempio: 1) ToString: abusato nella maggior parte dei casi; 2) GetHashCode: a seconda di ciò che fa il programma, la maggior parte delle classi non ha bisogno di questo metodo; 3) GetType: non deve essere un metodo di istanza
Codism

2
In che modo .NET "perde completamente ogni granello di sicurezza del tipo" "abusando" dell'idea che tutto dovrebbe ereditare da una classe specifica? C'è qualche codice merdoso che può essere scritto in giro Objectche non è stato possibile scrivere void *in C ++?
Carson63000,

3
Chi ha detto che pensavo void*fosse meglio? Non lo è. Se qualcosa. è anche peggio .
DeadMG

void*è peggio in C con tutti i cast di puntatori impliciti, ma C ++ è più rigoroso in questo senso. Almeno devi lanciare esplicitamente.
Tamás Szelei,

2
@fish: un cavillo terminologico: in C non esiste un "cast implicito". Un cast è un operatore esplicito che specifica una conversione. Intendi " conversioni puntatore implicite ".
Keith Thompson,

11

Ricordo che Eric Lippert una volta disse che ereditare dalla System.Objectclasse forniva "il miglior valore per il cliente". [modifica: sì, l'ha detto qui ]:

... Non hanno bisogno di un tipo di base comune. Questa scelta non è stata fatta per necessità. È nato dal desiderio di fornire il miglior valore per il cliente.

Quando si progetta un sistema di tipi o qualsiasi altra cosa, a volte si raggiungono i punti decisionali: è necessario decidere X o non-X ... I vantaggi di avere un tipo di base comune superano i costi e il vantaggio netto in tal modo maturato è maggiore del vantaggio netto di non avere un tipo di base comune. Quindi scegliamo di avere un tipo di base comune.

Questa è una risposta piuttosto vaga. Se desideri una risposta più specifica, prova a porre una domanda più specifica.

Avere tutto derivato dalla System.Objectclasse offre un'affidabilità e un'utilità che ho imparato a rispettare molto. So che tutti gli oggetti avranno un tipo ( GetType), che saranno in linea con i metodi di finalizzazione del CLR ( Finalize) e che posso usare GetHashCodesu di essi quando ho a che fare con le raccolte.


2
Questo è difettoso. Non è necessario GetType, puoi semplicemente estenderlo typeofper sostituirne la funzionalità. Per quanto riguarda Finalize, quindi, in realtà, molti tipi non sono completamente definibili e non possiedono un metodo Finalize significativo. Inoltre, molti tipi non sono hash né convertibili in stringhe, in particolare tipi interni che non sono mai destinati a tale scopo e non vengono mai distribuiti per uso pubblico. Tutti questi tipi hanno le loro interfacce inutilmente inquinate.
DeadMG

5
No, non è viziata - Non ho mai sostenuto che si usa GetType, Finalizeo GetHashCodeper ogni System.Object. Ho semplicemente dichiarato che erano lì se li volevi. Per quanto riguarda "le loro interfacce inutilmente inquinate", farò riferimento alla mia citazione originale di elippert: "miglior valore per il cliente". Ovviamente il team .NET era disposto a gestire i compromessi.
gws2,

Se non è necessario, allora è l'inquinamento dell'interfaccia. "Se vuoi" non è mai un buon motivo per includere un metodo. Dovrebbe essere lì solo perché ne hai bisogno e i tuoi consumatori lo chiameranno . Altrimenti, non va bene. Inoltre, la tua citazione di Lippert è un appello all'errore di autorità, poiché non dice altro che "È buono".
DeadMG

Non sto cercando di discutere i tuoi punti @DeadMG - la domanda era specificamente "Perché tutte le classi in .NET ereditano globalmente dalla classe Object" e quindi ho collegato a un post precedente di qualcuno molto vicino al team di .NET in Microsoft, che aveva fornito una risposta legittima, sebbene vaga, sul perché il team di sviluppo di .NET avesse scelto questo approccio.
gws2

Troppo vaga per essere una risposta a questa domanda.
DeadMG

4

Penso che i designer Java e C # abbiano aggiunto un oggetto root perché era essenzialmente gratuito per loro, come nel pranzo libero .

I costi per l'aggiunta di un oggetto root sono molto uguali ai costi per l'aggiunta della prima funzione virtuale. Poiché sia ​​CLR che JVM sono ambienti garbage collection con finalizzatori di oggetti, è necessario disporre di almeno uno virtuale per il java.lang.Object.finalizeproprio System.Object.Finalizemetodo. Quindi, i costi per l'aggiunta del tuo oggetto root sono già pagati in anticipo, puoi ottenere tutti i vantaggi senza pagarlo. Questo è il migliore dei due mondi: gli utenti che hanno bisogno di una classe radice comune otterrebbero ciò che vogliono e gli utenti a cui non potrebbe importare di meno sarebbero in grado di programmare come se non ci fosse.


4

Mi sembra che tu abbia tre domande qui: una è il motivo per cui una radice comune è stata introdotta in .NET, la seconda è quali sono i vantaggi e gli svantaggi di ciò, e la terza è se è una buona idea che un elemento framework abbia un valore globale radice.

Perché .NET ha una radice comune?

Nel senso più tecnico, avere una radice comune è essenziale per la riflessione e per i contenitori pre-generici.

Inoltre, per quanto ne so , avere una radice comune con metodi di base come equals()ed è hashCode()stato accolto molto positivamente in Java, e C # è stato influenzato da (tra gli altri) Java, quindi volevano avere anche quella caratteristica.

Quali sono i vantaggi e gli svantaggi di avere una radice comune in una lingua?

Il vantaggio di avere una radice comune:

  • Puoi consentire a tutti gli oggetti di avere una certa funzionalità che ritieni importante, come equals()e hashCode(). Ciò significa, ad esempio, che ogni oggetto in Java o C # può essere utilizzato in una mappa hash: confronta quella situazione con C ++.
  • Puoi fare riferimento a oggetti di tipo sconosciuto, ad esempio se trasferisci informazioni in giro, come hanno fatto i contenitori pre-generici.
  • È possibile fare riferimento a oggetti di tipo inconoscibile , ad esempio quando si utilizza la riflessione.
  • Può essere utilizzato nei rari casi in cui si desidera essere in grado di accettare un oggetto che può essere di tipi diversi e altrimenti non correlati, ad esempio come parametro di un printfmetodo simile.
  • Ti dà la flessibilità di cambiare il comportamento di tutti gli oggetti cambiando solo una classe. Ad esempio, se si desidera aggiungere un metodo a tutti gli oggetti nel programma C #, è possibile aggiungere un metodo di estensione a object. Non molto comune, forse, ma senza una radice comune non sarebbe possibile.

Lo svantaggio di avere una radice comune:

  • Si può abusare di fare riferimento a un oggetto di un certo valore anche se si sarebbe potuto usare un tipo più accurato per esso.

Dovresti andare con una radice comune nel tuo progetto quadro?

Molto soggettivo, ovviamente, ma quando guardo la lista pro-con sopra risponderei sicuramente di . In particolare, l'ultimo punto elenco nell'elenco dei professionisti, ovvero la flessibilità di modificare in seguito il comportamento di tutti gli oggetti modificando la radice, diventa molto utile in una piattaforma, in cui le modifiche alla radice hanno maggiori probabilità di essere accettate dai clienti rispetto alle modifiche a un intero linguaggio.

Inoltre, sebbene sia ancora più soggettivo, trovo il concetto di radice comune molto elegante e accattivante. Inoltre semplifica l'utilizzo di alcuni strumenti, ad esempio ora è facile chiedere a uno strumento di mostrare tutti i discendenti di quella radice comune e ricevere una rapida e buona panoramica dei tipi di framework.

Certo, i tipi devono essere almeno leggermente correlati per questo, e in particolare, non sacrificherei mai la regola "is-a" solo per avere una radice comune.


Penso che c # non sia stato semplicemente influenzato da Java, ma ad un certo punto -was- Java. Microsoft non poteva più usare Java, quindi perderebbe miliardi di investimenti. Hanno iniziato dando la lingua che avevano già un nuovo nome.
Mike Braun,

3

Matematicamente, è un po 'più elegante avere un sistema di tipi che contiene top e ti permette di definire un linguaggio un po' più completamente.

Se stai creando un framework, le cose saranno necessariamente consumate da una base di codice esistente. Non puoi avere un supertipo universale poiché esistono tutti gli altri tipi in qualunque consuma il framework.

A quel punto, la decisione di avere una classe base comune dipende da cosa stai facendo. È molto raro che molte cose abbiano un comportamento comune e che sia utile fare riferimento a queste cose solo attraverso il comportamento comune.

Ma succede In caso affermativo, procedi subito nell'astrarre quel comportamento comune.


2

Hanno spinto per un oggetto root perché volevano che tutte le classi nel framework supportassero determinate cose (ottenere un codice hash, convertire in una stringa, controlli di uguaglianza, ecc.). Sia C # che Java hanno trovato utile mettere tutte queste caratteristiche comuni di un oggetto in una classe radice.

Tieni presente che non stanno violando alcun principio OOP o altro. Qualsiasi cosa nella classe Object ha senso in qualsiasi sottoclasse (leggi: qualsiasi classe) nel sistema. Se scegli di adottare questo disegno, assicurati di seguire questo schema. Cioè, non includere cose nella radice che non appartengono a ogni singola classe nel tuo sistema. Se segui attentamente questa regola, non vedo alcun motivo per cui non dovresti avere una classe radice che contenga un codice utile e comune per l'intero sistema.


2
Questo non è affatto vero. Esistono molte classi solo interne che non sono mai sottoposte a hash, mai riflesse, mai convertite in stringa. Eredità inutili e superflui metodi sicuramente fa violare molti principi.
DeadMG

2

Un paio di ragioni, funzionalità comuni che possono condividere tutte le classi figlio. E ha anche dato agli autori del framework la possibilità di scrivere molte altre funzionalità nel framework che tutto poteva usare. Un esempio potrebbe essere la cache e le sessioni ASP.NET. Quasi tutto può essere memorizzato in essi, perché hanno scritto i metodi add per accettare gli oggetti.

Una classe radice può essere molto allettante, ma è molto, molto facile da usare in modo improprio. Sarebbe invece possibile avere un'interfaccia root? E hai solo uno o due piccoli metodi? O aggiungerebbe molto più codice di quanto è necessario al tuo framework?

Chiedo di essere curioso di sapere quali funzionalità sono necessarie per esporre a tutte le classi possibili nel framework che si sta scrivendo. Sarà veramente usato da tutti gli oggetti? O dalla maggior parte di loro? E una volta creata la classe root, come impedirai alle persone di aggiungere funzionalità casuali ad essa? O variabili casuali che vogliono essere "globali"?

Diversi anni fa c'era un'applicazione molto grande in cui ho creato una classe radice. Dopo alcuni mesi di sviluppo, era popolato da un codice che non conteneva attività commerciali. Stavamo convertendo una vecchia applicazione ASP e la classe root sostituiva il vecchio file global.inc che avevamo usato in passato. Ho dovuto imparare quella lezione nel modo più duro.


2

Tutti gli oggetti heap autonomi ereditano da Object; questo ha senso perché tutti gli oggetti heap autonomi devono avere alcuni aspetti comuni, come un mezzo per identificarne il tipo. Altrimenti, se il Garbage Collector avesse un riferimento a un oggetto heap di tipo sconosciuto, non avrebbe modo di sapere quali bit all'interno del blocco di memoria associati a quell'oggetto dovrebbero essere considerati riferimenti ad altri oggetti heap.

Inoltre, all'interno del sistema dei tipi, è conveniente usare lo stesso meccanismo per definire i membri delle strutture e i membri delle classi. Il comportamento delle posizioni di archiviazione di tipo valore (variabili, parametri, campi, slot di array, ecc.) È molto diverso da quello delle posizioni di archiviazione di tipo classe, ma tali differenze comportamentali si ottengono nei compilatori di codice sorgente e nel motore di esecuzione (incluso il compilatore JIT) anziché essere espresso nel sistema dei tipi.

Una conseguenza di ciò è che la definizione di un tipo di valore definisce in modo efficace due tipi: un tipo di posizione di archiviazione e un tipo di oggetto heap. Il primo può essere convertito in modo implicito nel secondo e il secondo può essere convertito nel primo tramite typecast. Entrambi i tipi di conversione funzionano copiando tutti i campi pubblici e privati ​​da un'istanza del tipo in questione a un'altra. Inoltre, è possibile utilizzare vincoli generici per richiamare direttamente i membri dell'interfaccia in un percorso di archiviazione di tipo valore, senza prima crearne una copia.

Tutto ciò è importante perché i riferimenti agli oggetti heap di tipo valore si comportano come riferimenti di classe e non come tipi di valore. Si consideri, ad esempio, il seguente codice:

string testEnumerator <T> (T it) dove T: IEnumerator <stringa>
{
  var it2 = it;
  it.MoveNext ();
  it2.MoveNext ();
  restituirlo.Corrente;
}
public void test ()
{
  var theList = new List <string> ();
  theList.Add ( "Fred");
  theList.Add ( "George");
  theList.Add ( "Percy");
  theList.Add ( "Molly");
  theList.Add ( "Ron");

  var enum1 = theList.GetEnumerator ();
  IEnumerator <stringa> enum2 = enum1;

  Debug.Print (testEnumerator (enum1));
  Debug.Print (testEnumerator (enum1));
  Debug.Print (testEnumerator (enum2));
  Debug.Print (testEnumerator (enum2));
}

Se al testEnumerator()metodo viene passata una posizione di archiviazione di tipo valore, itverrà ricevuta un'istanza i cui campi pubblici e privati ​​vengono copiati dal valore passato. La variabile locale it2conterrà un'altra istanza i cui campi sono tutti copiati it. Chiamando MoveNextil it2non influirà it.

Se al codice sopra riportato viene passata una posizione di archiviazione di tipo classe, il valore passato ite, quindi it2, si riferiranno tutti allo stesso oggetto, e quindi chiamando MoveNext()uno di essi lo chiamerà effettivamente su tutti.

Si noti che la fusione List<String>.Enumeratora IEnumerator<String>efficacemente lo trasforma da un tipo di valore a un tipo di classe. Il tipo di oggetto heap è List<String>.Enumeratorma il suo comportamento sarà molto diverso dal tipo di valore con lo stesso nome.


+1 per non menzionare ToString ()
JeffO

1
Si tratta di dettagli di implementazione. Non è necessario esporli all'utente.
DeadMG

@DeadMG: cosa intendi? Il comportamento osservabile di un List<T>.Enumeratoroggetto memorizzato come List<T>.Enumeratorè significativamente diverso dal comportamento di un oggetto archiviato in un IEnumerator<T>, in quanto la memorizzazione di un valore del precedente tipo in una posizione di archiviazione di entrambi i tipi farà una copia dello stato dell'enumeratore, mentre si memorizza un valore di quest'ultimo tipo in una posizione di archiviazione che è anche di quest'ultimo tipo non verrà eseguita una copia.
supercat,

Sì, ma nonObject ha nulla a che fare con questo fatto.
DeadMG

@DeadMG: Il mio punto è che un'istanza heap di tipo System.Int32è anche un'istanza di System.Object, ma una posizione di archiviazione di tipo System.Int32non contiene né System.Objectun'istanza né un riferimento a una.
Supercat,

2

Questo design risale davvero a Smalltalk, che vedrei in gran parte come un tentativo di perseguire l'orientamento agli oggetti a spese di quasi tutte le altre preoccupazioni. Come tale, tende (a mio avviso) a utilizzare l'orientamento agli oggetti, anche quando altre tecniche sono probabilmente (o addirittura certamente) superiori.

Avere una singola gerarchia con Object(o qualcosa di simile) alla radice rende abbastanza facile (per un esempio) creare le tue classi di raccolta come raccolte di Object, quindi è banale che una raccolta contenga qualsiasi tipo di oggetto.

In cambio di questo vantaggio piuttosto minore, si ottiene una serie di svantaggi. Innanzitutto, dal punto di vista del design, ti ritrovi con alcune idee davvero folli. Almeno secondo la visione Java dell'universo, cosa hanno in comune l'ateismo e una foresta? Che entrambi hanno hashcode! Una mappa è una raccolta? Secondo Java, no, non lo è!

Negli anni '70, quando fu progettato Smalltalk, questo tipo di assurdità fu accettato, principalmente perché nessuno aveva progettato un'alternativa ragionevole. Smalltalk fu ultimato nel 1980 e nel 1983 fu progettato Ada (che include i generici). Sebbene Ada non abbia mai raggiunto il tipo di popolarità che alcuni avevano previsto, i suoi generici erano sufficienti a supportare raccolte di oggetti di tipo arbitrario, senza la follia insita nelle gerarchie monolitiche.

Quando furono progettati Java (e, in misura minore, .NET), la gerarchia di classi monolitiche era probabilmente vista come una scelta "sicura" - una con problemi, ma per lo più nota . La programmazione generica, al contrario, era quella che quasi tutti (anche allora) hanno capito che almeno teoricamente era un approccio molto migliore al problema, ma che molti sviluppatori orientati al commercio consideravano piuttosto scarsamente esplorato e / o rischioso (cioè, nel mondo commerciale , Ada è stato in gran parte respinto come un fallimento).

Vorrei essere cristallino però: la gerarchia monolitica è stata un errore. Le ragioni di quell'errore erano almeno comprensibili, ma era comunque un errore. È una cattiva progettazione e i suoi problemi di progettazione pervadono quasi tutto il codice che la utilizza.

Per un nuovo design oggi, tuttavia, non c'è una domanda ragionevole: usare una gerarchia monolitica è un errore evidente e una cattiva idea.


Vale la pena affermare che i modelli erano noti per essere fantastici e utili prima della spedizione di .NET.
DeadMG

Nel mondo commerciale, Ada è stato un fallimento, non tanto per la sua verbosità, ma per la mancanza di interfacce standardizzate per i servizi del sistema operativo. Nessuna API standard per il file system, la grafica, la rete, ecc. Ha reso estremamente costoso lo sviluppo di applicazioni "ospitate" di Ada.
Kevin Cline,

@kevincline: probabilmente avrei dovuto formularlo in modo un po 'diverso, per il fatto che Ada nel suo insieme è stata respinta come un fallimento a causa del suo fallimento sul mercato. Rifiutare di usare Ada era (IMO) abbastanza ragionevole, ma rifiutarsi di imparare da esso molto meno (nella migliore delle ipotesi).
Jerry Coffin,

@Jerry: Penso che i modelli C ++ abbiano adottato le idee dei generici di Ada, quindi le abbiano migliorate notevolmente con un'istanza implicita.
Kevin Cline,

1

Quello che vuoi fare è in realtà interessante, è difficile provare se è sbagliato o giusto fino a quando non hai finito.

Ecco alcune cose a cui pensare:

  • Quando mai passeresti deliberatamente questo oggetto di livello superiore su un oggetto più specifico?
  • Quale codice intendi inserire in ciascuna delle tue classi?

Queste sono le uniche due cose che possono beneficiare di una radice condivisa. In una lingua viene utilizzato per definire alcuni metodi molto comuni e consentire di passare oggetti che non sono stati ancora definiti, ma che non dovrebbero applicarsi a te.

Inoltre, per esperienza personale:

Ho usato un toolkit una volta scritto da uno sviluppatore il cui background era Smalltalk. Ha fatto in modo che tutte le sue classi "Data" estendessero una singola classe e tutti i metodi hanno preso quella classe - finora tutto bene. Il problema è che le diverse classi di dati non erano sempre intercambiabili, quindi ora il mio editor e compilatore non potevano darmi alcun aiuto su cosa passare in una determinata situazione e dovevo fare riferimento ai suoi documenti che non sempre aiutavano. .

Questa è stata la biblioteca più difficile da usare con cui abbia mai avuto a che fare.


0

Perché questo è il modo di OOP. È importante avere un tipo (oggetto) che possa fare riferimento a qualsiasi cosa. Un argomento di tipo oggetto può accettare qualsiasi cosa. C'è anche l'eredità, puoi essere sicuro che ToString (), GetHashCode () ecc. Sono disponibili su qualsiasi cosa.

Anche Oracle ha capito l'importanza di avere un tale tipo di base e sta pianificando di rimuovere le primitive in Java vicino al 2017


6
Non è né la via della OOP né importante. Non si danno argomenti reali diversi da "È buono".
DeadMG

1
@DeadMG Puoi leggere gli argomenti dopo la prima frase. I primitivi si comportano diversamente dagli oggetti, costringendo così il programmatore a scrivere molte varianti dello stesso codice scopo per oggetti e primitivi.
Dante,

2
@DeadMG bene ha detto che significa che puoi assumere ToString()e GetType()sarà su ogni oggetto. Vale probabilmente la pena sottolineare che è possibile utilizzare una variabile di tipo objectper archiviare qualsiasi oggetto .NET.
JohnL

Inoltre, è perfettamente possibile scrivere un tipo definito dall'utente che può contenere un riferimento di qualsiasi tipo. Non hai bisogno di funzioni linguistiche speciali per raggiungere questo obiettivo.
DeadMG

Dovresti avere solo un oggetto a livelli che contengono un codice specifico, non dovresti mai averne uno solo per collegare il tuo grafico. L '"oggetto" in realtà implementa alcuni metodi importanti e funge da modo per passare un oggetto negli strumenti in cui il tipo non è stato ancora creato.
Bill K,
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.