O que é um uso adequado de downcasting?

68

Downcasting significa conversão de uma classe base (ou interface) para uma subclasse ou folha.

Um exemplo de downcast pode ser se você transmitir System.Objectpara outro tipo.

O downcasting é impopular, talvez um cheiro de código: a doutrina orientada a objetos é preferir, por exemplo, definir e chamar métodos virtuais ou abstratos em vez do downcasting.

  • Quais são, se houver, casos de uso adequados e adequados para downcasting? Ou seja, em que circunstância (s) é apropriado escrever código que faça downcasts?
  • Se sua resposta for "nenhuma", por que o downcast é suportado pelo idioma?
ChrisW
fonte
5
O que você descreve é ​​geralmente chamado de "downcasting". Armado com esse conhecimento, você poderia primeiro pesquisar um pouco mais e explicar por que as razões existentes que você achou não são suficientes? TL; DR: o downcasting quebra a digitação estática, mas às vezes um sistema de tipos é muito restritivo. Portanto, é sensato que um idioma ofereça downcast seguro.
26618 amon
Você quer dizer downcasting? Upcasting seria através da hierarquia de classes, ou seja, de subclasse a superclasse, ao contrário de downcasting, de superclasse de subclasse ...
Andrei Socaciu
Minhas desculpas: você está certo. Editei a pergunta para renomear "upcast" para "downcast".
26418 ChrisW
@amon en.wikipedia.org/wiki/Downcasting fornece "converter para string" como um exemplo (que o .Net suporta usando um virtual ToString); seu outro exemplo é "contêineres Java" porque o Java não suporta genéricos (o que o C # faz). stackoverflow.com/questions/1524197/downcast-and-upcast diz o que é downcasting , mas sem exemplos de quando é apropriado .
26418 ChrisW
4
@ Chrishr Isso before Java had support for genericsnão é because Java doesn't support generics. E amon deu a resposta - quando o sistema de tipos com o qual você trabalha é muito restritivo e você não está em condições de alterá-lo.
Ordous

Respostas:

12

Aqui estão alguns usos adequados de downcasting.

E eu respeitosamente discordo veementemente de outras pessoas aqui que dizem que o uso de downcasts é definitivamente um cheiro de código, porque acredito que não há outra maneira razoável de resolver os problemas correspondentes.

Equals:

class A
{
    // Nothing here, but if you're a C++ programmer who dislikes object.Equals(object),
    // pretend it's not there and we have abstract bool A.Equals(A) instead.
}
class B : A
{
    private int x;
    public override bool Equals(object other)
    {
        var casted = other as B;  // cautious downcast (dynamic_cast in C++)
        return casted != null && this.x == casted.x;
    }
}

Clone:

class A : ICloneable
{
    // Again, if you dislike ICloneable, that's not the point.
    // Just ignore that and pretend this method returns type A instead of object.
    public virtual object Clone()
    {
        return this.MemberwiseClone();  // some sane default behavior, whatever
    }
}
class B : A
{
    private int[] x;
    public override object Clone()
    {
        var copy = (B)base.Clone();  // known downcast (static_cast in C++)
        copy.x = (int[])this.x.Clone();  // oh hey, another downcast!!
        return copy;
    }
}

Stream.EndRead/Write:

class MyStream : Stream
{
    private class AsyncResult : IAsyncResult
    {
        // ...
    }
    public override int EndRead(IAsyncResult other)
    {
        return Blah((AsyncResult)other);  // another downcast (likely ~static_cast)
    }
}

Se você quiser dizer que são odores de código, precisará fornecer soluções melhores para eles (mesmo que sejam evitados devido a inconveniências). Não acredito que existam melhores soluções.

Mehrdad
fonte
9
"Cheiro de código" não significa "este design é definitivamente ruim ", significa "este design é suspeito ". O fato de existirem recursos de linguagem que são inerentemente suspeitos é uma questão de pragmatismo por parte do autor do idioma.
Caleth
5
@Caleth: E meu ponto principal é que esses designs surgem o tempo todo, e que não há absolutamente nada de inerentemente suspeito nos designs que eu mostrei, e, portanto, o elenco não é um cheiro de código.
Mehrdad 27/02
4
@Caleth eu discordo. O cheiro do código, para mim, significa que o código está incorreto ou, no mínimo, pode ser melhorado.
27518 Konrad Rudolph
6
@Mehrdad Sua opinião parece ser que o cheiro do código deve ser culpa dos programadores de aplicativos. Por quê? Nem todo cheiro de código precisa ser um problema real ou ter uma solução. Um cheiro de código de acordo com Martin Fowler é simplesmente "uma indicação de superfície que geralmente corresponde a um problema mais profundo do sistema" object.Equalsse encaixa muito bem nessa descrição (tente fornecer corretamente um contrato igual em uma superclasse que permita que subclasses o implementem corretamente - é absolutamente não trivial). Que como programadores de aplicativos não podemos resolvê-lo (em alguns casos, podemos evitá-lo) é um tópico diferente.
Voo 27/02
5
@Mehrdad Bem, vamos ver o que um dos designers de idiomas originais tem a dizer sobre o Equals ... Object.Equals is horrid. Equality computation in C# is completely messed up(comentário de Eric sobre sua resposta). É engraçado como você não quer reconsiderar sua opinião, mesmo quando um dos principais designers da linguagem diz que você está errado.
Voo 28/02
136

Downcasting é impopular, talvez um cheiro de código

Discordo. Downcasting é extremamente popular ; um grande número de programas do mundo real contém um ou mais downcasts. E talvez não seja um cheiro de código. Definitivamente, é um cheiro de código. É por isso que a operação de downcast deve se manifestar no texto do programa . É para que você possa notar mais facilmente o cheiro e passar a atenção da revisão do código.

em que circunstância (s) é apropriado escrever código que faça downcasts?

Em qualquer circunstância em que:

  • você tem um conhecimento 100% correto de um fato sobre o tipo de tempo de execução de uma expressão que é mais específico que o tipo de expressão em tempo de compilação e
  • você precisa aproveitar esse fato para usar um recurso do objeto não disponível no tipo de tempo de compilação e
  • é melhor usar o tempo e o esforço para escrever o elenco do que refatorar o programa para eliminar qualquer um dos dois primeiros pontos.

Se você pode refatorar um programa de forma barata para que o tipo de tempo de execução possa ser deduzido pelo compilador ou refatorar o programa para que você não precise da capacidade do tipo mais derivado, faça-o. Os downcasts foram adicionados ao idioma nas circunstâncias em que é difícil e caro refatorar o programa.

por que o downcasting é suportado pelo idioma?

O C # foi inventado por programadores pragmáticos que têm trabalhos a fazer, para programadores pragmáticos que têm trabalhos a fazer. Os designers de C # não são puristas de OO. E o sistema do tipo C # não é perfeito; por design, subestima as restrições que podem ser colocadas no tipo de tempo de execução de uma variável.

Além disso, o downcasting é muito seguro em c #. Temos uma forte garantia de que o downcast será verificado no tempo de execução e, se não puder ser verificado, o programa fará a coisa certa e trava. Isso é maravilhoso; significa que, se seu entendimento 100% correto da semântica de tipos estiver 99,99% correto, seu programa funcionará 99,99% do tempo e trava o resto do tempo, em vez de se comportar de forma imprevisível e corromper os dados do usuário em 0,01% .


EXERCÍCIO: Há pelo menos uma maneira de produzir um downcast em C # sem usar um operador de conversão explícito. Você consegue pensar em tais cenários? Como esses também são odores de código em potencial, que fatores de design você acha que foram para o design de um recurso que poderia produzir uma falha de downcast sem ter um manifesto convertido no código?

Eric Lippert
fonte
101
Lembre-se daqueles pôsteres na escola nas paredes "O que é popular nem sempre é certo, o que é certo nem sempre é popular?" Você pensou que eles estavam tentando impedir você de usar drogas ou engravidar no ensino médio, mas na verdade eram avisos sobre os perigos da dívida técnica.
CorsiKa
14
@ EricLippert, a declaração foreach, é claro. Isso foi feito antes que o IEnumerable fosse genérico, certo?
Arturo Torres Sánchez
18
Esta explicação fez o meu dia. Como professor, muitas vezes me perguntam por que o C # é projetado dessa ou daquela maneira, e minhas respostas geralmente são baseadas em motivações que você e seus colegas compartilharam aqui e em seus blogs. Muitas vezes, é um pouco mais difícil responder à pergunta de acompanhamento "Mas por que aaaa ?!". E aqui encontro a resposta perfeita: "O C # foi inventado por programadores pragmáticos que têm trabalhos a fazer, por programadores pragmáticos que têm trabalhos a fazer". :-) Obrigado @EricLippert!
Zano
12
Aparte: Obviamente, no meu comentário acima, eu quis dizer que temos um downcast de A para B. Eu realmente não gosto do uso de "up" e "down" e "parent" e "child" ao navegar na hierarquia de classes e eu os pego errado o tempo todo na minha escrita. A relação entre os tipos é uma relação de superconjunto / subconjunto, então, por que deveria haver uma direção para cima ou para baixo, não sei. E nem me inicie no "pai". Os pais de Girafa não são Mamíferos, os pais de Girafa são o Sr. e a Sra. Girafa.
Eric Lippert
9
Object.Equalsé horrível . O cálculo da igualdade em C # é completamente confuso , confuso demais para explicar em um comentário. Existem muitas maneiras de representar a igualdade; é muito fácil torná-los inconsistentes; é muito fácil, de fato, necessário violar as propriedades exigidas de um operador de igualdade (simetria, reflexividade e transitividade). Se eu estivesse projetando um sistema de tipos do zero hoje, não haveria Equals(ou GetHashcodeou ToString) Objectativado.
Eric Lippert 27/02
33

Manipuladores de eventos geralmente têm a assinatura MethodName(object sender, EventArgs e). Em alguns casos, é possível manipular o evento sem considerar o tipo sender, ou mesmo sem usá sender-lo, mas em outros senderdeve ser convertido para um tipo mais específico para manipular o evento.

Por exemplo, você pode ter dois TextBoxse deseja usar um único representante para manipular um evento de cada um deles. Em seguida, você precisaria converter senderpara a TextBoxpara poder acessar as propriedades necessárias para manipular o evento:

private void TextBox_TextChanged(object sender, EventArgs e)
{
   TextBox box = (TextBox)sender;
   box.BackColor = string.IsNullOrEmpty(box.Text) ? Color.Red : Color.White;
}

Sim, neste exemplo, você pode criar sua própria subclasse, a TextBoxqual fez isso internamente. Nem sempre é prático ou possível, no entanto.

mmathis
fonte
2
Obrigado, esse é um bom exemplo. O TextChangedevento é membro de Control- eu me pergunto por que sendernão é do tipo, e Controlnão do tipo object? Também me pergunto se (teoricamente) a estrutura poderia ter redefinido o evento para cada subclasse (para que, por exemplo, TextBoxpudesse ter uma nova versão do evento, usando TextBoxcomo o tipo de remetente) ... que pode (ou não) exigir downcasting ( para TextBox) dentro da TextBoximplementação (para implementar o novo tipo de evento), mas evitaria exigir um downcast no manipulador de eventos do código do aplicativo.
26418 ChrisW
3
Isso é considerado aceitável porque é difícil generalizar a manipulação de eventos se cada evento tiver sua própria assinatura de método. Embora eu suponha que seja uma limitação aceita, e não algo que é considerado uma boa prática, porque a alternativa é realmente mais problemática. Se fosse possível começar com um TextBox senderao invés de um object sendersem complicar demais o código, tenho certeza que isso seria feito.
Neil
6
@Neil Os sistemas de eventos são feitos especificamente para permitir o encaminhamento de blocos de dados arbitrários para objetos arbitrários. Isso é quase impossível sem verificações em tempo de execução quanto à correção do tipo.
Joker_vD
@Joker_vD O ideal é que a API ofereça suporte a assinaturas de retorno de chamada fortemente tipadas usando contravariância genérica. O fato de o .NET (esmagadoramente) não fazer isso se deve apenas ao fato de que os genéricos e a covariância foram introduzidos posteriormente. - Em outras palavras, o downcast aqui (e, geralmente, em outros lugares) é necessário devido a inadequações no idioma / API, não porque é fundamentalmente uma boa idéia.
Konrad Rudolph
@KonradRudolph Claro, mas como você armazenaria eventos de tipos arbitrários na fila de eventos? Você se livra das filas e vai para o envio direto?
Joker_vD 27/02
17

Você precisa fazer downcasting quando algo lhe der um supertipo e precisar lidar com isso de maneira diferente, dependendo do subtipo.

decimal value;
Donation d = user.getDonation();
if (d is CashDonation) {
     value = ((CashDonation)d).getValue();
}
if (d is ItemDonation) {
     value = itemValueEstimator.estimateValueOf(((ItemDonation)d).getItem());
}
System.out.println("Thank you for your generous donation of $" + value);

Não, isso não cheira bem. Em um mundo perfeito Donation, teria um método abstrato getValuee cada subtipo de implementação teria uma implementação apropriada.

Mas e se essas classes forem de uma biblioteca que você não pode ou não deseja alterar? Então não há outra opção.

Philipp
fonte
Parece um bom momento para usar o padrão de visitante. Ou a dynamicpalavra - chave que alcança o mesmo resultado.
user2023861
3
@ user2023861 Não, acho que o padrão de visitantes depende da cooperação das subclasses de Doação - ou seja, a Doação precisa declarar um resumo void accept(IDonationVisitor visitor), que suas subclasses implementam chamando métodos específicos (por exemplo, sobrecarregados) de IDonationVisitor.
26418 ChrisW
@ ChrisW, você está certo sobre isso. Sem cooperação, você ainda pode fazê-lo usando a dynamicpalavra - chave.
user2023861
Nesse caso específico, você pode adicionar um método valuValue à Doação.
user253751
@ user2023861: dynamicainda inativo , apenas mais lento.
Mehrdad
12

Para adicionar à resposta de Eric Lippert, já que não posso comentar ...

Muitas vezes, você pode evitar downcasting refatorando uma API para usar genéricos. Mas os genéricos não foram adicionados ao idioma até a versão 2.0 . Portanto, mesmo que seu próprio código não precise suportar versões de idiomas antigos, você poderá usar classes herdadas que não são parametrizadas. Por exemplo, algumas classes podem ser definidas para transportar uma carga útil do tipo Object, que você é forçado a converter no tipo que realmente é.

TKK
fonte
De fato, pode-se dizer que os genéricos em Java ou C # são essencialmente apenas um invólucro seguro para armazenar objetos de superclasse e fazer downcast para a subclasse correta (verificada pelo compilador) antes de usar os elementos novamente. (Ao contrário de modelos C ++, que reescrever o código para usar sempre a subclasse em si e, assim, evitar ter de baixos - o que pode aumentar o desempenho de memória, se muitas vezes à custa de tempo de compilação e tamanho executável.)
leftaroundabout
4

Há uma troca entre linguagens estáticas e dinamicamente tipadas. A digitação estática fornece ao compilador muitas informações para poder oferecer garantias bastante fortes sobre a segurança de (partes de) programas. Isso tem um custo, no entanto, porque você não apenas precisa saber que seu programa está correto, mas também deve escrever o código apropriado para convencer o compilador de que esse é o caso. Em outras palavras, fazer reivindicações é mais fácil do que prová-las.

Existem construções "inseguras" que ajudam a fazer afirmações não comprovadas para o compilador. Por exemplo, chamadas incondicionais para Nullable.Value, downcasts incondicionais, dynamicobjetos etc. Eles permitem que você afirme uma reivindicação ("Eu afirmo que o objeto aé String, se estiver errado, lance um InvalidCastException") sem a necessidade de provar. Isso pode ser útil nos casos em que provar que é significativamente mais difícil do que valer a pena.

Abusar disso é arriscado, e é exatamente por isso que a notação explícita de downcast existe e é obrigatória; é sal sintático destinado a chamar a atenção para uma operação insegura. O idioma poderia ter sido implementado com downcasting implícito (onde o tipo inferido é inequívoco), mas isso ocultaria essa operação insegura, o que não é desejável.

Alexander
fonte
Acho que estou pedindo, então, alguns exemplos de onde / quando é necessário ou desejável (ou seja, boas práticas) fazer esse tipo de "afirmação não comprovada".
26418 ChrisW
11
Que tal lançar o resultado de uma operação de reflexão? stackoverflow.com/a/3255716/3141234
Alexander
3

O downcasting é considerado ruim por alguns motivos. Penso principalmente porque é anti-OOP.

OOP realmente gostaria se você nunca tivesse que se abaixar, pois sua "razão de ser" é que o polimorfismo significa que você não precisa ir

if(object is X)
{
    //do the thing we do with X's
}
else
{
    //of the thing we do with Y's
}

você apenas faz

x.DoThing()

e o código automaticamente faz a coisa certa.

Existem algumas razões 'difíceis' para não fazer downcast:

  • Em C ++, é lento.
  • Você recebe um erro de tempo de execução se escolher o tipo errado.

Mas a alternativa ao downcasting em alguns cenários pode ser bastante feia.

O exemplo clássico é o processamento de mensagens, no qual você não deseja adicionar a função de processamento ao objeto, mas a mantém no processador de mensagens. Em seguida, tenho uma tonelada de MessageBaseClassem uma matriz para processar, mas também preciso de cada uma por subtipo para o processamento correto.

Você pode usar o Double Dispatch para solucionar o problema ( https://en.wikipedia.org/wiki/Double_dispatch ) ... Mas isso também tem seus problemas. Ou você pode simplesmente fazer o downcast por algum código simples, correndo o risco desses motivos difíceis.

Mas isso foi antes da criação dos genéricos. Agora você pode evitar o downcasting fornecendo um tipo em que os detalhes especificados posteriormente.

MessageProccesor<T> pode variar seus parâmetros de método e retornar valores pelo tipo especificado, mas ainda fornecer funcionalidade genérica

É claro que ninguém está forçando você a escrever código OOP e há muitos recursos de linguagem que são fornecidos, mas que são mal vistos, como reflexão.

Ewan
fonte
11
Duvido que seja lento em C ++, especialmente se você em static_castvez de dynamic_cast.
26418 ChrisW
"Processamento de mensagens" - penso nisso como significado, por exemplo, conversão lParampara algo específico. Não tenho certeza se esse é um bom exemplo de C #.
26418 ChrisW
3
@ ChrisW - bem, sim, static_casté sempre rápido ... mas não é garantido que não falhe de maneira indefinida se você cometer um erro e o tipo não for o que você está esperando.
Jules
@ Jules: Isso é completamente irrelevante.
Mehrdad 28/02
@ Mehrdad, é exatamente o ponto. fazer o c # equivalente convertido em c ++ é lento. e esta é a razão tradicional contra o elenco. o static_cast alternativo não é equivalente e considerou a escolha pior devido ao comportamento indefinido
Ewan
0

Além de tudo o que foi dito anteriormente, imagine uma propriedade Tag que seja do tipo objeto. Ele fornece uma maneira de armazenar um objeto de sua escolha em outro objeto para uso posterior conforme necessário. Você precisa fazer o downcast dessa propriedade.

Em geral, não usar algo até hoje nem sempre é uma indicação de algo inútil ;-)

disco
fonte
Sim, veja por exemplo Qual uso é a propriedade Tag em .net . Em uma das respostas lá, alguém escreveu, eu costumava usá-lo para inserir instruções ao usuário nos aplicativos Windows Forms. Quando o evento GotFocus de controle foi acionado, as instruções da propriedade Label.Text receberam o valor da minha propriedade Tag de controle que continha a sequência de instruções. Eu acho que uma maneira alternativa de 'estender' a Control(em vez de usar a object Tagpropriedade) seria criar uma Dictionary<Control, string>maneira de armazenar o rótulo para cada controle.
26418 ChrisW
11
Gosto dessa resposta porque Tagé uma propriedade pública de uma classe de estrutura .Net, que mostra que você (mais ou menos) deveria usá-la.
26418 ChrisW
@ChrisW: Não quer dizer, a conclusão é errado (ou direito), mas note que isso é o raciocínio superficial, porque há uma abundância de funcionalidade pública .NET que é obsoleto (por exemplo, System.Collections.ArrayList) ou de outra forma obsoleta (por exemplo, IEnumerator.Reset).
Mehrdad 28/02
0

O uso mais comum de downcasting no meu trabalho é devido a algum idiota que quebra o princípio da substituição de Liskov.

Imagine que há uma interface em alguma biblioteca de terceiros.

public interface Foo
{
     void SayHello();
     void SayGoodbye();
 }

E 2 classes que o implementam. Bare Qux. Baré bem comportado.

public class Bar
{
    void SayHello()
    {
        Console.WriteLine(“Bar says hello.”);
    }
    void SayGoodbye()
    {
         Console.WriteLine(“Bar says goodbye”);
    }
}

Mas Quxnão é bem comportado.

public class Qux
{
    void SayHello()
    {
        Console.WriteLine(“Qux says hello.”);
    }
    void SayGoodbye()
    {
        throw new NotImplementedException();
    }
}

Bem ... agora não tenho escolha. Eu tenho que digitar check e (possivelmente) downcast para evitar que meu programa pare.

Pato de borracha
fonte
11
Não é verdade para este exemplo em particular. Você pode capturar a NotImplementedException ao chamar SayGoodbye.
Por Zweigbergk,
Talvez seja um exemplo excessivamente simplificado, mas trabalhe um pouco com algumas das APIs .Net mais antigas e você encontrará essa situação. Às vezes, a maneira mais fácil de escrever o código é lançar com segurança asa var qux = foo as Qux;.
28418 RubberDuck
0

Outro exemplo do mundo real são os WPFs VisualTreeHelper. Ele usa downcast para transmitir DependencyObjectpara Visuale Visual3D. Por exemplo, VisualTreeHelper.GetParent . O downcast acontece em VisualTreeUtils.AsVisualHelper :

private static bool AsVisualHelper(DependencyObject element, out Visual visual, out Visual3D visual3D)
{
    Visual elementAsVisual = element as Visual;

    if (elementAsVisual != null)
    {
        visual = elementAsVisual;
        visual3D = null;
        return true;
    }

    Visual3D elementAsVisual3D = element as Visual3D;

    if (elementAsVisual3D != null)
    {
        visual = null;
        visual3D = elementAsVisual3D;
        return true;
    }            

    visual = null;
    visual3D = null;
    return false;
}
zwcloud
fonte