O que é um uso adequado do downcasting?

64

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

Um exemplo de downcast pode ser se você converter de System.Object para algum outro tipo.

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 de downcasting.

  • Quais são, se houver, casos de uso bons e adequados para downcasting? Isto é, em que circunstância (s) é apropriado escrever código que declara?
  • Se a sua resposta for "nenhum", por que o downcast é apoiado pela linguagem?
por ChrisW 26.02.2018 / 14:11
fonte

9 respostas

8

Aqui estão alguns usos apropriados do downcasting.

E eu respeitosamente veementemente discordo com outros que dizem que o uso de downcasts é definitivamente um cheiro de código, porque eu 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ê vai dizer que estes são cheiros de código, você precisa fornecer melhores soluções para eles (mesmo que eles sejam evitados devido a inconveniências). Não acredito que existam soluções melhores.

    
por 27.02.2018 / 12:25
fonte
128

Downcasting is unpopular, maybe a code smell

Eu discordo. Downcasting é extremamente popular ; um grande número de programas do mundo real contém um ou mais downcasts. E não é talvez um cheiro de código. É definitivamente um cheiro de código. É por isso que a operação de downcast é necessária para se manifestar no texto do programa . É para que você possa notar com mais facilidade o cheiro e gastar atenção na revisão do código.

in what circumstance[s] is it appropriate to write code which 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ífica do que o tipo de tempo de compilação da expressã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 um dos dois primeiros pontos.

Se você puder 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 isso. Downcasts foram adicionados ao idioma para as circunstâncias em que é difícil e caro para refatorar o programa.

why is downcasting supported by the language?

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 projetistas de C # não são puristas da OO. E o sistema do tipo C # não é perfeito; por design, ele 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 strong garantia de que o downcast será verificado em tempo de execução e, se não puder ser verificado, o programa faz a coisa certa e falha. Isso é maravilhoso; Isso significa que se o seu entendimento 100% correto da semântica do tipo se mostrar 99,99% correto, então seu programa funciona 99,99% do tempo e trava o resto do tempo, em vez de se comportar de forma imprevisível e corrompendo os dados do usuário em 0,01% do tempo .

EXERCÍCIO: Existe pelo menos uma maneira de produzir um downcast em C # sem usando um operador de conversão explícito. Você consegue pensar em algum desses cenários? Como esses também são cheiros de código em potencial, quais fatores de design você acha que entraram no design de um recurso que poderia produzir um travamento abafado sem ter um manifesto convertido no código?

    
por 26.02.2018 / 16:10
fonte
32

Os 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 usar sender , mas em outros sender deve ser convertido em um tipo mais específico para manipular o evento.

Por exemplo, você pode ter dois TextBox s e deseja usar um único delegado para manipular um evento de cada um deles. Em seguida, você precisaria converter sender em TextBox para 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 de TextBox , que fez isso internamente. Nem sempre é prático ou possível, no entanto.

    
por 26.02.2018 / 15:25
fonte
17

Você precisa de downcasting quando alguma coisa lhe der um supertipo e você precisa lidar com isso de forma 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 getValue e 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 quer alterar? Então não há outra opção.

    
por 26.02.2018 / 14:50
fonte
12

Para adicionar a resposta de Eric Lippert , pois não posso comentar ...

Você pode evitar o downcasting ao refatorar uma API para usar genéricos. Porém, 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 antigas do idioma, você pode se encontrar usando classes herdadas que não são parametrizadas. Por exemplo, alguma classe pode ser definida para transportar uma carga útil do tipo Object , que você é forçado a converter para o tipo que você sabe que realmente é.

    
por 26.02.2018 / 18:48
fonte
4

Existe um trade-off entre linguagens estáticas e dinamicamente tipadas. A tipagem estática dá ao compilador muitas informações para poder dar garantias bastante strongs sobre a segurança de (partes de) programas. Isso tem um custo, no entanto, porque você não precisa apenas saber que seu programa está correto, mas 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 você a fazer afirmações não comprovadas para o compilador. Por exemplo, chamadas incondicionais para Nullable.Value , downcasts incondicionais, dynamic objects, etc. Eles permitem que você declare uma declaração ("Eu afirmo que o objeto a é um String , se estiver errado, lance InvalidCastException ") sem precisando provar isso. Isso pode ser útil em casos em que é muito mais difícil do que valer a pena.

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

    
por 26.02.2018 / 18:32
fonte
3

Downcasting é considerado ruim por alguns motivos. Principalmente eu acho que é anti-OOP.

OOP realmente gostaria que você nunca tivesse que ser abatido, já que o seu "raison-d'etre" é 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ê acabou de fazer

x.DoThing()

e o código automagicamente faz a coisa certa.

Existem algumas razões 'difíceis' para não ser rejeitado:

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

Mas a alternativa ao downcast em alguns cenários pode ser bem feia.

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

Você pode usar o Double Dispatch para contornar o problema ( link ) ... Mas isso também tem seus problemas. Ou você pode simplesmente usar um código simples com o risco dessas razões difíceis.

Mas isso foi antes dos genéricos serem inventados. Agora você pode evitar downcasting fornecendo um tipo onde os detalhes foram especificados posteriormente.

MessageProccesor<T> pode variar seus parâmetros de método e retornar valores pelo tipo especificado, mas ainda fornece 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 desaprovados, como reflexão.

    
por 26.02.2018 / 15:18
fonte
0

Além de tudo dito antes, imagine uma propriedade Tag que seja do tipo type. Ele fornece uma maneira de armazenar um objeto de sua escolha em outro objeto para uso posterior conforme necessário. Você precisa baixar essa propriedade.

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

    
por 26.02.2018 / 17:28
fonte
0

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

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

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

E duas classes que implementam. Bar e Qux . Bar é bem comportado.

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

Mas Qux não é bem comportado.

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

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

    
por 26.02.2018 / 18:45
fonte