Che cosa è un uso appropriato del downcasting?

64

Downcasting significa lanciare da una classe base (o interfaccia) a una sottoclasse o una classe foglia.

Un esempio di downcast potrebbe essere se lanci da System.Object ad un altro tipo.

Il downcasting è impopolare, forse un odore di codice: la dottrina orientata agli oggetti preferisce, ad esempio, definire e chiamare metodi virtuali o astratti anziché downcasting.

  • Quali sono, se del caso, casi di utilizzo buoni e corretti per downcasting? Cioè, in quale circostanza (i) è appropriato scrivere il codice che downcasts?
  • Se la tua risposta è "nessuna", allora perché il downcasting è supportato dalla lingua?
posta ChrisW 26.02.2018 - 14:11
fonte

9 risposte

8

Ecco alcuni usi appropriati del downcasting.

E rispettoso con veemenza non sono d'accordo con altri qui che dicono che l'uso dei downcast è sicuramente un odore di codice, perché credo che non ci sia altro modo ragionevole per risolvere i problemi corrispondenti.

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 stai per dire che questi sono odori di codice, devi fornire soluzioni migliori per loro (anche se sono evitati a causa di disagi). Non credo esistano soluzioni migliori.

    
risposta data 27.02.2018 - 12:25
fonte
128

Downcasting is unpopular, maybe a code smell

Non sono d'accordo. Downcasting è estremamente popolare ; un numero enorme di programmi reali contiene uno o più downcast. E non è forse un odore di codice. È sicuramente un odore di codice. Ecco perché è necessario che l'operazione di downcasting si manifesti nel testo del programma . È così che puoi più facilmente notare l'odore e spendere l'attenzione per la revisione del codice su di esso.

in what circumstance[s] is it appropriate to write code which downcasts?

In ogni circostanza in cui:

  • hai una conoscenza corretta al 100% di un fatto sul tipo di esecuzione di un'espressione che è più specifico del tipo di espressione di compilazione dell'espressione, e
  • devi approfittare di questo fatto per utilizzare una capacità dell'oggetto non disponibile sul tipo di compilazione, e
  • è meglio usare il tempo e gli sforzi per scrivere il cast piuttosto che rifattorizzare il programma per eliminare uno dei primi due punti.

Se è possibile effettuare un refacting di un programma a basso costo in modo che il tipo di esecuzione possa essere dedotto dal compilatore o effettuare il refactoring del programma in modo tale che non sia necessario il tipo più derivato, eseguire tale operazione. I downlink sono stati aggiunti alla lingua per quelle circostanze in cui è difficile e costoso per il refactoring del programma.

why is downcasting supported by the language?

C # è stato inventato da programmatori pragmatici che hanno un lavoro da fare, per programmatori pragmatici che hanno un lavoro da fare. I designer C # non sono puristi OO. E il sistema di tipo C # non è perfetto; in base alla progettazione sottostima le restrizioni che possono essere inserite nel tipo di runtime di una variabile.

Inoltre, il downcasting è molto sicuro in C #. Abbiamo una strong garanzia che il downcast verrà verificato in fase di esecuzione e se non può essere verificato, il programma fa la cosa giusta e si blocca. Questo è meraviglioso; significa che se la tua comprensione al 100% della semantica dei tipi risulta corretta al 99,99%, allora il tuo programma funziona il 99,99% delle volte e si blocca per il resto del tempo, invece di comportarsi in modo imprevedibile e corrompendo i dati degli utenti 0,01% delle volte .

ESERCIZIO: esiste almeno un modo per produrre un downcast in C # senza utilizzando un operatore di cast esplicito. Riesci a pensare a questi scenari? Dal momento che questi sono anche potenziali odori di codice, quali fattori di progettazione pensi siano andati nella progettazione di una funzionalità che potrebbe causare un arresto anomalo del downcast senza avere un cast manifest nel codice?

    
risposta data 26.02.2018 - 16:10
fonte
32

I gestori di eventi di solito hanno la firma MethodName(object sender, EventArgs e) . In alcuni casi è possibile gestire l'evento senza preoccuparsi del tipo sender o anche senza utilizzare sender , ma in altri sender deve essere convertito in un tipo più specifico per gestire l'evento.

Ad esempio, potresti avere due TextBox s e vuoi utilizzare un singolo delegato per gestire un evento da ognuno di essi. Quindi devi convertire sender in TextBox in modo da poter accedere alle proprietà necessarie per gestire l'evento:

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

Sì, in questo esempio, potresti creare la tua sottoclasse di TextBox che lo ha fatto internamente. Non sempre pratico o possibile, però.

    
risposta data 26.02.2018 - 15:25
fonte
17

Hai bisogno di downcasting quando qualcosa ti dà un supertipo e devi gestirlo in modo diverso a seconda del sottotipo.

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);

No, questo non ha un buon odore. In un mondo perfetto Donation avrebbe un metodo astratto getValue e ciascun sottotipo di implementazione avrebbe un'implementazione appropriata.

Ma cosa succede se queste classi provengono da una libreria che non puoi o non vuoi cambiare? Quindi non c'è altra opzione.

    
risposta data 26.02.2018 - 14:50
fonte
12

Da aggiungere alla risposta di Eric Lippert poiché non posso commentare ...

Spesso puoi evitare il downcasting rifattorizzando un'API per utilizzare i generici. Ma i generici non sono stati aggiunti alla lingua fino alla versione 2.0 . Quindi, anche se il tuo codice non ha bisogno di supportare versioni in lingue antiche, potresti trovarti a utilizzare classi legacy che non sono parametrizzate. Ad esempio, alcune classi possono essere definite per trasportare un payload di tipo Object , che sei costretto a trasmettere al tipo che sai essere effettivamente.

    
risposta data 26.02.2018 - 18:48
fonte
4

Esiste un compromesso tra le lingue statiche e quelle tipizzate dinamicamente. La tipizzazione statica fornisce al compilatore molte informazioni per essere in grado di fornire garanzie piuttosto forti sulla sicurezza di (parti di) programmi. Questo ha un costo, tuttavia, perché non solo è necessario sapere che il programma è corretto, ma è necessario scrivere il codice appropriato per convincere il compilatore che questo è il caso. In altre parole, fare affermazioni è più facile che provarle.

Ci sono costrutti "non sicuri" che ti aiutano a fare asserzioni unproven al compilatore. Ad esempio, chiamate incondizionate a Nullable.Value , downcast non condizionali, dynamic oggetti, ecc. Permettono di affermare un reclamo ("Suppongo che l'oggetto a sia un String , se sbaglio, lanciare un InvalidCastException ") senza bisogno di dimostrarlo Questo può essere utile nei casi in cui è dimostrato che è molto più difficile che utile.

Abusare di questo è rischioso, che è esattamente il motivo per cui esiste la notazione downcast esplicita ed è obbligatoria; è sale sintattico pensato per attirare l'attenzione su un'operazione non sicura. La lingua potrebbe essere stata implementata con downcast implicito (in cui il tipo dedotto non è ambiguo), ma ciò nasconderebbe questa operazione non sicura, che non è desiderabile.

    
risposta data 26.02.2018 - 18:32
fonte
3

Il downcasting è considerato negativo per un paio di motivi. Principalmente penso perché è anti-OOP.

A OOP piacerebbe molto se non hai mai dovuto downcast come la sua "ragion d'essere" è che il polimorfismo significa che non devi andare

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

devi solo

x.DoThing()

e il codice automaticamente fa la cosa giusta.

Ci sono alcuni motivi "difficili" per non essere scartati:

  • In C ++ è lento.
  • Si ottiene un errore di runtime se si seleziona il tipo sbagliato.

Ma l'alternativa al downcasting in alcuni scenari può essere piuttosto brutta.

L'esempio classico è l'elaborazione dei messaggi, in cui non si desidera aggiungere la funzione di elaborazione all'oggetto, ma tenerlo nel processore dei messaggi. Poi ho una tonnellata di MessageBaseClass in un array da elaborare, ma ho anche bisogno di ognuno per sottotipo per l'elaborazione corretta.

Puoi usare Double Dispatch per risolvere il problema ( link ) ... Ma anche questo ha i suoi problemi. Oppure puoi semplicemente downcast per un semplice codice a rischio di quelle ragioni difficili.

Ma questo prima che i generici venissero inventati. Ora puoi evitare il downcasting fornendo un tipo in cui i dettagli specificati in seguito.

MessageProccesor<T> può variare i suoi parametri di metodo e restituire i valori in base al tipo specificato ma fornire comunque funzionalità generiche

Ovviamente nessuno ti obbliga a scrivere codice OOP e ci sono molte funzionalità linguistiche fornite ma disapprovate, come la riflessione.

    
risposta data 26.02.2018 - 15:18
fonte
0

In aggiunta a quanto detto prima, immagina una proprietà Tag che sia di tipo oggetto. Fornisce un modo per memorizzare un oggetto di tua scelta in un altro oggetto per un uso successivo di cui hai bisogno. Hai bisogno di downcast questa proprietà.

In generale, non usare qualcosa fino ad oggi non è sempre un'indicazione di qualcosa di inutile; -)

    
risposta data 26.02.2018 - 17:28
fonte
0

L'uso più comune del downcasting nel mio lavoro è dovuto a qualche idiota che infrange il principio di sostituzione di Liskov.

Immagina che ci sia un'interfaccia in qualche lib di terze parti.

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

E 2 classi che lo implementano. Bar e Qux . Bar è ben educato.

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

Ma Qux non si comporta bene.

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

Bene ... ora non ho scelta. Io ho per digitare check e (possibilmente) downcast per evitare che il mio programma si fermi bruscamente.

    
risposta data 26.02.2018 - 18:45
fonte

Leggi altre domande sui tag