“Pai x = new Child ();” em vez de “Child x = new Child ();” é uma prática ruim se pudermos usar o último?

32

Por exemplo, eu tinha visto alguns códigos que criam um fragmento como este:

Fragment myFragment=new MyFragment();

que declara uma variável como fragmento em vez de MyFragment, que MyFragment é uma classe filho de fragmento. Não estou satisfeito com esta linha de códigos porque acho que esse código deve ser:

MyFragment myFragment=new MyFragment();

o que é mais específico, isso é verdade?

Ou, na generalização da questão, é uma má prática usar:

Parent x=new Child();

ao invés de

Child x=new Child();

se podemos mudar o primeiro para o último sem erro de compilação?

ggrr
fonte
6
Se fizermos o último, não há mais motivo para abstração / generalização. Claro que há exceções ...
Walfrat
2
Em um projeto em que estou trabalhando, minhas funções esperam o tipo pai e executam métodos do tipo pai. Posso aceitar qualquer um dos vários filhos, e o código por trás desses métodos (herdados e substituídos) é diferente, mas quero executar esse código, independentemente do filho que estou usando.
Stephen S
4
@ Walfrat De fato, há pouco sentido em abstração quando você já está instanciando o tipo concreto, mas ainda é possível retornar o tipo abstrato para que o código do cliente seja alegremente ignorante.
Jacob Raihle
4
Não use Pai / Filho. Use Interface / Classe (para Java).
Thorbjørn Ravn Andersen
4
Google "programando para uma interface" para várias explicações sobre por que usar Parent x = new Child()é uma boa idéia.
Kevin Workman

Respostas:

69

Depende do contexto, mas eu diria que você deve declarar o tipo mais abstrato possível. Dessa forma, seu código será o mais genérico possível e não dependerá de detalhes irrelevantes.

Um exemplo seria ter um LinkedListe do ArrayListqual ambos descendem List. Se o código funcionaria igualmente bem com qualquer tipo de lista, não há razão para restringi-lo arbitrariamente a uma das subclasses.

JacquesB
fonte
24
Exatamente. Para adicionar um exemplo, pense List list = new ArrayList(): O código que usa a lista não se importa com o tipo de lista, apenas com o contrato da interface da Lista. No futuro, poderíamos mudar facilmente para uma implementação de Lista diferente e o código subjacente não precisaria ser alterado ou atualizado.
Maybe_Factor
19
uma boa resposta, mas deve expandir o tipo mais abstrato possível, significa que o código no escopo não deve ser convertido de volta ao filho para executar alguma operação específica do filho.
Mike
10
@ Mike: Espero que não seja preciso dizer, caso contrário, todas as variáveis, parâmetros e campos seriam declarados como Object!
precisa saber é o seguinte
3
@ JacquesB: como essa pergunta e resposta receberam tanta atenção, pensei que ser explícito seria útil para pessoas com todos os níveis de habilidade.
Mike
1
Depende do contexto não é uma resposta.
Billal Begueradj
11

A resposta de JacquesB está correta, embora um pouco abstrata. Deixe-me enfatizar alguns aspectos mais claramente:

No seu snippet de código, você usa explicitamente a newpalavra - chave e talvez queira continuar com outras etapas de inicialização que provavelmente são específicas para o tipo concreto: é necessário declarar a variável com esse tipo concreto específico.

A situação é diferente quando você obtém esse item de outro lugar, por exemplo IFragment fragment = SomeFactory.GiveMeFragments(...);. Aqui, a fábrica normalmente deve retornar o tipo mais abstrato possível e o código descendente não deve depender dos detalhes da implementação.

Bernhard Hiller
fonte
18
"embora um pouco abstrato". Badumtish.
rosuav
1
isso não poderia ter sido uma edição?
beppe9000
6

Existem diferenças de desempenho potencialmente.

Se a classe derivada for selada, o compilador poderá incorporar e otimizar ou eliminar o que de outra forma seriam chamadas virtuais. Portanto, pode haver uma diferença significativa de desempenho.

Exemplo: ToStringé uma chamada virtual, mas Stringestá selada. Ativado String, ToStringé um no-op; portanto, se você declarar objectque é uma chamada virtual, se declarar que Stringo compilador sabe que nenhuma classe derivada substituiu o método porque a classe está selada, então isso é um no-op. Considerações semelhantes aplicam-se a ArrayListvs LinkedList.

Portanto, se você conhece o tipo concreto do objeto (e não há motivo para encapsulá-lo), você deve declarar esse tipo. Desde que você acabou de criar o objeto, você conhece o tipo concreto.

Ben
fonte
4
Na maioria dos casos, as diferenças de desempenho são minúsculas. E na maioria dos casos (ouvi dizer que isso acontece cerca de 90% do tempo), devemos esquecer essas pequenas diferenças. Somente quando soubermos (preferencialmente por meio de medição) que envelhecemos como seção crítica de desempenho do código devemos nos preocupar com essas otimizações.
Jules
E o compilador sabe que "new Child ()" retorna um Child, mesmo quando atribuído a um Parent, e desde que ele saiba, ele pode usar esse conhecimento e chamar o código Child ().
precisa saber é o seguinte
+1. Também roubou seu exemplo na minha resposta. = P
Nat
1
Observe que existem otimizações de desempenho que tornam a coisa toda discutível. O HotSpot, por exemplo, notará que um parâmetro passado para uma função é sempre de uma classe específica e é otimizado de acordo. Se essa suposição for falsa, o método será desoptimizado. Mas isso depende fortemente do ambiente do compilador / tempo de execução. Última vez que verifiquei o CLR não poderia mesmo fazer a otimização bastante trivial de perceber que p a partir Parent p = new Child()é sempre uma criança ..
Voo
4

A principal diferença é o nível de acesso necessário. E toda boa resposta sobre abstração envolve animais, pelo que me disseram.

Suponhamos que você tenha alguns animais - Cat Birde Dog. Agora, estes animais têm alguns comportamentos comuns - move(), eat()e speak(). Todos comem de maneira diferente e falam de maneira diferente, mas se eu preciso que meu animal coma ou fale, eu realmente não me importo com o que eles fazem.

Mas às vezes eu faço. Eu nunca endureci um gato de guarda ou um pássaro de guarda, mas tenho um cão de guarda. Então, quando alguém invade minha casa, não posso confiar apenas em falar para espantar o intruso. Eu realmente preciso que meu cachorro latir - isso é diferente do que ele fala, que é apenas uma brincadeira.

Então, no código que requer um invasor, eu realmente preciso fazer Dog fido = getDog(); fido.barkMenancingly();

Mas, na maioria das vezes, posso alegremente que qualquer animal faça truques onde mia, mia ou twita por guloseimas. Animal pet = getAnimal(); pet.speak();

Para ser mais concreto, o ArrayList possui alguns métodos que Listnão. Notavelmente trimToSize(). Agora, em 99% dos casos, não nos importamos com isso. A Listquase nunca é suficientemente grande para nós para cuidar. Mas há momentos em que é. Portanto, ocasionalmente, precisamos solicitar especificamente que um ArrayListexecute trimToSize()e reduza sua matriz de apoio.

Observe que isso não precisa ser feito na construção - isso pode ser feito através de um método de retorno ou mesmo de um elenco, se tivermos certeza absoluta.

corsiKa
fonte
Confuso por que isso pode ter um voto negativo. Parece canônico, pessoalmente. Exceto para a idéia de um cão de guarda programável ...
corsiKa
Por que eu tive que ler páginas, para obter a melhor resposta? Classificação é uma merda.
Basilevs
Obrigado pelas palavras amáveis. Existem prós e contras nos sistemas de classificação, e um deles é que respostas posteriores às vezes podem ser deixadas na poeira. Acontece!
precisa saber é o seguinte
2

De um modo geral, você deve usar

var x = new Child(); // C#

ou

auto x = new Child(); // C++

para variáveis ​​locais, a menos que haja um bom motivo para a variável ter um tipo diferente do que é inicializado.

(Aqui, estou ignorando o fato de que o código C ++ provavelmente deve estar usando um ponteiro inteligente. Há casos que não ocorrem com mais de uma linha e sem mais de uma linha.)

A idéia geral aqui é com linguagens que suportam a detecção automática de variáveis ​​de tipo, usando-a para facilitar a leitura do código e faz a coisa certa (ou seja, o sistema de tipos do compilador pode otimizar o máximo possível e alterar a inicialização para ser uma nova tipo funciona bem se a maioria dos abstratos tiver sido usada).

Joshua
fonte
1
Em C ++, as variáveis são principalmente criado sem a nova palavra-chave: stackoverflow.com/questions/6500313/...
Marian Spanik
1
A questão não é sobre C # / C ++, mas sobre um problema geral orientado a objetos em uma linguagem que possui pelo menos alguma forma de digitação estática (ou seja, seria absurdo perguntar algo como Ruby). Além disso, mesmo nessas duas línguas, não vejo realmente uma boa razão para fazê-lo como você diz.
AnoE
1

Bom porque é fácil de entender e modificar este código:

var x = new Child(); 
x.DoSomething();

Bom porque comunica intenção:

Parent x = new Child(); 
x.DoSomething(); 

Ok, porque é comum e fácil de entender:

Child x = new Child(); 
x.DoSomething(); 

A única opção que é realmente ruim é usar Parentse Childtiver apenas um método DoSomething(). É ruim porque comunica intenção incorretamente:

Parent x = new Child(); 
(x as Child).DoSomething(); // DON'T DO THIS! IF YOU WANT x AS CHILD, STORE x AS CHILD

Agora, vamos elaborar um pouco mais sobre o caso em que a pergunta especificamente é feita:

Parent x = new Child(); 
x.DoSomething(); 

Vamos formatar isso de maneira diferente, fazendo na segunda metade uma chamada de função:

WhichType x = new Child(); 
FunctionCall(x);

void FunctionCall(WhichType x)
    x.DoSomething(); 

Isso pode ser reduzido para:

FunctionCall(new Child());

void FunctionCall(WhichType x)
    x.DoSomething();

Aqui, suponho que seja amplamente aceito que WhichTypedeve ser o tipo mais básico / abstrato que permite que a função funcione (a menos que haja problemas de desempenho). Nesse caso, o tipo apropriado seria ou Parent, ou algo Parentderiva.

Essa linha de raciocínio explica por que usar o tipo Parenté uma boa escolha, mas não entra no clima, as outras opções são ruins (não são).

Pedro
fonte
1

tl; dr - O usoChildexcessivoParenté preferível no escopo local. Isso não apenas ajuda na legibilidade, mas também é necessário garantir que a resolução do método sobrecarregado funcione corretamente e ajude a permitir a compilação eficiente.


No escopo local,

Parent obj = new Child();  // Works
Child  obj = new Child();  // Better
var    obj = new Child();  // Best

Conceitualmente, trata-se de manter o máximo de informações de tipo possível. Se fizermos o downgrade para Parent, estamos basicamente retirando informações de tipo que poderiam ter sido úteis.

Manter as informações completas sobre o tipo tem quatro vantagens principais:

  1. Fornece mais informações ao compilador.
  2. Fornece mais informações ao leitor.
  3. Código mais limpo e padronizado.
  4. Torna a lógica do programa mais mutável.

Vantagem 1: Mais informações para o compilador

O tipo aparente é usado na resolução do método sobrecarregado e na otimização.

Exemplo: Resolução de método sobrecarregado

main()
{
    Parent parent = new Child();
    foo(parent);

    Child  child  = new Child();
    foo(child);
}
foo(Parent arg) { /* ... */ }  // More general
foo(Child  arg) { /* ... */ }  // Case-specific optimizations

No exemplo acima, ambas as foo()chamadas funcionam , mas em um caso, obtemos a melhor resolução do método sobrecarregado.

Exemplo: Otimização do Compilador

main()
{
    Parent parent = new Child();
    var x = parent.Foo();

    Child  child  = new Child();
    var y = child .Foo();
}
class Parent
{
    virtual         int Foo() { return 1; }
}
class Child : Parent
{
    sealed override int Foo() { return 2; }
}

No exemplo acima, as duas .Foo()chamadas finalmente chamam o mesmo overridemétodo que retorna 2. Apenas, no primeiro caso, há uma pesquisa de método virtual para encontrar o método correto; essa pesquisa de método virtual não é necessária no segundo caso, uma vez que é desse método sealed.

Agradecemos a @Ben, que forneceu um exemplo semelhante em sua resposta .

Vantagem 2: Mais informações para o leitor

Conhecer o tipo exato, ou seja Child, fornece mais informações para quem está lendo o código, facilitando a visualização do que o programa está fazendo.

Claro, talvez não importa para o código real uma vez que ambos parent.Foo();e child.Foo();faz sentido, mas para alguém ver o código pela primeira vez, mais informações é simplesmente útil.

Além disso, dependendo do seu ambiente de desenvolvimento, o IDE pode ser capaz de fornecer dicas de ferramentas mais úteis e metadados sobre Childa Parent.

Vantagem 3: Código mais limpo e mais padronizado

A maioria dos exemplos de código C # que eu tenho visto ultimamente usa var, o que é basicamente uma abreviação de Child.

Parent obj = new Child();  // Sub-optimal
Child  obj = new Child();  // Optimal, but anti-pattern syntax
var    obj = new Child();  // Optimal, clean, patterned syntax "everyone" uses now

Ver uma vardeclaração de não declaração simplesmente desaparece; se houver uma razão situacional para isso, incrível, mas, caso contrário, parece antipadrão.

// Clean:
var foo1 = new Person();
var foo2 = new Job();
var foo3 = new Residence();

// Staggered:
Person foo1 = new Person();
Job foo2 = new Job();
Residence foo3 = new Residence();   

Vantagem 4: Lógica de programa mais mutável para prototipagem

As três primeiras vantagens foram as grandes; esse é muito mais situacional.

Ainda assim, para pessoas que usam código como outros usam o Excel, estamos constantemente alterando nosso código. Talvez a gente não precisa chamar um método único para Childa presente versão do código, mas pode redirecionar ou refazer o código mais tarde.

A vantagem de um sistema de tipo forte é que ele nos fornece certos metadados sobre a lógica do programa, tornando as possibilidades mais aparentes. Isso é incrivelmente útil na criação de protótipos, por isso é melhor mantê-lo sempre que possível.

Sumário

O uso de Parentbagunças na resolução do método sobrecarregado, inibe algumas otimizações do compilador, remove as informações do leitor e torna o código mais feio.

Usar varé realmente o caminho a percorrer. É rápido, limpo, padronizado e ajuda o compilador e o IDE a fazer seu trabalho corretamente.


Importante : Esta resposta é sobreParentvs.Childno escopo local de um método. A questão deParentvs.Childé muito diferente para tipos de retorno, argumentos e campos de classe.

Nat
fonte
2
Eu acrescentaria que Parent list = new Child()não faz muito sentido. O código que cria diretamente a instância concreta de um objeto (em vez de indireto através de fábricas, métodos de fábrica etc.) possui informações perfeitas sobre o tipo que está sendo criado; pode ser necessário interagir com a interface concreta para configurar o objeto recém-criado etc. O escopo local não é onde você obtém flexibilidade. Flexibilidade é alcançada quando você expõe o objeto recém-criado para um cliente de sua classe usando uma interface mais abstrato (fábricas e métodos de fábrica são um exemplo perfeito)
crizzis
"tl; dr Usar filho sobre pai é preferível no escopo local. Isso não apenas ajuda na legibilidade", - não necessariamente ... se você não usar os detalhes do filho, ele adicionará mais detalhes do que o necessário. "mas também é necessário garantir que a resolução do método sobrecarregado funcione corretamente e ajude a permitir uma compilação eficiente". - isso depende muito da linguagem de programação. Há muitos que não têm nenhum problema decorrente disso.
AnoE
1
Você tem uma fonte que Parent p = new Child(); p.doSomething();e Child c = new Child; c.doSomething();ter um comportamento diferente? Talvez isso seja uma peculiaridade em alguma linguagem, mas é suposto que o objeto controla o comportamento, não a referência. Essa é a beleza da herança! Contanto que você possa confiar nas implementações filhas para cumprir o contrato da implementação pai, você estará pronto.
precisa saber é o seguinte
@corsiKa Sim, é possível de algumas maneiras. Dois exemplos rápidos seriam onde as assinaturas de métodos são substituídas, por exemplo, com a newpalavra - chave em C # , e quando há várias definições, por exemplo, com herança múltipla em C ++ .
Nat
@corsiKa Apenas um terceiro exemplo rápido (porque acho que é um contraste puro entre C ++ e C #), o C # tem um problema como o do C ++, onde você também pode obter esse comportamento usando métodos de interface explícita . É engraçado porque linguagens como C # usam interfaces em vez de herança múltipla em parte para evitar esses problemas, mas ao adotar interfaces, elas ainda fazem parte do problema - mas não são tão ruins porque, nesse cenário, elas se aplicam apenas para quando Parenté um interface.
Nat
0

Dado parentType foo = new childType1();, o código será limitado ao uso de métodos do tipo pai foo, mas poderá armazenar em foouma referência a qualquer objeto cujo tipo deriva de parent- e não apenas de instâncias de childType1. Se a nova childType1instância for a única coisa para a qual foouma referência será mantida, será melhor declarar fooo tipo como childType1. Por outro lado, se for foonecessário manter referências a outros tipos, tê-lo declarado como parentTypetornará isso possível.

supercat
fonte
0

Eu sugiro usar

Child x = new Child();

ao invés de

Parent x = new Child();

O último perde informações enquanto o primeiro não.

Você pode fazer tudo com o primeiro que você pode fazer com o último, mas não vice-versa.


Para elaborar mais o ponteiro, vamos ter vários níveis de herança.

Base-> DerivedL1->DerivedL2

E você deseja inicializar o resultado de new DerivedL2()para uma variável. Usar um tipo de base deixa a opção de usar Baseou DerivedL1.

Você pode usar

Base x = new DerivedL2();

ou

DerivedL1 x = new DerivedL2();

Não vejo nenhuma maneira lógica de preferir uma à outra.

Se você usar

DerivedL2 x = new DerivedL2();

não há o que discutir.

R Sahu
fonte
3
Ser capaz de fazer menos se você não precisa fazer mais é uma coisa boa, não é?
Peter
1
@ Peter, a capacidade de fazer menos não é restrita. Sempre é possível. Se a capacidade de fazer mais for restrita, pode ser um ponto delicado.
R: Sahu
Mas perder informações às vezes é uma coisa boa. Em relação à sua hierarquia, a maioria das pessoas provavelmente votaria Base(supondo que funcione). Você prefere o mais específico, AFAIK a maioria prefere o menos específico. Eu diria que, para variáveis ​​locais, isso pouco importa.
Maaartinus
@maaartinus, estou surpreso que a maioria dos usuários prefira perder informações. Não estou vendo os benefícios de perder informações tão claramente quanto os outros.
precisa
Quando você declara Base x = new DerivedL2();, fica mais restrito e isso permite que você troque outro (neto) filho com o mínimo de esforço. Além disso, você vê imediatamente que é possível. +++ Concordamos que void f(Base)é melhor do que void f(DerivedL2)?
Maaartinus
0

Eu sugeriria sempre retornar ou armazenar variáveis ​​como o tipo mais específico, mas aceitar parâmetros como os tipos mais amplos.

Por exemplo

<K, V> LinkedHashMap<K, V> keyValuePairsToMap(List<K> keys, List<V> values) {
   //...
}

Os parâmetros são List<T>, um tipo muito geral. ArrayList<T>, LinkedList<T>e outros poderiam ser aceitos por esse método.

Importante, o tipo de retorno é LinkedHashMap<K, V>, não Map<K, V>. Se alguém quiser atribuir o resultado desse método a Map<K, V>, ele pode fazer isso. Ele comunica claramente que esse resultado é mais do que apenas um mapa, mas um mapa com uma ordem definida.

Suponha que esse método retorne Map<K, V>. Se o chamador quisesse um LinkedHashMap<K, V>, eles teriam que fazer uma verificação de tipo, conversão e manipulação de erros, o que é realmente complicado.

Alexander - Restabelecer Monica
fonte
A mesma lógica que força você a aceitar um tipo amplo como parâmetro também deve se aplicar ao tipo de retorno. Se o objetivo da função é mapear chaves para valores, ele deve retornar um Mapnão a LinkedHashMap- você só desejaria retornar a LinkedHashMappara uma função como keyValuePairsToFifoMapalgo assim. Mais amplo é melhor até que você tenha um motivo específico para não fazê-lo.
CorsiKa
@corsiKa Hmm, eu concordo que é um nome melhor, mas este é apenas um exemplo. Eu discordo que (em geral) ampliar seu tipo de retorno é melhor. Suas informações de tipo de remoção artificial, sem qualquer justificativa. Se você imagina que seu usuário precisará razoavelmente rejeitar o tipo amplo que você forneceu, então fez um desserviço.
Alexander - Restabelecer Monica
"sem qualquer justificativa" - mas há justificativa! Se eu conseguir retornar um tipo mais amplo, a refatoração será mais fácil no futuro. Se alguém PRECISA do tipo específico por causa de algum comportamento, sou a favor de retornar o tipo apropriado. Mas não quero ser trancado em um tipo específico se não precisar. Eu gostaria de ser livre para alterar as especificidades do meu método sem prejudicar aqueles que consumi-lo, e se eu mudá-lo de, digamos, LinkedHashMappara TreeMap, por qualquer razão, agora eu tenho um monte de coisas para a mudança.
CorsiKa
Na verdade, eu concordo com isso até a última frase. Você alterou de LinkedHashMappara TreeMapnão é uma alteração trivial, como alterar de ArrayListpara LinkedList. É uma mudança com importância semântica. Nesse caso, você deseja que o compilador gere um erro, para forçar os consumidores do valor de retorno a observar a alteração e tomar as medidas apropriadas.
Alexander - Restabelecer Monica
Mas pode ser uma mudança trivial se tudo o que você se importa é com o comportamento do mapa (o que, se você pode, deveria.) Se o contrato for "você receberá um mapa de valores-chave" e a ordem não importa (o que é verdade para na maioria dos mapas), não importa se é um mapa de árvore ou hash. Por exemplo, Thread#getAllStackTraces()- retorna um em Map<Thread,StackTraceElement[]>vez de um HashMapespecificamente tão adiante que eles podem alterá-lo para o tipo Mapque quiserem.
corsiKa