Por que todas as classes no .NET herdam globalmente da classe Object?

16

É muito interessante para mim quais vantagens oferecem a abordagem "classe raiz global" para o framework. Em palavras simples, quais motivos resultaram na estrutura do .NET que foi projetada para ter uma classe de objeto raiz com funcionalidade geral adequada para todas as classes.

Atualmente, estamos projetando uma nova estrutura para uso interno (a estrutura da plataforma SAP) e todos nós dividimos em dois campos - primeiro quem pensa que essa estrutura deve ter raiz global, e o segundo - quem pensa oposto.

Estou no campo "raiz global". E minhas razões pelas quais essa abordagem renderia boa flexibilidade e redução de custos de desenvolvimento, porque não desenvolveremos mais a funcionalidade geral.

Portanto, estou muito interessado em saber por que motivos realmente levam os arquitetos .NET a projetar a estrutura dessa maneira.

Anton Semenov
fonte
6
Lembre-se de que o .NET Framework faz parte de um ambiente de desenvolvimento de software generalizado. Estruturas mais específicas, como a sua, podem não precisar de um objeto "raiz". Existe uma raiz objectna estrutura do .NET em parte porque ela fornece alguns recursos básicos em todos os objetos, como ToString()eGetHashCode()
Robert Harvey
10
"Sou muito interessante saber por que razões realmente levam os arquitetos .NET a projetar a estrutura dessa maneira". Existem três razões muito boas para isso: 1. Java fez dessa maneira, 2. Java fez dessa maneira e 3. Java fez dessa maneira.
dasblinkenlight
2
@vcsjones: e Java, por exemplo, já poluído com wait()/ notify()/ notifyAll()e clone().
Joachim Sauer
3
@daskblinkenlight Isso é cocô. Java não fez dessa maneira.
Dant
2
@dasblinkenlight: FYI, Java é o único atualmente copiar C #, com lambdas, funções LINQ, etc ...
user541686

Respostas:

18

A causa mais urgente Objectsão os contêineres (antes dos genéricos) que podem conter qualquer coisa em vez de ter que seguir o estilo C "Escreva novamente para tudo o que você precisa". É claro que, sem dúvida, a idéia de que tudo deveria herdar de uma classe específica e, em seguida, abusar desse fato para perder totalmente todos os pontos de segurança de tipo é tão terrível que deveria ter sido uma luz de advertência gigante sobre o envio do idioma sem genéricos e também significa isso Objecté completamente redundante para o novo código.

DeadMG
fonte
6
Esta é a resposta certa. A falta de suporte genérico foi o único motivo. Outros argumentos "métodos comuns" não são argumentos reais porque métodos comuns não precisam ser comuns - Exemplo: 1) ToString: abusado na maioria dos casos; 2) GetHashCode: dependendo do que o programa faz, a maioria das classes não precisa desse método; 3) gettype: não tem que ser um método de instância
Codism
2
De que maneira o .NET "perde totalmente todos os tipos de segurança de tipo" ao "abusar" da idéia de que tudo deve herdar de uma classe específica? Existe algum código de merda que possa ser escrito em torno Objectque não possa ser escrito void *em C ++?
Carson63000
3
Quem disse que eu pensei que void*era melhor? Não é. Se alguma coisa. é ainda pior .
DeadMG
void*é pior em C com todos os lançamentos implícitos do ponteiro, mas o C ++ é mais rigoroso nesse sentido. Pelo menos você deve transmitir explicitamente.
Tamás Szelei
2
@ fish: Uma terminologia queixa: em C, não existe um "elenco implícito". Uma conversão é um operador explícito que especifica uma conversão. Você quer dizer " conversões implícitas de ponteiro ".
30512 Keith Thompson
11

Lembro-me de Eric Lippert dizendo uma vez que herdar da System.Objectclasse fornecia "o melhor valor para o cliente". [edit: sim, ele disse aqui ]:

... Eles não precisam de um tipo de base comum. Esta escolha não foi feita por necessidade. Foi criado com o desejo de fornecer o melhor valor para o cliente.

Ao projetar um sistema de tipos, ou qualquer outra coisa a esse respeito, às vezes você alcança pontos de decisão - você precisa decidir X ou não-X ... Os benefícios de ter um tipo de base comum superam os custos e, portanto, o benefício líquido acumulado é maior que o benefício líquido de não ter um tipo de base comum. Então escolhemos ter um tipo de base comum.

Essa é uma resposta bastante vaga. Se você quiser uma resposta mais específica, tente fazer uma pergunta mais específica.

Ter tudo derivado da System.Objectclasse fornece uma confiabilidade e utilidade que eu respeito muito. Eu sei que todos os objetos terão um type ( GetType), que eles estarão alinhados com os métodos de finalização do CLR ( Finalize) e que eu posso usá GetHashCode-los ao lidar com coleções.

gws2
fonte
2
Isso é falho. Você não precisa GetType, você pode simplesmente estender typeofpara substituir sua funcionalidade. Quanto a Finalize, na verdade, muitos tipos não são completamente finalizáveis ​​e não possuem um método Finalize significativo. Além disso, muitos tipos não são laváveis ​​nem conversíveis em strings - especialmente tipos internos que nunca são destinados a tal coisa e nunca são distribuídos para uso público. Todos esses tipos têm suas interfaces desnecessariamente poluídas.
21412 DeadMG
5
Não, não é falho - Eu nunca disse que você usaria GetType, Finalizeou GetHashCodepara cada System.Object. Apenas afirmei que eles estavam lá, se você os quisesse. Quanto a "suas interfaces desnecessariamente poluídas", vou me referir à minha citação original de elippert: "melhor valor para o cliente". Obviamente, a equipe do .NET estava disposta a lidar com as compensações.
precisa saber é
Se não for necessário, é a poluição da interface. "Se você quiser" nunca é um bom motivo para incluir um método. Ele só deve estar lá porque você precisa e seus consumidores o chamam . Senão, não é bom. Além disso, sua citação de Lippert é um apelo à falácia da autoridade, pois ele também não diz nada além de "Está bom".
19412 DeadMG
Não estou tentando argumentar o seu argumento @DeadMG - a pergunta era especificamente "Por que todas as classes no .NET herdam globalmente da classe Object" e, portanto, vinculei a uma postagem anterior por alguém muito próximo à equipe do .NET em A Microsoft que deu uma resposta legítima - ainda que vaga - por que a equipe de desenvolvimento do .NET escolheu essa abordagem.
gws2
Muito vago para ser uma resposta a esta pergunta.
19412 DeadMG
4

Eu acho que os designers de Java e C # adicionaram um objeto raiz porque era essencialmente gratuito para eles, como no almoço grátis .

Os custos de adicionar um objeto raiz são muito iguais aos custos de adicionar sua primeira função virtual. Como o CLR e a JVM são ambientes de coleta de lixo com finalizadores de objetos, você precisa ter pelo menos um virtual para o seu java.lang.Object.finalizeou para o seu System.Object.Finalizemétodo. Portanto, os custos de adicionar seu objeto raiz já são pré-pagos, você pode obter todos os benefícios sem precisar pagar por isso. Esse é o melhor dos dois mundos: os usuários que precisam de uma classe raiz comum obtêm o que desejam e os usuários que não se importam menos poderiam programar como se não estivesse lá.

dasblinkenlight
fonte
4

Parece-me que você tem três perguntas aqui: uma é por que uma raiz comum foi introduzida no .NET, a segunda é quais são as vantagens e desvantagens disso e a terceira é se é uma boa idéia para um elemento da estrutura ter uma global raiz.

Por que o .NET tem uma raiz comum?

No sentido mais técnico, ter uma raiz comum é essencial para a reflexão e para os contêineres pré-genéricos.

Além disso, que eu saiba, ter uma raiz comum com métodos básicos como equals()e hashCode()foi recebido muito positivamente em Java, e o C # foi influenciado por (entre outros) Java, então eles queriam ter esse recurso também.

Quais são as vantagens e desvantagens de ter uma raiz comum em um idioma?

A vantagem de ter uma raiz comum:

  • Você pode permitir que todos os objetos tenham uma certa funcionalidade que considere importante, como equals()e hashCode(). Isso significa, por exemplo, que todos os objetos em Java ou C # podem ser usados ​​em um mapa de hash - compare essa situação com C ++.
  • Você pode se referir a objetos de tipos desconhecidos - por exemplo, se você apenas transferir informações, como fizeram os contêineres pré-genéricos.
  • Você pode se referir a objetos de tipo desconhecido - por exemplo, ao usar reflexão.
  • Pode ser usado em casos raros, quando você deseja aceitar um objeto que pode ser de tipos diferentes e não relacionados, por exemplo, como um parâmetro de um printfmétodo semelhante.
  • Ele oferece a flexibilidade de alterar o comportamento de todos os objetos, alterando apenas uma classe. Por exemplo, se você deseja adicionar um método a todos os objetos no seu programa C #, você pode adicionar um método de extensão a object. Talvez não seja muito comum, mas sem uma raiz comum não seria possível.

A desvantagem de ter uma raiz comum:

  • Pode ser abusado se referir a um objeto de algum valor, mesmo que você possa ter usado um tipo mais preciso.

Você deve ir com uma raiz comum em seu projeto de estrutura?

Muito subjetivo, é claro, mas quando eu olho para a lista pró-contra acima, eu definitivamente respondo que sim . Especificamente, o último marcador na lista profissional - flexibilidade de alterar posteriormente o comportamento de todos os objetos alterando a raiz - se torna muito útil em uma plataforma, na qual as alterações na raiz têm mais probabilidade de serem aceitas pelos clientes do que as alterações em um todo língua.

Além disso - embora isso seja ainda mais subjetivo - acho o conceito de uma raiz comum muito elegante e atraente. Isso também facilita o uso de algumas ferramentas, por exemplo, agora é fácil solicitar que uma ferramenta mostre todos os descendentes dessa raiz comum e receba uma rápida e boa visão geral dos tipos da estrutura.

É claro que os tipos precisam estar pelo menos um pouco relacionados a isso e, em particular, eu nunca sacrificaria a regra "é-a" apenas para ter uma raiz comum.

Carvalho
fonte
Eu acho que o c # não foi meramente influenciado pelo Java, mas em algum momento foi Java. A Microsoft não podia mais usar Java, perdendo bilhões em investimentos. Eles começaram dando o idioma que eles já tinham um novo nome.
Mike Braun
3

Matematicamente, é um pouco mais elegante ter um sistema de tipos que contém top , e permite definir um idioma um pouco mais completamente.

Se você estiver criando uma estrutura, as coisas necessariamente serão consumidas por uma base de código existente. Você não pode ter um supertipo universal, já que existem todos os outros tipos em qualquer que seja o consumo da estrutura.

No que ponto, a decisão de ter uma classe base comum depende do que você está fazendo. É muito raro que muitas coisas tenham um comportamento comum e que seja útil fazer referência a essas coisas apenas pelo comportamento comum.

Mas acontece. Se isso acontecer, siga em frente abstraindo esse comportamento comum.

Telastyn
fonte
2

Eles pressionaram por um objeto raiz porque queriam que todas as classes na estrutura suportassem certas coisas (obter um código hash, converter em uma string, verificações de igualdade etc.). Tanto o C # quanto o Java consideraram útil colocar todos esses recursos comuns de um Objeto em alguma classe raiz.

Lembre-se de que eles não estão violando nenhum princípio de POO. Qualquer coisa na classe Object faz sentido em qualquer subclasse (leia-se: qualquer classe) no sistema. Se você optar por adotar esse design, siga este padrão. Ou seja, não inclua itens na raiz que não pertençam a todas as classes do sistema. Se você seguir esta regra com cuidado, não vejo motivo para não ter uma classe raiz que contenha código comum e útil para todo o sistema.

Oleksi
fonte
2
Isso definitivamente não é verdade. Existem muitas classes somente internas que nunca são hash, nunca refletidas, nunca convertidas em string. Herança desnecessária e métodos supérfluos mais definitivamente faz violar muitos princípios.
19412 DeadMG
2

Por alguns motivos, funcionalidade comum que todas as classes filho podem compartilhar. E também deu aos escritores da estrutura a capacidade de escrever muitas outras funcionalidades na estrutura que tudo poderia usar. Um exemplo seria o cache e as sessões do ASP.NET. Quase tudo pode ser armazenado neles, porque eles escreveram os métodos add para aceitar objetos.

Uma classe raiz pode ser muito atraente, mas é muito, muito fácil de usar indevidamente. Seria possível ter uma interface raiz? E apenas tem um ou dois métodos pequenos? Ou isso adicionaria muito mais código do que o necessário para sua estrutura?

Pergunto, estou curioso sobre qual funcionalidade você precisa expor a todas as classes possíveis na estrutura que está escrevendo. Isso realmente será usado por todos os objetos? Ou pela maioria deles? E uma vez criada a classe raiz, como você impedirá que as pessoas adicionem funcionalidade aleatória a ela? Ou variáveis ​​aleatórias que eles querem ser "globais"?

Vários anos atrás, havia uma aplicação muito grande onde eu criei uma classe raiz. Após alguns meses de desenvolvimento, foi preenchido por código que não tinha nenhum negócio lá. Estávamos convertendo um aplicativo ASP antigo e a classe raiz era a substituição do antigo arquivo global.inc que tínhamos usado no passado. Tive que aprender essa lição da maneira mais difícil.

bwalk2895
fonte
2

Todos os objetos de heap autônomos herdam Object; isso faz sentido porque todos os objetos de heap independentes devem ter certos aspectos em comum, como um meio de identificar seu tipo. Caso contrário, se o coletor de lixo tivesse uma referência a um objeto de heap de tipo desconhecido, não haveria como saber quais bits do blob de memória associado a esse objeto deveriam ser considerados como referências a outros objetos de heap.

Além disso, dentro do sistema de tipos, é conveniente usar o mesmo mecanismo para definir os membros das estruturas e os membros das classes. O comportamento dos locais de armazenamento do tipo valor (variáveis, parâmetros, campos, slots de matriz etc.) é muito diferente do dos locais de armazenamento do tipo classe, mas essas diferenças de comportamento são alcançadas nos compiladores de código-fonte e no mecanismo de execução (incluindo o compilador JIT) em vez de ser expresso no sistema de tipos.

Uma conseqüência disso é que a definição de um tipo de valor define efetivamente dois tipos - um tipo de local de armazenamento e um tipo de objeto de heap. O primeiro pode ser implicitamente convertido no último, e o último pode ser convertido no primeiro via typecast. Os dois tipos de conversão funcionam copiando todos os campos públicos e privados de uma instância do tipo em questão para outra. Além disso, é possível usar restrições genéricas para chamar membros da interface diretamente em um local de armazenamento de valor, sem fazer uma cópia primeiro.

Tudo isso é importante porque as referências a objetos de heap do tipo valor se comportam como referências de classe e não como tipos de valor. Considere, por exemplo, o seguinte código:

string testEnumerator <T> (T it) em que T: IEnumerator <string>
{
  var it2 = it;
  it.MoveNext ();
  it2.MoveNext ();
  retorná-lo.Current;
}
teste de nulidade pública ()
{
  var theList = nova lista <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 o testEnumerator()método itreceber um local de armazenamento do tipo valor, receberá uma instância cujos campos públicos e privados serão copiados do valor passado. A variável local it2manterá outra instância cujos campos são todos copiados it. Chamando MoveNextem it2não afetará it.

Se o código acima for passado para um local de armazenamento do tipo de classe, o valor passado it, e it2, se referirá ao mesmo objeto e, portanto, a chamada MoveNext()de qualquer um deles o chamará efetivamente em todos eles.

Note-se que lançando List<String>.Enumeratorpara IEnumerator<String>efetivamente transforma-lo a partir de um tipo de valor para um tipo de classe. O tipo do objeto de heap é, List<String>.Enumeratormas seu comportamento será muito diferente do tipo de valor com o mesmo nome.

supercat
fonte
+1 por não mencionar ToString ()
JeffO 19/07/12
1
Isso é tudo sobre detalhes de implementação. Não há necessidade de expô-los ao usuário.
21412 DeadMG
@DeadMG: Como assim? O comportamento observável de um List<T>.Enumeratorque é armazenado como a List<T>.Enumeratoré significativamente diferente do comportamento de um que é armazenado em um IEnumerator<T>, pois o armazenamento de um valor do tipo anterior em um local de armazenamento de qualquer tipo fará uma cópia do estado do enumerador, enquanto armazenar um valor desse último tipo em um local de armazenamento que também seja do último tipo, não fará uma cópia.
Supercat
Sim, mas nãoObject tem nada a ver com esse fato.
23912 DeadMG
@DeadMG: Meu argumento é que uma instância heap do tipo System.Int32também é uma instância do System.Object, mas um local de armazenamento do tipo System.Int32não contém nem uma System.Objectinstância nem uma referência a uma.
Supercat 20/07
2

Esse design realmente remonta ao Smalltalk, que eu consideraria em grande parte como uma tentativa de buscar a orientação a objetos à custa de quase toda e qualquer outra preocupação. Como tal, tende (na minha opinião) a usar a orientação a objetos, mesmo quando outras técnicas são provavelmente (ou até certamente) superiores.

Ter uma única hierarquia com Object(ou algo semelhante) na raiz facilita bastante (por exemplo) criar suas classes de coleção como coleções de Object, portanto, é trivial que uma coleção contenha qualquer tipo de objeto.

Em troca dessa pequena vantagem, você obtém uma série de desvantagens. Primeiro, do ponto de vista do design, você acaba tendo algumas idéias realmente insanas. Pelo menos de acordo com a visão Java do universo, o que o ateísmo e a floresta têm em comum? Que ambos têm códigos de hash! Um mapa é uma coleção? De acordo com Java, não, não é!

Nos anos 70, quando o Smalltalk estava sendo projetado, esse tipo de absurdo foi aceito, principalmente porque ninguém havia projetado uma alternativa razoável. O Smalltalk foi finalizado em 1980 e, em 1983, Ada (que inclui genéricos) foi projetado. Embora Ada nunca tenha alcançado o tipo de popularidade que alguns previram, seus genéricos foram suficientes para suportar coleções de objetos de tipos arbitrários - sem a loucura inerente às hierarquias monolíticas.

Quando o Java (e, em menor grau, o .NET) foram projetados, a hierarquia monolítica de classes provavelmente foi vista como uma opção "segura" - com problemas, mas principalmente problemas conhecidos . A programação genérica, por outro lado, foi uma que quase todo mundo (até então) percebeu que era pelo menos teoricamente uma abordagem muito melhor para o problema, mas que muitos desenvolvedores com orientação comercial consideravam pouco explorada e / ou arriscada (isto é, no mundo comercial). Ada foi amplamente descartado como um fracasso).

Deixe-me ser claro: a hierarquia monolítica foi um erro. As razões para esse erro eram pelo menos compreensíveis, mas era um erro de qualquer maneira. É um design ruim e seus problemas de design permeiam quase todo o código que o utiliza.

Para um novo design hoje, no entanto, não há uma pergunta razoável: usar uma hierarquia monolítica é um erro claro e uma má idéia.

Jerry Coffin
fonte
Vale a pena afirmar que os modelos eram impressionantes e úteis antes do .NET ser enviado.
19412 DeadMG
No mundo comercial, Ada foi um fracasso, não devido à sua verbosidade, mas à falta de interfaces padronizadas para os serviços do sistema operacional. Nenhuma API padrão para o sistema de arquivos, gráficos, rede, etc. tornou o desenvolvimento de aplicativos 'hospedados' da Ada extremamente caro.
Kevin cline
@kevincline: Eu provavelmente deveria ter dito isso de maneira um pouco diferente - no sentido de que Ada como um todo foi descartada como um fracasso devido a seu fracasso no mercado. Recusar-se a usar o Ada era (IMO) bastante razoável - mas recusar-se a aprender com isso muito menos (na melhor das hipóteses).
Jerry Coffin
@Jerry: Eu acho que os modelos C ++ adotaram as idéias dos Ada genéricos, depois as melhoraram bastante com instanciação implícita.
Kevin cline
1

O que você quer fazer é realmente interessante, é difícil provar se está certo ou errado até você terminar.

Aqui estão algumas coisas para pensar:

  • Quando você passaria deliberadamente esse objeto de nível superior por um objeto mais específico?
  • Qual código você pretende colocar em todas as suas aulas?

Essas são as únicas duas coisas que podem se beneficiar de uma raiz compartilhada. Em um idioma, é usado para definir alguns métodos muito comuns e permitir que você passe objetos que ainda não foram definidos, mas eles não devem se aplicar a você.

Além disso, por experiência pessoal:

Eu usei um kit de ferramentas uma vez escrito por um desenvolvedor que teve como background o Smalltalk. Ele fez isso para que todas as suas classes "Data" estendessem uma única classe e todos os métodos a classificassem - até agora tudo bem. O problema é que as diferentes classes de dados nem sempre eram intercambiáveis, então agora meu editor e compilador não podiam me dar QUALQUER ajuda sobre o que passar para uma determinada situação e eu tinha que me referir aos documentos dele, que nem sempre ajudavam. .

Essa foi a biblioteca mais difícil de usar que eu já tive que lidar.

Bill K
fonte
0

Porque esse é o caminho do POO. É importante ter um tipo (objeto) que possa se referir a qualquer coisa. Um argumento do tipo objeto pode aceitar qualquer coisa. Também há herança, você pode ter certeza de que ToString (), GetHashCode () etc estão disponíveis em tudo e qualquer coisa.

Até a Oracle percebeu a importância de ter esse tipo de base e planeja remover as primitivas em Java perto de 2017

Dante
fonte
6
Não é o caminho do POO nem importante. Você não dá argumentos reais além de "Está bom".
19412 DeadMG
1
@DeadMG Você pode ler os argumentos após a primeira frase. As primitivas se comportam de maneira diferente dos objetos, forçando o programador a escrever muitas variantes do mesmo código de finalidade para objetos e primitivas.
Dante
2
@DeadMG bem, ele disse que significa que você pode assumir ToString()e GetType()estará em todos os objetos. Provavelmente vale ressaltar que você pode usar uma variável do tipo objectpara armazenar qualquer objeto .NET.
johnl
Além disso, é perfeitamente possível escrever um tipo definido pelo usuário que possa conter uma referência de qualquer tipo. Você não precisa de recursos especiais de idioma para conseguir isso.
23912 DeadMG
Você só deve ter um objeto em níveis que contenham código específico; nunca deve ter um apenas para unir seu gráfico. O "Objeto" na verdade implementa alguns métodos importantes e atua como uma maneira de passar um objeto para ferramentas onde o tipo ainda não foi criado.
Bill K