Quando o teste de tipo está OK?

53

Supondo uma linguagem com alguma segurança de tipo inerente (por exemplo, não JavaScript):

Dado um método que aceita a SuperType, sabemos que, na maioria dos casos, podemos ser tentados a executar testes de tipo para escolher uma ação:

public void DoSomethingTo(SuperType o) {
  if (o isa SubTypeA) {
    o.doSomethingA()
  } else {
    o.doSomethingB();
  }
}

Normalmente, se não sempre, devemos criar um método único e substituível no SuperTypee fazer o seguinte:

public void DoSomethingTo(SuperType o) {
  o.doSomething();
}

... em que cada subtipo recebe sua própria doSomething()implementação. O restante de nosso aplicativo pode então ser adequadamente ignorante se qualquer dado SuperTypeé realmente um SubTypeAou um SubTypeB.

Maravilhoso.

Mas ainda temos is aoperações semelhantes na maioria, se não em todas, linguagens de tipo seguro. E isso sugere uma necessidade potencial de teste explícito de tipo.

Então, em que situações, se houver, devemos ou devemos executar testes explícitos de tipo?

Perdoe minha falta de espírito ou falta de criatividade. Eu sei que já fiz isso antes; mas, honestamente, faz muito tempo que não me lembro se o que fiz foi bom! E, na memória recente, acho que não encontrei a necessidade de testar tipos fora do JavaScript do meu cowboy.

svidgen
fonte
11
Leia sobre inferência de tipos e sistemas de tipos
Basile Starynkevitch
4
Vale ressaltar que nem Java nem C # tinham genéricos em sua primeira versão. Você tinha que converter para e de um objeto para usar contêineres.
Doval
6
"Verificação de tipo" quase sempre se refere à verificação se o código observa as regras de digitação estática do idioma. O que você quer dizer é geralmente chamado de teste de tipo .
3
É por isso que gosto de escrever C ++ e deixar o RTTI desativado. Quando alguém literalmente não pode testar tipos de objeto em tempo de execução, força o desenvolvedor a aderir ao bom design de OO com relação à pergunta que está sendo feita aqui.

Respostas:

48

"Nunca" é a resposta canônica para "quando o teste de tipo está bom?" Não há como provar ou refutar isso; faz parte de um sistema de crenças sobre o que faz "bom design" ou "bom design orientado a objetos". Também é hokum.

Para ter certeza, se você tem um conjunto integrado de classes e também mais de uma ou duas funções que precisam desse tipo de teste direto de tipo, provavelmente você ESTÁ FAZENDO ERRADO. O que você realmente precisa é de um método implementado de maneira diferente SuperTypee em seus subtipos. Isso é parte integrante da programação orientada a objetos, e toda a razão pela qual classes e herança existem.

Nesse caso, o teste de tipo explicitamente está errado, não porque o teste de tipo é inerentemente errado, mas porque a linguagem já possui uma maneira limpa, extensível e idiomática de realizar a discriminação de tipo, e você não a usou. Em vez disso, você voltou a um idioma primitivo, frágil e não extensível.

Solução: Use o idioma. Como você sugeriu, adicione um método a cada uma das classes e deixe que a herança padrão e os algoritmos de seleção de método determinem qual caso se aplica. Ou, se você não pode alterar os tipos de base, subclasse e adicione seu método lá.

Tanta coisa para a sabedoria convencional e algumas respostas. Alguns casos em que o teste explícito de tipo faz sentido:

  1. É único. Se você teve muita discriminação de tipo, pode estender os tipos ou subclasses. Mas você não. Você tem apenas um ou dois lugares onde precisa de testes explícitos; portanto, não vale a pena voltar e trabalhar na hierarquia de classes para adicionar as funções como métodos. Ou não vale a pena o esforço prático para adicionar o tipo de generalidade, testes, revisões de projeto, documentação ou outros atributos das classes base para um uso tão simples e limitado. Nesse caso, adicionar uma função que faz testes diretos é racional.

  2. Você não pode ajustar as classes. Você pensa em subclassificar - mas não pode. Muitas classes em Java, por exemplo, são designadas final. Você tenta inserir um public class ExtendedSubTypeA extends SubTypeA {...} e o compilador diz, sem incerteza, que o que você está fazendo não é possível. Desculpe, graça e sofisticação do modelo orientado a objetos! Alguém decidiu que você não pode estender seus tipos! Infelizmente, muitas das bibliotecas padrão são final, e fazer aulas finalé uma orientação comum de design. Uma execução final de função é o que fica disponível para você.

    BTW, isso não se limita aos idiomas estaticamente digitados. Linguagem dinâmica O Python possui várias classes base que, sob as coberturas implementadas em C, não podem realmente ser modificadas. Como o Java, isso inclui a maioria dos tipos padrão.

  3. Seu código é externo. Você está desenvolvendo classes e objetos provenientes de vários servidores de banco de dados, mecanismos de middleware e outras bases de código que não podem ser controladas ou ajustadas. Seu código é apenas um consumidor humilde de objetos gerados em outros lugares. Mesmo se você pudesse subclasses SuperType, não poderá obter as bibliotecas das quais depende para gerar objetos em suas subclasses. Eles vão fornecer instâncias dos tipos que eles conhecem, não suas variantes. Isso nem sempre é o caso ... algumas vezes eles são criados para oferecer flexibilidade e instanciam dinamicamente instâncias de classes que você as alimenta. Ou eles fornecem um mecanismo para registrar as subclasses que você deseja que suas fábricas construam. Os analisadores XML parecem particularmente bons em fornecer esses pontos de entrada; veja por exemplo ou lxml em Python . Mas a maioria das bases de código não fornece essas extensões. Eles vão lhe devolver as aulas com as quais foram construídas e as que conhecem. Geralmente, não faz sentido proxyizar seus resultados nos resultados personalizados, apenas para que você possa usar um seletor de tipo puramente orientado a objetos. Se você vai fazer discriminação de tipo, precisará fazê-lo de forma relativamente grosseira. Seu código de teste de tipo parece bastante apropriado.

  4. Genéricos da pessoa pobre / despacho múltiplo. Você deseja aceitar uma variedade de tipos diferentes para o seu código e acha que ter uma matriz de métodos muito específicos do tipo não é agradável. public void add(Object x)Parece lógico, mas não um conjunto de addByte, addShort, addInt, addLong, addFloat, addDouble, addBoolean, addChar, e addStringvariantes (para citar alguns). Tendo funções ou métodos que assumem um supertipo alto e determinam o que fazer, tipo a tipo - eles não ganharão o Purity Award no Simpósio anual de design Booch-Liskov, mas abandonarão o A nomeação húngara fornecerá uma API mais simples. De certo modo, você is-aouis-instance-of o teste está simulando genérico ou multi-despacho em um contexto de linguagem que não o suporta nativamente.

    O suporte a idiomas integrados para genéricos e digitação de pato reduz a necessidade de verificação de tipo, tornando mais provável "fazer algo elegante e apropriado". A seleção múltipla de despacho / interface vista em idiomas como Julia e Go substituem similarmente o teste direto de tipo por mecanismos internos para a seleção baseada em tipo de "o que fazer". Mas nem todos os idiomas suportam isso. Java, por exemplo, geralmente é de despacho único, e seus idiomas não são super amigáveis ​​para se digitar.

    Mas mesmo com todos esses recursos de discriminação de tipo - herança, genéricos, tipagem de pato e despacho múltiplo - às vezes é conveniente ter uma rotina única e consolidada que faz com que você esteja fazendo algo com base no tipo do objeto claro e imediato. Na metaprogramação , achei essencialmente inevitável. Se o retorno às perguntas diretas do tipo constitui "pragmatismo em ação" ou "codificação suja" dependerá da sua filosofia e crenças de design.

Jonathan Eunice
fonte
Se uma operação não puder ser executada em um tipo específico, mas puder ser executada em dois tipos diferentes nos quais seria implicitamente conversível, a sobrecarga será adequada apenas se ambas as conversões produzirem resultados idênticos. Caso contrário, acabamos com comportamentos ruins como no .NET: (1.0).Equals(1.0f)produzindo true [o argumento promove double], mas (1.0f).Equals(1.0)produzindo false [o argumento promove object]; em Java, Math.round(123456789*1.0)gera 123456789, mas Math.round(123456789*1.0)gera 123456792 [o argumento promove ao floatinvés de double].
Supercat
Esse é o argumento clássico contra a conversão / escalação automática de tipos. Resultados ruins e paradoxais, pelo menos em casos extremos. Eu concordo, mas não tenho certeza de como você pretende relacionar com a minha resposta.
Jonathan Eunice
Eu estava respondendo ao seu ponto 4, que parecia defender a sobrecarga, em vez de usar métodos de nomes diferentes com tipos diferentes.
Supercat
2
@supercat Responsabilize minha visão fraca, mas as duas expressões Math.roundparecem idênticas para mim. Qual é a diferença?
Lily Chung
2
@IstvanChung: Opa ... este último deve ter sido Math.round(123456789)[indicativo do que pode acontecer se alguém reescritas Math.round(thing.getPosition() * COUNTS_PER_MIL)para retornar um valor de posição sem escala, não percebendo que getPositionretorna um intou long.]
supercat
25

A principal situação que eu sempre precisei foi ao comparar dois objetos, como em um equals(other)método, que podem exigir algoritmos diferentes, dependendo do tipo exato de other. Mesmo assim, é bastante raro.

A outra situação que tive, novamente, muito raramente, é após a desserialização ou análise, onde às vezes você precisa que ele seja convertido com segurança para um tipo mais específico.

Além disso, às vezes você só precisa de um hack para contornar o código de terceiros que você não controla. É uma daquelas coisas que você realmente não deseja usar regularmente, mas está feliz por estar lá quando você realmente precisa.

Karl Bielefeldt
fonte
11
Fiquei momentaneamente encantado com o caso de desserialização, por ter me lembrado falsamente de usá-lo ali; mas agora não consigo imaginar como teria! Eu sei que fiz algumas pesquisas de tipo estranhas para isso; mas não tenho certeza se isso constitui teste de tipo . Talvez a serialização seja um paralelo mais próximo: ter que interrogar objetos por seu tipo concreto.
svidgen
11
A serialização é geralmente possível usando polimorfismo. Ao desserializar, geralmente você faz algo como BaseClass base = deserialize(input), porque ainda não conhece o tipo, e if (base instanceof Derived) derived = (Derived)basearmazena-o como seu tipo derivado exato.
Karl Bielefeldt
11
Igualdade faz sentido. Na minha experiência, esses métodos geralmente têm uma forma como “se esses dois objetos tiverem o mesmo tipo concreto, retorne se todos os seus campos são iguais; caso contrário, retorne false (ou incomparável) ”.
Jon Purdy
2
Em particular, o fato de você se encontrar testando é um bom indicador de que as comparações polimórficas de igualdade são um ninho de víboras.
Steve Jessop
12

O caso padrão (mas espero que raro) se parece com o seguinte: se na seguinte situação

public void DoSomethingTo(SuperType o) {
  if (o isa SubTypeA) {
    DoSomethingA((SubTypeA) o )
  } else {
    DoSomethingB((SubTypeB) o );
  }
}

as funções DoSomethingAou DoSomethingBnão podem ser facilmente implementadas como funções-membro da árvore de herança de SuperType/ SubTypeA/ SubTypeB. Por exemplo, se

  • os subtipos fazem parte de uma biblioteca que você não pode alterar ou
  • se adicionar o código DoSomethingXXXa essa biblioteca significaria introduzir uma dependência proibida.

Observe que geralmente há situações em que você pode contornar esse problema (por exemplo, criando um wrapper ou adaptador para SubTypeAe SubTypeBou tentando DoSomethingreimplementar completamente em termos de operações básicas de SuperType), mas às vezes essas soluções não valem o aborrecimento ou coisas mais complicadas e menos extensíveis do que fazer o teste de tipo explícito.

Um exemplo do meu trabalho de ontem: eu tive uma situação em que eu iria paralelizar o processamento de uma lista de objetos (do tipo SuperType, com exatamente dois subtipos diferentes, onde é extremamente improvável que haja mais). A versão incomparável continha dois loops: um loop para objetos do subtipo A, chamado DoSomethingA, e um segundo loop para objetos do subtipo B, chamado DoSomethingB.

Os métodos "DoSomethingA" e "DoSomethingB" são cálculos demorados, usando informações de contexto que não estão disponíveis no escopo dos subtipos A e B. (portanto, não faz sentido implementá-los como funções-membro dos subtipos). Do ponto de vista do novo "loop paralelo", torna as coisas muito mais fáceis ao lidar com elas de maneira uniforme, então implementei uma função semelhante à DoSomethingTode cima. No entanto, examinar as implementações de "DoSomethingA" e "DoSomethingB" mostra que elas funcionam de maneira muito diferente internamente. Portanto, tentar implementar um "DoSomething" genérico, estendendo-se SuperTypecom muitos métodos abstratos, não iria realmente funcionar, ou significaria projetar as coisas completamente.

Doc Brown
fonte
2
Alguma chance de você poder adicionar um pequeno exemplo concreto para convencer meu cérebro nebuloso de que esse cenário não é artificial?
svidgen
Para ser claro, não estou dizendo que é artificial. Suspeito que estou supremamente covarde hoje.
svidgen
@svidgen: isso está longe de ser artificial, na verdade eu encontrei essa situação hoje (embora não possa postar este exemplo aqui porque ele contém informações internas de negócios). E eu concordo com as outras respostas que o uso do operador "is" deve ser uma exceção e somente é feito em casos raros.
Doc Brown
Eu acho que sua edição, que eu ignorei, dá um bom exemplo. ... Existem casos em que este é OK, mesmo quando você fazer o controle SuperTypee é subclasses?
svidgen
6
Aqui está um exemplo concreto: o contêiner de nível superior em uma resposta JSON de um serviço da Web pode ser um dicionário ou uma matriz. Normalmente, você tem alguma ferramenta que transforma o JSON em objetos reais (por exemplo, NSJSONSerializationno Obj-C), mas não deseja simplesmente confiar que a resposta contém o tipo que você espera; portanto, antes de usá-lo, verifique-o (por exemplo if ([theResponse isKindOfClass:[NSArray class]])...) .
Caleb
5

Como o tio Bob chama:

When your compiler forgets about the type.

Em um de seus episódios de Clean Coder, ele deu um exemplo de uma chamada de função usada para retornar Employees. Manageré um subtipo de Employee. Vamos supor que temos um serviço de aplicativo que aceita um ManagerID e o convoca para o escritório :) A função getEmployeeById()retorna um supertipo Employee, mas quero verificar se um gerente é retornado nesse caso de uso.

Por exemplo:

var manager = employeeRepository.getEmployeeById(empId);
if (!(manager is Manager))
   throw new Exception("Invalid Id specified.");
manager.summon();

Aqui, estou verificando se o funcionário retornado pela consulta é realmente um gerente (ou seja, espero que seja um gerente e, caso contrário, falhe rapidamente).

Não é o melhor exemplo, mas é tio Bob, afinal.

Atualizar

Atualizei o exemplo o máximo que me lembro de memória.

Songo
fonte
11
Por que Managera implementação de summon()apenas não lança a exceção neste exemplo?
svidgen
@svidgen talvez o CEOpode convocar Managers.
user253751
@svidgen, Em seguida, ele não seria tão claro que employeeRepository.getEmployeeById (EmpID) é esperado para retornar um gerente
Ian
@ Ian Não vejo isso como um problema. Se o código de chamada estiver solicitando um Employee, ele deve se preocupar apenas em obter algo que se comporte como um Employee. Se diferentes subclasses de Employeetêm permissões, responsabilidades, etc. diferentes, o que torna o teste de tipo uma opção melhor do que um sistema de permissões real?
svidgen
3

Quando a verificação de tipo está OK?

Nunca.

  1. Ao ter o comportamento associado a esse tipo em alguma outra função, você está violando o Princípio Aberto Fechado , porque pode modificar o comportamento existente do tipo alterando a iscláusula, ou (em alguns idiomas ou dependendo do cenário) porque você não pode estender o tipo sem modificar as partes internas da função fazendo a isverificação.
  2. Mais importante, as isverificações são um forte sinal de que você está violando o Princípio de Substituição de Liskov . Qualquer coisa que funcione SuperTypedeve ser completamente ignorante de quais subtipos podem existir.
  3. Você está associando implicitamente algum comportamento ao nome do tipo. Isso torna seu código mais difícil de manter, porque esses contratos implícitos estão espalhados por todo o código e não garantem a aplicação universal e consistente como os membros reais da classe.

Tudo isso dito, os ischeques podem ser menos ruins do que outras alternativas. Colocar todas as funcionalidades comuns em uma classe base é difícil e muitas vezes leva a problemas piores. Usar uma classe única que tenha uma flag ou enum para qual "tipo" a instância é ... é pior do que horrível, já que agora você está espalhando a evasão do sistema de tipos para todos os consumidores.

Em resumo, você deve sempre considerar as verificações de tipo como um forte cheiro de código. Mas, como em todas as diretrizes, haverá momentos em que você será forçado a escolher entre qual violação de diretrizes é a menos ofensiva.

Telastyn
fonte
3
Há uma pequena advertência: implementar tipos de dados algébricos em idiomas que não os suportam. No entanto, o uso de herança e datilografia é puramente um detalhe de implementação hacky; a intenção não é introduzir subtipos, mas categorizar valores. Trago isso à tona apenas porque os ADTs são úteis e nunca são um qualificador muito forte, mas, caso contrário, concordo plenamente; instanceofvaza detalhes de implementação e quebra a abstração.
Doval
17
"Nunca" é uma palavra que eu realmente não gosto nesse contexto, especialmente quando contradiz o que você escreve abaixo.
Doc Brown
10
Se por "nunca" você realmente quer dizer "às vezes", então você está certo.
Caleb em
2
OK significa permitido, aceitável etc., mas não necessariamente ideal ou ideal. Contraste com o que não está bom : se algo não estiver bem , você não deve fazê-lo. Como você indica, a necessidade de verificar o tipo de algo pode ser uma indicação de problemas mais profundos no código, mas há momentos em que é a opção mais conveniente e menos ruim e, nessas situações, é obviamente bom usar as ferramentas em seu disposição. (Se fosse fácil evitá-los em todas as situações, eles provavelmente não estariam lá em primeiro lugar.) A questão se resume a identificar essas situações e nunca ajuda.
Caleb
2
@ supercat: Um método deve exigir o tipo menos específico possível que satisfaça seus requisitos . IEnumerable<T>não promete que um "último" elemento exista. Se o seu método precisar desse elemento, ele deverá exigir um tipo que garanta a existência de um. E os subtipos desse tipo podem fornecer implementações eficientes do método "Last".
Chao
2

Se você possui uma grande base de código (mais de 100 mil linhas de código) e está perto do envio ou está trabalhando em uma filial que posteriormente precisará ser mesclada, há, portanto, muito custo / risco para alterar muitas situações.

Às vezes, você tem a opção de um grande refrator do sistema ou de um simples "teste de tipo" localizado. Isso cria uma dívida técnica que deve ser paga o mais rápido possível, mas geralmente não é.

(É impossível criar um exemplo, pois qualquer código pequeno o suficiente para ser usado como exemplo também é pequeno o suficiente para que o melhor design seja claramente visível.)

Ou, em outras palavras, quando o objetivo é receber seu salário em vez de obter "votos positivos" pela limpeza do seu projeto.


O outro caso comum é o código da interface do usuário, quando, por exemplo, você mostra uma interface do usuário diferente para alguns tipos de funcionários, mas claramente não deseja que os conceitos da interface do usuário escapem para todas as suas classes de "domínio".

Você pode usar o "teste de tipo" para decidir qual versão da interface do usuário exibir ou ter uma tabela de pesquisa sofisticada que converte de "classes de domínio" em "classes de interface do usuário". A tabela de pesquisa é apenas uma maneira de ocultar o "teste de tipo" em um só lugar.

(O código de atualização do banco de dados pode ter os mesmos problemas que o código da interface do usuário, no entanto, você costuma ter apenas um conjunto de códigos de atualização do banco de dados, mas pode ter várias telas diferentes que precisam se adaptar ao tipo de objeto que está sendo mostrado.)

Ian
fonte
O padrão de visitante geralmente é uma boa maneira de resolver seu caso de interface do usuário.
Ian Goldby
@IanGoldby, concordou que às vezes pode ser, no entanto, você ainda está fazendo "testes de tipo", apenas um pouco escondido.
Ian
Escondido no mesmo sentido que está oculto quando você chama um método virtual comum? Ou você quer dizer outra coisa? O Padrão de Visitante que eu usei não possui instruções condicionais que dependem do tipo. Tudo é feito pela linguagem.
Ian Goldby
@IanGoldby, eu quis dizer oculto no sentido de que isso pode dificultar a compreensão do código WPF ou WinForms. Espero que, para algumas UIs da base da Web, funcione muito bem.
Ian
2

A implementação do LINQ usa muita verificação de tipo para possíveis otimizações de desempenho e, em seguida, um fallback para IEnumerable.

O exemplo mais óbvio é provavelmente o método ElementAt (pequeno trecho da fonte .NET 4.5):

public static TSource ElementAt<TSource>(this IEnumerable<TSource> source, int index) { 
    IList<TSource> list = source as IList<TSource>;

    if (list != null) return list[index];
    // ... and then an enumerator is created and MoveNext is called index times

Mas há muitos lugares na classe Enumerable onde um padrão semelhante é usado.

Portanto, talvez otimizar o desempenho para um subtipo comumente usado seja um uso válido. Não tenho certeza de como isso poderia ter sido projetado melhor.

Menno van den Heuvel
fonte
Poderia ter sido projetado melhor, fornecendo um meio conveniente pelo qual as interfaces podem fornecer implementações padrão e, em seguida, IEnumerable<T>incluindo muitos métodos, como os de List<T>, juntamente com uma Featurespropriedade que indica quais métodos podem funcionar bem, devagar ou não, bem como várias suposições que um consumidor pode fazer com segurança sobre a coleção (por exemplo, é garantido que seu tamanho e / ou seu conteúdo existente nunca será alterado [um tipo pode suportar, Addenquanto ainda garante que o conteúdo existente seria imutável]).
Supercat #
Exceto nos cenários em que um tipo pode precisar conter uma de duas coisas completamente diferentes em momentos diferentes e os requisitos são claramente mutuamente exclusivos (e, portanto, o uso de campos separados seria redundante), geralmente considero a necessidade de try-casting ser um sinal que os membros que deveriam fazer parte de uma interface base não eram. Isso não quer dizer que o código que usa uma interface que deveria ter incluído alguns membros, mas não deve usar try-casting para contornar a omissão da interface de base, mas essas interfaces de base de escrita devem minimizar a necessidade de try-cast dos clientes.
Supercat 10/14 /
1

Há um exemplo que aparece frequentemente no desenvolvimento de jogos, especificamente na detecção de colisões, que é difícil de lidar sem o uso de alguma forma de teste de tipo.

Suponha que todos os objetos do jogo derivem de uma classe base comum GameObject. Cada objecto tem uma forma de colisão corpo rígido CollisionShapeo qual pode fornecer uma interface comum (para dizer posição de consulta, orientação, etc), mas as formas de colisão reais serão todas as subclasses de betão, tais como Sphere, Box, ConvexHull, etc armazenar informação específica para esse tipo de objecto geométrico (veja aqui um exemplo real)

Agora, para testar uma colisão, preciso escrever uma função para cada par de tipos de forma de colisão:

detectCollision(Sphere, Sphere)
detectCollision(Sphere, Box)
detectCollision(Sphere, ConvexHull)
detectCollision(Box, ConvexHull)
...

que contêm a matemática específica necessária para executar uma interseção desses dois tipos geométricos.

Em cada 'tick' do meu loop de jogo, preciso verificar pares de objetos em busca de colisões. Mas eu só tenho acesso a se GameObjectseus CollisionShapes correspondentes . Claramente, preciso conhecer tipos concretos para saber qual função de detecção de colisão chamar. Nem mesmo o envio duplo (que logicamente não é diferente de verificar o tipo) pode ajudar aqui *.

Na prática, nessa situação, os mecanismos de física que eu vi (Bullet e Havok) dependem de testes de tipo de uma forma ou de outra.

Não estou dizendo que essa é necessariamente uma boa solução, é apenas que pode ser a melhor de um pequeno número de soluções possíveis para esse problema

* Tecnicamente, é possível usar o despacho duplo de uma maneira horrenda e complicada que exigiria combinações N (N + 1) / 2 (onde N é o número de tipos de formas que você possui) e apenas ofuscaria o que você está realmente fazendo. é descobrir simultaneamente os tipos das duas formas, por isso não considero que seja uma solução realista.

Martin
fonte
1

Às vezes, você não deseja adicionar um método comum a todas as classes, porque realmente não é responsabilidade deles executar essa tarefa específica.

Por exemplo, você deseja desenhar algumas entidades, mas não deseja adicionar o código de desenho diretamente a elas (o que faz sentido). Em idiomas que não oferecem suporte a vários despachos, você pode acabar com o seguinte código:

void DrawEntity(Entity entity) {
    if (entity instanceof Circle) {
        DrawCircle((Circle) entity));
    else if (entity instanceof Rectangle) {
        DrawRectangle((Rectangle) entity));
    } ...
}

Isso se torna problemático quando esse código aparece em vários locais e você precisa modificá-lo em qualquer lugar ao adicionar um novo tipo de entidade. Se for esse o caso, pode ser evitado usando o padrão Visitor, mas, às vezes, é melhor manter as coisas simples e não fazer o overengineer. Essas são as situações em que o teste de tipo está OK.

Honza Brabec
fonte
0

A única vez que uso é em combinação com a reflexão. Mas mesmo assim a verificação dinâmica é principalmente, não codificada para uma classe específica (ou apenas codificada para classes especiais como Stringou List).

Por verificação dinâmica, quero dizer:

boolean checkType(Type type, Object object) {
    if (object.isOfType(type)) {

    }
}

e não codificado

boolean checkIsManaer(Object object) {
    if (object instanceof Manager) {

    }
}
m3th0dman
fonte
0

Teste de tipo e conversão de tipo são dois conceitos muito próximos. Tão intimamente relacionado que me sinto confiante em dizer que você nunca deve fazer um teste de tipo, a menos que sua intenção seja digitar o objeto com base no resultado.

Quando você pensa no design ideal orientado a objetos, o teste de tipo (e a conversão) nunca deve acontecer. Mas, esperançosamente, até agora você já descobriu que a programação orientada a objetos não é o ideal. Às vezes, especialmente com código de nível inferior, o código não pode permanecer fiel ao ideal. Este é o caso de ArrayLists em Java; como eles não sabem em tempo de execução que classe está sendo armazenada na matriz, eles criam Object[]matrizes e as convertem estaticamente no tipo correto.

Tem sido apontado que uma necessidade comum de teste de tipo (e conversão de tipo) vem do Equalsmétodo, que na maioria dos idiomas deve ser simples Object. A implementação deve ter algumas verificações detalhadas para verificar se os dois objetos são do mesmo tipo, o que exige ser capaz de testar qual o tipo.

O teste de tipo também surge com frequência na reflexão. Freqüentemente, você terá métodos que retornam Object[]ou alguma outra matriz genérica e deseja retirar todos os Fooobjetos por qualquer motivo. Este é um uso perfeitamente legítimo de teste e conversão de tipo.

Em geral, o teste de tipo é ruim quando associa desnecessariamente seu código à forma como uma implementação específica foi gravada. Isso pode facilmente levar à necessidade de um teste específico para cada tipo ou combinação de tipos, como se você deseja encontrar a interseção de linhas, retângulos e círculos, e a função de interseção possui um algoritmo diferente para cada combinação. Seu objetivo é colocar todos os detalhes específicos de um tipo de objeto no mesmo local que esse objeto, pois isso facilitará a manutenção e a extensão do seu código.

meustrus
fonte
11
ArrayListsnão conhece a classe que está sendo armazenada em tempo de execução porque o Java não tinha genéricos e, quando foram introduzidos, a Oracle optou pela compatibilidade com versões anteriores com código sem genéricos. equalstem o mesmo problema e é uma decisão questionável do projeto; comparações de igualdade não fazem sentido para todos os tipos.
Doval
11
Tecnicamente, as coleções Java não convertem seu conteúdo para nada. O compilador injeta tipografias no local de cada acesso: por exemploString x = (String) myListOfStrings.get(0)
A última vez que olhei para a fonte Java (que pode ter sido 1.6 ou 1.5), houve uma conversão explícita na fonte ArrayList. Ele gera um aviso do compilador (suprimido) por um bom motivo, mas é permitido de qualquer maneira. Suponho que você possa dizer que, devido à forma como os genéricos são implementados, sempre foi Objectaté que fosse acessado; Os genéricos em Java fornecem apenas a conversão implícita, protegida pelas regras do compilador.
meustrus
0

É aceitável em um caso em que você precise tomar uma decisão que envolva dois tipos e essa decisão seja encapsulada em um objeto fora da hierarquia de tipos. Por exemplo, digamos que você esteja planejando qual objeto será processado a seguir em uma lista de objetos aguardando processamento:

abstract class Vehicle
{
    abstract void Process();
}

class Car : Vehicle { ... }
class Boat : Vehicle { ... }
class Truck : Vehicle { ... }

Agora, digamos que nossa lógica de negócios seja literalmente "todos os carros têm precedência sobre barcos e caminhões". A adição de uma Prioritypropriedade à classe não permite que você expresse essa lógica de negócios de maneira limpa, pois você terminará com isso:

abstract class Vehicle
{
    abstract void Process();
    abstract int Priority { get }
}

class Car : Vehicle { public Priority { get { return 1; } } ... }
class Boat : Vehicle { public Priority { get { return 2; } } ... }
class Truck : Vehicle { public Priority { get { return 2; } } ... }

O problema é que agora, para entender a ordem de prioridade, é necessário examinar todas as subclasses ou, em outras palavras, você adicionou o acoplamento às subclasses.

Obviamente, você deve transformar as prioridades em constantes e colocá-las em uma classe, o que ajuda a manter o agendamento da lógica de negócios:

static class Priorities
{
    public const int CAR_PRIORITY = 1;
    public const int BOAT_PRIORITY = 2;
    public const int TRUCK_PRIORITY = 2;
}

No entanto, na realidade, o algoritmo de agendamento é algo que pode mudar no futuro e, eventualmente, pode depender de mais do que apenas digitar. Por exemplo, pode-se dizer que "caminhões com um peso de 5000 kg têm prioridade especial sobre todos os outros veículos". É por isso que o algoritmo de agendamento pertence à sua própria classe, e é uma boa ideia inspecionar o tipo para determinar qual deles deve seguir primeiro:

class VehicleScheduler : IScheduleVehicles
{
    public Vehicle WhichVehicleGoesFirst(Vehicle vehicle1, Vehicle vehicle2)
    {
        if(vehicle1 is Car) return vehicle1;
        if(vehicle2 is Car) return vehicle2;
        return vehicle1;
    }
}

Essa é a maneira mais direta de implementar a lógica de negócios e ainda a mais flexível para futuras mudanças.

Scott Whitlock
fonte
Não concordo com o seu exemplo em particular, mas concordaria com o princípio de ter uma referência particular que pode conter tipos diferentes com significados diferentes. O que eu sugeriria como um exemplo melhor seria um campo que pode conter null, a Stringou a String[]. Se 99% dos objetos precisarem de exatamente uma sequência, encapsular cada sequência em um construído separadamente String[]poderá adicionar uma sobrecarga considerável de armazenamento. O tratamento do caso de cadeia única usando uma referência direta a Stringexigirá mais código, mas economizará armazenamento e poderá tornar as coisas mais rápidas.
Supercat
0

O teste de tipo é uma ferramenta, use-o com sabedoria e pode ser um aliado poderoso. Use mal e seu código começará a cheirar.

Em nosso software, recebemos mensagens pela rede em resposta a solicitações. Todas as mensagens desserializadas compartilhavam uma classe base comum Message.

As classes em si eram muito simples, apenas a carga útil como as propriedades e rotinas C # digitadas para ordenar e remover a ordenação (na verdade, eu gerei a maioria das classes usando modelos t4 da descrição XML do formato da mensagem)

Código seria algo como:

Message response = await PerformSomeRequest(requestParameter);

// Server (out of our control) would send one message as response, but 
// the actual message type is not known ahead of time (it depended on 
// the exact request and the state of the server etc.)
if (response is ErrorMessage)
{ 
    // Extract error message and pass along (for example using exceptions)
}
else if (response is StuffHappenedMessage)
{
    // Extract results
}
else if (response is AnotherThingHappenedMessage)
{
    // Extract another type of result
}
// Half a dozen other possible checks for messages

Concedido, alguém poderia argumentar que a arquitetura da mensagem poderia ser melhor projetada, mas foi projetada há muito tempo e não para C #, por isso é o que é. Aqui, o teste de tipo resolveu um problema real para nós de uma maneira não muito pobre.

Vale ressaltar que o C # 7.0 está obtendo uma correspondência de padrões (que em muitos aspectos é o teste de tipo com esteróides) não pode ser tão ruim ...

Isak Savo
fonte
0

Faça um analisador JSON genérico. O resultado de uma análise bem-sucedida é uma matriz, um dicionário, uma sequência, um número, um valor booleano ou nulo. Pode ser qualquer um desses. E os elementos de uma matriz ou os valores em um dicionário podem ser novamente qualquer um desses tipos. Como os dados são fornecidos de fora do seu programa, você deve aceitar qualquer resultado (ou seja, você deve aceitá-lo sem travar; você pode rejeitar um resultado que não é o que você espera).

gnasher729
fonte
Bem, alguns desserializadores JSON tentarão instanciar uma árvore de objetos se sua estrutura tiver informações de tipo para "campos de inferência" nela. Mas sim. Eu acho que esta é a direção em que a resposta de Karl B foi encaminhada.
svidgen 20/12/16