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

15

È molto interessante per me quali vantaggi offra un approccio alla "root-root globale" per il framework. In parole semplici quali sono le ragioni che hanno portato il framework .NET è stato progettato per avere una classe root object con funzionalità generali adatte a tutte le classi.

Al giorno d'oggi stiamo progettando un nuovo framework per uso interno (il framework sotto la piattaforma SAP) e ci dividiamo in due campi: prima chi pensa che il framework debba avere una radice globale, e il secondo - chi pensa al contrario.

Sono al campo "global root". E le mie ragioni per cui un simile approccio darebbe una buona flessibilità e una riduzione dei costi di sviluppo non determinerebbero più la funzionalità generale.

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

    
posta Anton Semenov 19.07.2012 - 17:51
fonte

11 risposte

14

La causa più pressante per Object è contenitori (prima di generici) che potrebbero contenere qualsiasi cosa invece di dover andare in stile C "Scrivilo di nuovo per tutto il necessario". Naturalmente, probabilmente, l'idea che tutto dovrebbe ereditare da una classe specifica e poi abusare di questo fatto per perdere completamente ogni punto di sicurezza del tipo è così terribile che avrebbe dovuto essere una gigantesca luce di avviso sulla spedizione della lingua senza generici, e significa anche che Object è completamente ridondante per il nuovo codice.

    
risposta data 19.07.2012 - 18:49
fonte
9

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

...They don't need a common base type. This choice was not made out of necessity. It was made out of a desire to provide the best value for the customer.

When designing a type system, or anything else for that matter, sometimes you reach decision points -- you have to decide either X or not-X... The benefits of having a common base type outweigh the costs, and the net benefit thereby accrued is larger than the net benefit of having no common base type. So we choose to have a common base type.

That's a pretty vague answer. If you want a more specific answer, try asking a more specific question.

Il fatto che tutto derivi dalla classe System.Object fornisce 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 GetHashCode su di essi quando si tratta di collezioni.

    
risposta data 19.07.2012 - 18:14
fonte
3

Matematicamente, è un po 'più elegante avere un sistema di tipi che contiene top e ti consente di definire una lingua 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é tutti gli altri tipi in tutto ciò che consuma il framework esistono.

Al punto che , 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 tramite il comportamento comune da solo.

Ma succede. Se lo fa, vai avanti nel ritirare questo comportamento comune.

    
risposta data 19.07.2012 - 18:16
fonte
3

Penso che i progettisti Java e C # abbiano aggiunto un oggetto radice perché era essenzialmente libero da essi, come in pranzo libero .

I costi per l'aggiunta di un oggetto radice sono molto simili ai costi di aggiunta della prima funzione virtuale. Poiché CLR e JVM sono ambienti garbage collector con finalizzatori di oggetti, è necessario disporre di almeno un virtual per il java.lang.Object.finalize o per il metodo System.Object.Finalize . Quindi, i costi di aggiunta dell'oggetto radice sono già prepagati, è possibile ottenere tutti i benefici senza pagare per questo. Questo è il meglio di entrambi i mondi: gli utenti che hanno bisogno di una classe radice comune otterrebbero ciò che vogliono, e gli utenti che non potrebbero preoccuparsi di meno sarebbero in grado di programmare come se non ci fossero.

    
risposta data 19.07.2012 - 18:19
fonte
3

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

Perché .NET ha una radice comune?

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

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

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 di oggetti di avere una certa funzionalità che consideri importante, come equals() e hashCode() . Ciò significa, ad esempio, che ogni oggetto in Java o C # può essere utilizzato in una mappa hash - confronta la situazione con C ++.
  • Puoi fare riferimento a oggetti di tipi sconosciuti - ad es. se trasferisci semplicemente le informazioni, come hanno fatto i contenitori pre-generici.
  • Puoi fare riferimento a oggetti di tipo non conoscibile - ad es. quando si usa il riflesso.
  • Può essere utilizzato nei rari casi in cui desideri essere in grado di accettare un oggetto che può essere di tipi diversi e altrimenti non correlati, ad es. come parametro di un metodo printf -like.
  • 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 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:

  • Può essere abusato fare riferimento a un oggetto di qualche valore anche se potresti aver usato un tipo più accurato per questo.

Dovresti andare con una radice comune nel tuo progetto quadro?

Ovviamente molto soggettivo, ma quando guardo la lista dei pro-con sopra risponderei sicuramente . In particolare, l'ultimo punto elenco nella lista dei professionisti - la flessibilità di modificare in seguito il comportamento di tutti gli oggetti modificando la radice - diventa molto utile in una piattaforma, in cui è più probabile che le modifiche alla radice vengano accettate dai client rispetto alle modifiche a un'intera lingua.

Inoltre - anche se questo è ancora più soggettivo - trovo il concetto di una radice comune molto elegante e accattivante. Semplifica anche 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 panoramica dei tipi di framework.

Naturalmente, i tipi devono essere almeno leggermente correlati per quello, e in particolare, non avrei mai sacrificato la regola "is-a" solo per avere una radice comune.

    
risposta data 20.07.2012 - 12:42
fonte
2

Il push per un oggetto radice perché volevano tutte classi nel framework per supportare determinate cose (ottenendo un codice hash, convertendo in una stringa, controlli di uguaglianza, ecc.). Sia C # che Java hanno trovato utile mettere tutte queste caratteristiche comuni di un oggetto in qualche classe radice.

Ricorda 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 design, 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 root che contiene codice comune utile per l'intero sistema.

    
risposta data 19.07.2012 - 18:06
fonte
2

Un paio di motivi, funzionalità comune che tutte le classi figlie possono condividere. E ha anche dato agli sceneggiatori la possibilità di scrivere molte altre funzionalità nel framework che tutto potrebbe usare. Un esempio potrebbe essere il caching e le sessioni ASP.NET. Quasi tutto può essere memorizzato in essi, perché hanno scritto i metodi di aggiunta per accettare oggetti.

Una classe radice può essere molto allettante, ma è molto, molto facile da abusare. Sarebbe possibile avere una interfaccia di root, invece? E hai solo uno o due piccoli metodi? O aggiungerebbe molto più codice di quello richiesto per il tuo framework?

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

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

    
risposta data 19.07.2012 - 18:44
fonte
2

Tutti gli oggetti heap standalone ereditano da Object ; questo ha senso perché tutti gli oggetti heap standalone devono avere alcuni aspetti comuni, come un mezzo per identificare il loro 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 blob di memoria associato a quell'oggetto dovrebbero essere considerati come riferimenti ad altri oggetti heap.

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

Una conseguenza di ciò è che la definizione di un tipo di valore definisce efficacemente due tipi: un tipo di posizione di memoria e un tipo di oggetto heap. Il primo può essere implicitamente convertito in quest'ultimo e quest'ultimo 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 i vincoli generici per invocare direttamente i membri dell'interfaccia su una posizione di memoria di tipo valore, senza prima farne 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) where T:IEnumerator<string>
{
  var it2 = it;
  it.MoveNext();
  it2.MoveNext();
  return it.Current;
}
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<string> enum2 = enum1;

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

Se il metodo testEnumerator() viene passato a un percorso di memorizzazione di tipo valore, it riceverà un'istanza i cui campi pubblici e privati vengono copiati dal valore passato. La variabile locale it2 contiene un'altra istanza i cui campi sono tutti copiati da it . Chiamare MoveNext su it2 non influenzerà it .

Se il codice precedente viene passato a un percorso di archiviazione di tipo classe, quindi il valore passato, it e it2 , si riferiscono tutti allo stesso oggetto e quindi chiamano MoveNext() su uno di essi lo chiamerà efficacemente su tutti loro.

Tieni presente che il lancio di List<String>.Enumerator in IEnumerator<String> lo trasforma efficacemente da un tipo di valore a un tipo di classe. Il tipo di oggetto heap è List<String>.Enumerator , ma il suo comportamento sarà molto diverso dal tipo di valore con lo stesso nome.

    
risposta data 19.07.2012 - 19:02
fonte
2

Questo disegno riconduce davvero a Smalltalk, che considererei in gran parte come un tentativo di perseguire l'orientamento degli oggetti a scapito di quasi tutte le altre preoccupazioni. In quanto tale, tende (secondo me) a usare l'orientamento all'oggetto, anche quando altre tecniche sono probabilmente (o anche 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 secondario, si ottiene comunque una serie di svantaggi. In primo luogo, da un punto di vista del design, si finisce con alcune idee veramente folli. Almeno secondo la visione di Java dell'universo, che cosa hanno in comune l'ateismo e una foresta? Che entrambi hanno codici hash! Una mappa è una collezione? Secondo Java, no, non lo è!

Negli anni '70, quando Smalltalk veniva progettato, questo tipo di assurdità veniva accettato, principalmente perché nessuno aveva progettato un'alternativa ragionevole. Smalltalk è stato finalizzato nel 1980 e, nel 1983, è stata progettata Ada (che include i generici). Sebbene Ada non abbia mai raggiunto il tipo di popolarità che qualcuno aveva predetto, i suoi generici erano sufficienti a supportare raccolte di oggetti di tipi arbitrari - senza la follia insita nelle gerarchie monolitiche.

Quando sono stati progettati Java (e in misura minore, .NET), la gerarchia di classe monolitica è stata probabilmente considerata una scelta "sicura", una con problemi, ma soprattutto con problemi noti . La programmazione generica, al contrario, era quella che quasi tutti (anche allora) realizzarono almeno teoricamente un approccio molto migliore al problema, ma che molti sviluppatori commercialmente orientati consideravano piuttosto scarsamente esplorato e / o rischioso (cioè, nel mondo commerciale, Ada è stato in gran parte respinto come un fallimento).

Però, lasciami essere chiaro: la gerarchia monolitica era un errore. Le ragioni di tale errore erano almeno comprensibili, ma era comunque un errore. È un cattivo design e i suoi problemi di progettazione pervadono quasi tutto il codice che lo utilizza.

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

    
risposta data 19.07.2012 - 19:19
fonte
1

Quello che vuoi fare è davvero interessante, è difficile provare se è sbagliato o giusto finché non hai finito.

Ecco alcune cose a cui pensare:

  • Quando passare 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 benificarsi da 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 prendessero quella classe - fin qui tutto bene. Il problema è che le diverse classi di dati non erano sempre intercambiabili, quindi ora il mio editore e compilatore non potevano darmi alcun aiuto su cosa passare in una data situazione e dovevo fare riferimento ai suoi documenti che non sempre aiutavano. .

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

    
risposta data 19.07.2012 - 21:38
fonte
0

Perché questa è la via dell'OOP. È importante avere un tipo (oggetto) che possa riferirsi a qualsiasi cosa. Un argomento di tipo oggetto può accettare qualsiasi cosa. C'è anche l'ereditarietà, puoi essere certo che ToString (), GetHashCode () ecc. Sono disponibili su qualsiasi cosa.

Anche Oracle ha compreso l'importanza di avere un tale tipo di base ed è pianificazione della rimozione delle primitive in Java vicino al 2017

    
risposta data 19.07.2012 - 18:17
fonte

Leggi altre domande sui tag