Qual è un uso corretto del downcasting?


68

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

Un esempio di downcast potrebbe essere se si esegue il cast da System.Objectun altro tipo.

Il downcasting è impopolare, forse un odore di codice: la dottrina orientata agli oggetti è quella di preferire, ad esempio, la definizione e la chiamata di metodi virtuali o astratti anziché il downcasting.

  • Quali sono, se del caso, i casi di utilizzo validi e corretti per il downcasting? Cioè, in quali circostanze è appropriato scrivere codice che esegue il downcast?
  • Se la tua risposta è "nessuna", allora perché il downcasting è supportato dalla lingua?

5
Quello che descrivi viene generalmente chiamato "downcasting". Grazie a questa conoscenza, potresti prima fare qualche ricerca in più e spiegare perché i motivi esistenti che hai scoperto non sono sufficienti? TL; DR: il downcast interrompe la tipizzazione statica, ma a volte un sistema di tipi è troppo restrittivo. Quindi è sensato che una lingua offra downcasting sicuro.
amon,

Intendi downcasting? L'upcasting sarebbe aumentato attraverso la gerarchia di classi, cioè dalla sottoclasse alla superclasse, al contrario del downcasting, dalla superclasse alla sottoclasse ...
Andrei Socaciu,

Mi scuso: hai ragione. Ho modificato la domanda per rinominare "upcast" in "downcast".
ChrisW,

@amon en.wikipedia.org/wiki/Downcasting fornisce "convert in stringa" come esempio (che .Net supporta l'utilizzo di un virtuale ToString); l'altro esempio è "contenitori Java" perché Java non supporta generici (cosa che fa C #). stackoverflow.com/questions/1524197/downcast-and-upcast dice quello downcasting è , ma senza esempi di quando è appropriata .
ChrisW,

4
@ChrisW Non è before Java had support for genericscosì because Java doesn't support generics. E amon ti ha praticamente dato la risposta: quando il sistema di tipi con cui stai lavorando è troppo restrittivo e non sei in grado di cambiarlo.
Ordous,

Risposte:


12

Ecco alcuni usi corretti del downcasting.

E sono rispettosamente con veemenza in disaccordo con gli altri qui che affermano che l'uso dei downcast è sicuramente un odore di codice, perché credo che non ci sia un 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 hai intenzione di dire che questi sono odori di codice, devi fornire loro soluzioni migliori (anche se vengono evitate a causa di inconvenienti). Non credo che esistano soluzioni migliori.


9
"Odore di codice" non significa "questo design è decisamente negativo " significa "questo design è sospetto ". Che ci siano caratteristiche del linguaggio intrinsecamente sospette è una questione di pragmatismo da parte dell'autore del linguaggio
Caleth,

5
@Caleth: E il mio punto è che questi disegni escono continuamente, e che non c'è assolutamente nulla di intrinsecamente sospetto nei disegni che ho mostrato, e quindi il casting non è un odore di codice.
Mehrdad,

4
@Caleth Non sono d'accordo. L'odore del codice, per me, significa che il codice è cattivo o, per lo meno, potrebbe essere migliorato.
Konrad Rudolph,

6
@Mehrdad La tua opinione sembra essere che gli odori di codice debbano essere colpa dei programmatori dell'applicazione. Perché? Non tutti gli odori di codice devono essere un problema reale o avere una soluzione. Un odore di codice secondo Martin Fowler è semplicemente "un'indicazione di superficie che di solito corrisponde a un problema più profondo nel sistema" si object.Equalsadatta abbastanza bene a quella descrizione (prova a fornire correttamente un contratto uguale in una superclasse che consente alle sottoclassi di implementarlo correttamente - è assolutamente non banale). Che come programmatori di applicazioni non possiamo risolverlo (in alcuni casi possiamo evitarlo) è un argomento diverso.
Voo,

5
@Mehrdad Bene, vediamo cosa ha da dire uno dei designer del linguaggio originale su Equals .. Object.Equals is horrid. Equality computation in C# is completely messed up(commento di Eric sulla sua risposta). È divertente come non voler riconsiderare la tua opinione anche quando uno dei principali designer del linguaggio stesso ti dice che ti sbagli.
Voo,

136

Il downcasting è impopolare, forse un odore di codice

Non sono d'accordo. Il downcasting è estremamente popolare ; un numero enorme di programmi del mondo reale contiene uno o più downcast. E non è forse un odore di codice. È sicuramente un odore di codice. Ecco perché è necessario che l'operazione di downcast sia manifestata nel testo del programma . È così che puoi notare più facilmente l'odore e dedicare attenzione alla revisione del codice su di esso.

in quali circostanze è appropriato scrivere un codice che esegue il downcast?

In ogni circostanza in cui:

  • hai una conoscenza corretta al 100% di un fatto sul tipo di runtime di un'espressione che è più specifico del tipo di tempo di compilazione dell'espressione e
  • è necessario sfruttare questo fatto per utilizzare una capacità dell'oggetto non disponibile nel tipo di tempo di compilazione e
  • è un uso migliore del tempo e dello sforzo per scrivere il cast piuttosto che riformattare il programma per eliminare uno dei primi due punti.

Se è possibile effettuare il refactoring a buon mercato di un programma in modo che il compilatore possa dedurre il tipo di runtime o rifattorizzare il programma in modo tale che non sia necessaria la capacità del tipo più derivato, farlo. I downcasts sono stati aggiunti alla lingua per quelle circostanze in cui è difficile e costoso riformattare così il programma.

perché il downcasting è supportato dalla lingua?

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

Inoltre, il downcasting è molto sicuro in C #. Abbiamo una forte 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 corretta comprensione al 100% della semantica di tipo risulta corretta al 99,99%, il tuo programma funziona il 99,99% del tempo e si arresta in modo anomalo per il resto del tempo, invece di comportarsi in modo imprevedibile e corrompere i dati dell'utente 0,01% del tempo .


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


101
Ricorda quei poster al liceo sui muri "Ciò che è popolare non è sempre giusto, ciò che è giusto non è sempre popolare?" Pensavi che stessero cercando di impedirti di drogarti o di rimanere incinta al liceo, ma in realtà erano avvertimenti sui pericoli del debito tecnico.
corsiKa

14
@EricLippert, l'affermazione foreach, ovviamente. Questo è stato fatto prima che IEnumerable fosse generico, giusto?
Arturo Torres Sánchez,

18
Questa spiegazione ha reso la mia giornata. Come insegnante, mi viene spesso chiesto perché C # è progettato in questo o quel modo, e le mie risposte si basano spesso sulle motivazioni che tu e i tuoi colleghi avete condiviso qui e sui vostri blog. Spesso è un po 'più difficile rispondere alla domanda di follow-up "Ma whyyyyy ?!". E qui trovo la risposta di follow-up perfetta: "C # è stato inventato da programmatori pragmatici che hanno lavori da svolgere, per programmatori pragmatici che hanno lavori da svolgere". :-) Grazie @EricLippert!
Zano,

12
A parte: ovviamente nel mio commento sopra intendevo dire che abbiamo un downcast da A a B. Non mi piace molto l'uso di "up" e "down" e "parent" e "child" quando navigo in una gerarchia di classi e li ottengo sempre sbagliato nei miei scritti. La relazione tra i tipi è una relazione superset / subset, quindi perché non ci dovrebbe essere una direzione su o giù associata, non lo so. E non farmi nemmeno iniziare su "genitore". Il genitore di Giraffe non è Mammifero, i genitori di Giraffe sono Mr. e Mrs. Giraffe.
Eric Lippert,

9
Object.Equalsè orribile . Il calcolo dell'uguaglianza in C # è completamente incasinato , troppo incasinato per essere spiegato in un commento. Esistono molti modi per rappresentare l'uguaglianza; è molto facile renderli incoerenti; è molto semplice, infatti è necessario violare le proprietà richieste a un operatore di uguaglianza (simmetria, riflessività e transitività). Se stavo progettando un sistema di tipi da zero oggi non ci sarebbe Equals(o GetHashcodeo ToString) Objectaffatto.
Eric Lippert,

33

I gestori di eventi di solito hanno la firma MethodName(object sender, EventArgs e). In alcuni casi è possibile gestire l'evento senza tener conto di quale tipo sendersia, o addirittura senza utilizzarlo senderaffatto, ma in altri senderdeve essere eseguito il cast su un tipo più specifico per gestire l'evento.

Ad esempio, potresti avere due TextBoxse desideri utilizzare un singolo delegato per gestire un evento da ciascuno di essi. Quindi dovrai eseguire il cast senderin TextBoxmodo 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, è possibile creare la propria sottoclasse di TextBoxcui è stata eseguita internamente. Non sempre pratico o possibile, però.


2
Grazie, questo è un buon esempio. L' TextChangedevento è membro di Control- Mi chiedo perché sendernon sia di tipo Controlanziché di tipo object? Inoltre mi chiedo se (teoricamente) il framework avrebbe potuto ridefinire l'evento per ogni sottoclasse (in modo che, ad esempio, TextBoxpotrebbe avere una nuova versione dell'evento, usando TextBoxcome tipo di mittente) ... che potrebbe (o non potrebbe) richiedere il downcasting ( a TextBox) all'interno TextBoxdell'implementazione (per implementare il nuovo tipo di evento), ma eviterebbe di richiedere un downcast nel gestore eventi del codice dell'applicazione.
ChrisW,

3
Questo è considerato accettabile perché è difficile generalizzare la gestione degli eventi se ogni evento ha la propria firma del metodo. Anche se suppongo che sia una limitazione accettata piuttosto che qualcosa che è considerata la migliore pratica perché l'alternativa è in realtà più problematica. Se fosse possibile iniziare con un TextBox senderanziché un object sendersenza complicare eccessivamente il codice, sono sicuro che sarebbe stato fatto.
Neil,

6
@Neil I sistemi di eventi sono appositamente realizzati per consentire l'inoltro di blocchi di dati arbitrari a oggetti arbitrari. È quasi impossibile fare a meno dei controlli di runtime per la correttezza del tipo.
Joker_vD,

@Joker_vD Idealmente l'API supporterebbe ovviamente le firme di callback fortemente tipizzate usando la contraddizione generica. Il fatto che .NET (in modo schiacciante) non lo faccia è semplicemente dovuto al fatto che i generici e la covarianza sono stati introdotti in seguito. - In altre parole, il downcasting qui (e, generalmente, altrove) è necessario a causa di inadeguatezze nella lingua / API, non perché è fondamentalmente una buona idea.
Konrad Rudolph,

@KonradRudolph Sicuro, ma come memorizzeresti eventi di tipi arbitrari nella coda degli eventi? Ti basta sbarazzarti delle code e andare alla spedizione diretta?
Joker_vD,

17

Devi effettuare il 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 Donationavrebbe un metodo astratto getValuee ogni 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.


Sembra un buon momento per usare il modello visitatore. O la dynamicparola chiave che ottiene lo stesso risultato.
user2023861,

3
@ user2023861 No, penso che il modello di visitatore dipenda dalla cooperazione delle sottoclassi di donazione - vale a dire che la donazione deve dichiarare un abstract void accept(IDonationVisitor visitor), che le sue sottoclassi implementano chiamando metodi specifici (es. sovraccarichi) di IDonationVisitor.
ChrisW,

@ Chris, hai ragione. Senza collaborazione, puoi comunque farlo usando la dynamicparola chiave.
user2023861,

In questo caso particolare è possibile aggiungere un metodo estimValue alla donazione.
user253751

@ user2023861: dynamicancora downcasting, solo più lento.
Mehrdad,

12

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

Spesso è possibile evitare il downcasting rifattorizzando un'API per utilizzare 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 lingua antica, 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 conosci effettivamente.


In effetti, si potrebbe dire che i generici in Java o C # sono essenzialmente solo un wrapper sicuro per la memorizzazione di oggetti superclasse e il downcasting alla sottoclasse corretta (controllata dal compilatore) prima di riutilizzare gli elementi. (A differenza dei modelli C ++, che riscrivono il codice per utilizzare sempre la sottoclasse stessa ed evitare quindi di eseguire il downcast - il che può migliorare le prestazioni della memoria, se spesso a scapito del tempo di compilazione e delle dimensioni eseguibili.)
leftaroundabout

4

Esiste un compromesso tra le lingue statiche e quelle tipizzate dinamicamente. La digitazione 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 devi sapere che il tuo programma è corretto, ma devi scrivere il codice appropriato per convincere il compilatore che è così. In altre parole, fare affermazioni è più facile che provarle.

Esistono costrutti "non sicuri" che ti aiutano a fare affermazioni non comprovate al compilatore. Ad esempio, chiamate incondizionate a Nullable.Value, downcasts incondizionati, dynamicoggetti, ecc. Ti permettono di affermare un reclamo ("Asserisco che l'oggetto aè un String, se sbaglio, lancia un InvalidCastException") senza bisogno di dimostrarlo. Questo può essere utile nei casi in cui dimostrarlo è significativamente più difficile che utile.

Abusare di questo è rischioso, motivo per cui esiste la notazione esplicita del downcast ed è obbligatoria; è sale sintattico destinato ad attirare l'attenzione su un'operazione non sicura. Il linguaggio avrebbe potuto essere implementato con downcasting implicito (dove il tipo inferito non è ambiguo), ma ciò nasconderebbe questa operazione non sicura, che non è desiderabile.


Immagino di chiedere, quindi, alcuni esempi di dove / quando è necessario o desiderabile (cioè buone pratiche) fare questo tipo di "affermazione non dimostrata".
ChrisW,

1
Che ne dici di lanciare il risultato di un'operazione di riflessione? stackoverflow.com/a/3255716/3141234
Alexander,

3

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

OOP lo gradirebbe davvero se non avessi mai dovuto eseguire il downcast poiché 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
}

lo fai e basta

x.DoThing()

e il codice fa magicamente automaticamente la cosa giusta.

Esistono alcuni motivi "difficili" per non effettuare il downcast:

  • In C ++ è lento.
  • Si ottiene un errore di runtime se si sceglie 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 conservarla nel processore dei messaggi. Ho quindi una tonnellata di MessageBaseClassin un array da elaborare, ma ho anche bisogno di ognuno per sottotipo per l'elaborazione corretta.

Puoi usare Double Dispatch per aggirare il problema ( https://en.wikipedia.org/wiki/Double_dispatch ) ... Ma anche questo ha i suoi problemi. Oppure puoi semplicemente eseguire il downcast per alcuni semplici codici a rischio di quei motivi difficili.

Ma questo è stato prima dell'invenzione di Generics. Ora puoi evitare il downcasting fornendo un tipo in cui i dettagli specificati in seguito.

MessageProccesor<T> può variare i parametri del metodo e restituire i valori in base al tipo specificato, ma fornisce comunque funzionalità generiche

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


1
Dubito che sia lento in C ++, specialmente se static_castinvece di dynamic_cast.
ChrisW,

"Elaborazione dei messaggi": penso che significhi, ad esempio, trasmettere lParama qualcosa di specifico. Non sono sicuro che sia un buon esempio in C #, comunque.
ChrisW,

3
@ChrisW - beh, sì, static_castè sempre veloce ... ma non è garantito che non fallisca in modi indefiniti se commetti un errore e il tipo non è quello che ti aspetti.
Jules,

@Jules: Questo è completamente fuori punto.
Mehrdad,

@Mehrdad, è esattamente il punto. fare il cast equivalente di c # in c ++ è lento. e questa è la ragione tradizionale contro il casting. the static_cast alternativo non è equivalente e considerato la scelta peggiore a causa del comportamento indefinito
Ewan,

0

Oltre a tutto ciò che è stato detto prima, immagina una proprietà Tag di tipo object. Ti fornisce un modo per archiviare un oggetto a tua scelta in un altro oggetto per un uso successivo di cui hai bisogno. È necessario downcast questa proprietà.

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


Sì, vedi ad esempio A che serve la proprietà Tag in .net . In una delle risposte, qualcuno ha scritto, lo usavo per inserire le istruzioni per l'utente nelle applicazioni Windows Form. Quando si è attivato l'evento GotFocus di controllo, alle proprietà Label.Text delle istruzioni è stato assegnato il valore della proprietà Tag di controllo che conteneva la stringa di istruzioni. Immagino che un modo alternativo di 'estendere' il Control(invece di usare la object Tagproprietà) sarebbe quello di creare un modo Dictionary<Control, string>per memorizzare l'etichetta per ciascun controllo.
ChrisW,

1
Mi piace questa risposta perché Tagè una proprietà pubblica di una classe framework .Net, che dimostra che dovresti (più o meno) usarla.
ChrisW,

@ChrisW: per non dire che la conclusione è sbagliata (o giusta), ma nota che questo è un ragionamento sciatto, perché ci sono molte funzionalità di .NET pubbliche che sono obsolete (diciamo System.Collections.ArrayList) o altrimenti deprecate (diciamo IEnumerator.Reset).
Mehrdad,

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 alcune librerie di terze parti.

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

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

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

Ma Quxnon è ben educato.

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

Bene ... ora non ho scelta. Io devo digitare controllo e (possibilmente) abbattuto al fine di evitare di avere il mio programma venire a una battuta d'arresto crash.


1
Non è vero per questo esempio particolare. Potresti semplicemente catturare NotImplementedException quando chiami SayGoodbye.
Per von Zweigbergk,

È forse un esempio eccessivamente semplificato, ma lavora un po 'con alcune delle vecchie API .Net e ti imbatterai in questa situazione. A volte il modo più semplice per scrivere il codice è quello di lanciare in sicurezza var qux = foo as Qux;.
RubberDuck,

0

Un altro esempio del mondo reale è quello di WPF VisualTreeHelper. Usa downcast per trasmettere DependencyObjecta Visuale Visual3D. Ad esempio, VisualTreeHelper.GetParent . Il downcast si verifica in 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;
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.