Qu'est-ce qu'une utilisation appropriée du downcasting?

68

Le downcasting signifie la conversion d'une classe de base (ou interface) vers une classe sous-classe ou feuille.

Un exemple de downcast pourrait être si vous lancez à partir System.Objectd'un autre type.

Le downcasting est impopulaire, peut-être une odeur de code: la doctrine orientée objet consiste à préférer, par exemple, la définition et l’appel de méthodes virtuelles ou abstraites au lieu d’un downcasting.

  • Quels sont, le cas échéant, des cas d'utilisation corrects et appropriés pour le downcasting? C'est-à-dire, dans quelles circonstances est-il approprié d'écrire du code qui est downscast?
  • Si votre réponse est "néant", alors pourquoi le downcasting est-il pris en charge par la langue?
ChrisW
la source
5
Ce que vous décrivez est généralement appelé "downcasting". Fort de cette connaissance, pourriez-vous faire d’autres recherches et expliquez pourquoi les raisons existantes que vous avez trouvées ne sont pas suffisantes? TL; DR: le downcasting rompt le typage statique, mais parfois un système de types est trop restrictif. Il est donc judicieux qu'une langue propose un downcasting sécurisé.
amon
Voulez-vous dire le dénigrement? La diffusion en amont serait ascendante dans la hiérarchie des classes, c'est-à-dire de sous-classe à superclasse, par opposition à downcasting, de superclasse à sous-classe ...
Andrei Socaciu
Mes excuses: vous avez raison. J'ai édité la question pour renommer "upcast" en "downcast".
ChrisW
@amon en.wikipedia.org/wiki/Downcasting donne à titre d'exemple "convertir en chaîne" (que .Net prend en charge à l'aide d'un virtuel ToString); son autre exemple est "conteneurs Java" car Java ne supporte pas les génériques (ce que C # fait). stackoverflow.com/questions/1524197/downcast-and-upcast dit ce downcasting est , mais sans exemples quand il est approprié .
ChrisW
4
@ChrisW C'est before Java had support for generics, non because Java doesn't support generics. Et amon vous a à peu près donné la réponse - lorsque le système de types avec lequel vous travaillez est trop restrictif et que vous n’êtes pas en mesure de le modifier.
Ordous

Réponses:

12

Voici quelques utilisations appropriées du downcasting.

Et je respectueusement avec véhémence en désaccord avec d' autres ici qui disent l'utilisation de downcasts est certainement une odeur de code, parce que je crois qu'il n'y a pas d' autre moyen raisonnable de résoudre les problèmes correspondants.

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

Si vous dites que ce sont des odeurs de code, vous devez leur proposer de meilleures solutions (même si elles sont évitées en raison d'inconvénients). Je ne crois pas qu'il existe de meilleures solutions.

Mehrdad
la source
9
"Odeur de code" ne signifie pas "cette conception est définitivement mauvaise " mais "cette conception est suspecte ". Il existe des caractéristiques linguistiques qui sont intrinsèquement suspectes est une question de pragmatisme de la part de l'auteur de la langue
Caleth
5
@ Caleth: Et tout ce que je veux dire, c'est que ces conceptions apparaissent tout le temps et qu'il n'y a absolument rien de suspect par nature dans les conceptions que j'ai montrées. Par conséquent, le casting n'est pas une odeur de code.
Mehrdad
4
@ Caleth je ne suis pas d'accord. L'odeur de code, pour moi, signifie que le code est mauvais ou, à tout le moins, pourrait être amélioré.
Konrad Rudolph
6
@Mehrdad Votre opinion semble être que les odeurs de code doivent être la faute des programmeurs d'applications. Pourquoi? Chaque odeur de code ne doit pas nécessairement être un problème réel ou avoir une solution. Selon Martin Fowler, une odeur de code est simplement "une indication de surface qui correspond généralement à un problème plus profond dans le système" object.Equalscorrespond bien à cette description (essayez de fournir correctement un contrat d'égalité dans une super-classe qui permet aux sous-classes de le non-trivial). En tant que programmeurs d'applications, nous ne pouvons pas le résoudre (dans certains cas, nous pouvons l'éviter) est un sujet différent.
Voo le
5
@Mehrdad Eh bien, voyons ce que l'un des concepteurs de langage d'origine a à dire à propos d'Equal Object.Equals is horrid. Equality computation in C# is completely messed up(commentaire). (Commentaire d'Eric sur sa réponse). C'est drôle de ne pas vouloir reconsidérer son opinion, même lorsqu'un des concepteurs principaux du langage lui-même vous dit que vous vous trompez.
Voo le
136

Le downcasting est impopulaire, peut-être une odeur de code

Je ne suis pas d'accord. Le downcasting est extrêmement populaire ; un très grand nombre de programmes du monde réel contiennent une ou plusieurs diffusions à la baisse. Et ce n'est peut- être pas une odeur de code. C'est vraiment une odeur de code. C'est pourquoi l'opération de downcasting doit être manifeste dans le texte du programme . C’est pour que vous puissiez plus facilement remarquer l’odeur et consacrer votre attention à la révision de code.

dans quelles circonstances est-il approprié d'écrire du code qui diffuse en aval?

Dans toutes les circonstances où:

  • vous avez une connaissance correcte à 100% d'un fait concernant le type à l'exécution d'une expression qui est plus spécifique que le type à la compilation de l'expression, et
  • vous devez en tirer parti pour utiliser une fonctionnalité de l'objet non disponible sur le type à la compilation, et
  • le temps et les efforts consacrés à l'écriture du casting sont meilleurs que le refactorisation du programme pour éliminer l'un ou l'autre des deux premiers points.

Si vous pouvez refactoriser à moindre coût un programme afin que soit le compilateur puisse déduire le type d'exécution, soit le refactoriser afin que vous n'ayez pas besoin de la capacité du type le plus dérivé, faites-le. Des abaissements ont été ajoutés à la langue dans les cas où il est difficile et coûteux de restructurer le programme.

pourquoi le downcasting est-il pris en charge par la langue?

C # a été inventé par des programmeurs pragmatiques ayant des tâches à accomplir, pour des programmeurs pragmatiques ayant des tâches à accomplir. Les concepteurs C # ne sont pas des puristes OO. Et le système de type C # n’est pas parfait; de par sa conception, il sous-estime les restrictions pouvant être imposées au type d’exécution d’une variable.

De plus, le downcasting est très sûr en C #. Nous avons une forte garantie que le downcast sera vérifié au moment de l'exécution. S'il ne peut pas être vérifié, le programme fait le bon choix et se bloque. C'est merveilleux; cela signifie que si votre compréhension correcte à 100% de la sémantique des types s'avère correcte à 99,99%, votre programme fonctionne 99,99% du temps et se bloque le reste du temps, au lieu de se comporter de manière imprévisible et de corrompre les données utilisateur. .


EXERCICE: Il existe au moins un moyen de produire un downcast en C # sans utiliser d'opérateur de distribution explicite. Pouvez-vous penser à de tels scénarios? Etant donné que ce sont aussi des odeurs potentielles de code, quels facteurs ont été à l’origine de la conception d’une fonctionnalité susceptible de provoquer un plantage sans affichage du code dans le code?

Eric Lippert
la source
101
Rappelez-vous ces affiches au lycée sur les murs "Ce qui est populaire n’a pas toujours raison, ce qui est juste n’est pas toujours populaire?" Vous pensiez qu'ils essayaient de vous empêcher de prendre de la drogue ou de tomber enceinte au lycée, mais ils étaient en réalité des avertissements sur les dangers de la dette technique.
CorsiKa
14
@ EricLippert, la déclaration foreach, bien sûr. Cela a été fait avant que IEnumerable soit générique, non?
Arturo Torres Sánchez
18
Cette explication a fait ma journée. En tant qu'enseignant, on me demande souvent pourquoi C # est conçu de telle ou telle manière, et mes réponses sont souvent basées sur des motivations que vous et vos collègues avez partagées ici et sur vos blogs. Il est souvent un peu plus difficile de répondre à la question suivante "Mais pourquoi donc ?!". Et voici que je trouve la réponse parfaite: "C # a été inventé par des programmeurs pragmatiques qui ont un travail à faire, pour des programmeurs pragmatiques qui ont un travail à faire.". :-) Merci @EricLippert!
Zano
12
A part: évidemment dans mon commentaire ci-dessus je voulais dire que nous avons un downcast de A à B. Je n'aime pas vraiment l'utilisation de "up" et "down" et "parent" et "enfant" lorsque je navigue dans une hiérarchie de classes et je les ai mal tout le temps dans mon écriture. La relation entre les types est une relation sur-ensemble / sous-ensemble, alors je ne sais pas pourquoi il devrait y avoir une direction ascendante ou descendante. Et ne me lance même pas sur "parent". Le parent de Giraffe n'est pas un mammifère, le parent de Giraffe est M. et Mme Giraffe.
Eric Lippert
9
Object.Equalsest horrible . Le calcul de l'égalité en C # est complètement foiré , beaucoup trop foutu pour l'expliquer dans un commentaire. Il y a tellement de façons de représenter l'égalité. il est très facile de les rendre incompatibles; il est très facile, en effet nécessaire de violer les propriétés requises d'un opérateur d'égalité (symétrie, réflexivité et transitivité). Il n'y aurait pas la conception d' un si je étais système de type à partir de zéro aujourd'hui Equals(ou GetHashcodeou ToString) sur Objectdu tout.
Eric Lippert
33

Les gestionnaires d'événements ont généralement la signature MethodName(object sender, EventArgs e). Dans certains cas, il est possible de gérer l'événement sans tenir compte du type sender, voire de ne pas utiliser senderdu tout, mais dans d'autres cas, il senderdoit être converti en un type plus spécifique pour gérer l'événement.

Par exemple, vous pouvez avoir deux TextBoxs et utiliser un seul délégué pour gérer un événement de chacun d'eux. Ensuite, vous devez transtyper senderen un TextBoxafin de pouvoir accéder aux propriétés nécessaires pour gérer l'événement:

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

Oui, dans cet exemple, vous pouvez créer votre propre sous-classe, TextBoxqui l’a fait en interne. Pas toujours pratique ou possible, cependant.

mmathis
la source
2
Merci, c'est un bon exemple. L' TextChangedévénement est un membre de Control- je me demande pourquoi le type sendern'est pas Controlau lieu de type object? De plus, je me demande si (théoriquement) le cadre aurait pu redéfinir l'événement pour chaque sous-classe (afin que, par exemple, TextBoxune nouvelle version de l'événement puisse être utilisée, en utilisant TextBoxle type d'expéditeur) ... qui pourrait (ou pourrait ne pas) nécessiter un downcasting ( to TextBox) à l'intérieur de l' TextBoximplémentation (pour implémenter le nouveau type d'événement), mais éviterait de demander un downcast dans le gestionnaire d'événements du code de l'application.
ChrisW
3
Cela est considéré comme acceptable car il est difficile de généraliser le traitement des événements si chaque événement a sa propre signature de méthode. Bien que je suppose que ce soit une limitation acceptée plutôt que quelque chose qui est considéré comme la meilleure pratique car l’alternative est en réalité plus gênante. S'il était possible de commencer avec un code TextBox senderplutôt que de ne object senderpas trop compliquer le code, je suis sûr que cela serait fait.
Neil
6
@Neil Les systèmes de gestion des événements sont spécialement conçus pour permettre le transfert de fragments de données arbitraires vers des objets arbitraires. C'est quasiment impossible à faire sans vérification à l'exécution du type.
Joker_vD
@Joker_vD L'idéal serait que l'API prenne en charge les signatures de rappel fortement typées à l'aide de contravariance générique. Le fait que .NET (à une écrasante majorité) ne le fasse pas est simplement dû au fait que les génériques et la covariance ont été introduits plus tard. - En d'autres termes, la décroissance ici (et généralement ailleurs) est nécessaire en raison d'insuffisances de langage / API, et non parce que c'est fondamentalement une bonne idée.
Konrad Rudolph
@ KonradRudolph Bien sûr, mais comment stockeriez-vous des événements de types arbitraires dans la file d'attente des événements? Est-ce que vous venez de vous débarrasser de la file d'attente et optez pour l'envoi direct?
Joker_vD
17

Vous avez besoin d'un downcasting lorsque quelque chose vous donne un super-type et que vous devez le gérer différemment en fonction du sous-type.

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

Non, ça ne sent pas bon. Dans un monde parfait, il Donationy aurait une méthode abstraite getValueet chaque sous-type implémenté aurait une implémentation appropriée.

Mais que se passe-t-il si ces classes proviennent d'une bibliothèque que vous ne pouvez pas ou ne voulez pas changer? Alors il n'y a pas d'autre option.

Philipp
la source
Cela semble être un bon moment pour utiliser le modèle de visiteur. Ou le dynamicmot clé qui donne le même résultat.
user2023861
3
@ user2023861 Non, je pense que le modèle de visiteur dépend de la coopération des sous-classes Donation - c’est-à-dire que Donation doit déclarer un résumé void accept(IDonationVisitor visitor), que ses sous-classes implémentent en appelant des méthodes spécifiques (surchargées, par exemple) IDonationVisitor.
ChrisW
@ChrisW, vous avez raison à ce sujet. Sans coopération, vous pouvez toujours le faire en utilisant le dynamicmot - clé.
user2023861
Dans ce cas particulier, vous pouvez ajouter une méthode estimationValue à Donation.
user253751
@ user2023861: dynamictoujours en baisse , juste plus lent.
Mehrdad
12

Pour ajouter à la réponse d'Eric Lippert puisque je ne peux pas commenter ...

Vous pouvez souvent éviter le downcasting en restructurant une API pour qu'elle utilise des génériques. Mais les génériques n’ont pas été ajoutés à la langue jusqu’à la version 2.0 . Ainsi, même si votre propre code n'a pas besoin de prendre en charge les versions de langue ancienne, vous pouvez vous retrouver à utiliser des classes héritées qui ne sont pas paramétrées. Par exemple, certaines classes peuvent être définies pour porter une charge utile de type Object, que vous êtes obligé de transtyper vers le type que vous connaissez réellement.

Non U
la source
En effet, on pourrait dire que les génériques en Java ou en C # ne sont essentiellement que des moyens sûrs de stocker des objets de superclasse et de les attribuer à la sous-classe correcte (cochée par le compilateur) avant de réutiliser les éléments. (Contrairement aux modèles de C, qui réécrivent le code à utiliser toujours la sous - classe elle - même et éviter ainsi d' avoir à baissés - ce qui peut améliorer les performances de la mémoire, si souvent au détriment du temps de compilation et taille de l' exécutable.)
leftaroundabout
4

Il existe un compromis entre les langages statiques et à typage dynamique. Le typage statique fournit au compilateur de nombreuses informations lui permettant de donner des garanties assez fortes quant à la sécurité des (parties de) programmes. Cela a toutefois un coût, car non seulement vous devez savoir que votre programme est correct, mais vous devez écrire le code approprié pour convaincre le compilateur que tel est le cas. En d’autres termes, il est plus facile de faire des réclamations que de les prouver.

Il existe des constructions "non sûres" qui vous aident à faire des assertions non prouvées au compilateur. Par exemple, appels inconditionnels à Nullable.Value, downcasts, dynamicobjets, etc. , sans condition . Ils vous permettent d'affirmer une revendication ("J'affirme que l'objet aest un String, si je me trompe, jette un InvalidCastException") sans qu'il soit nécessaire de le prouver. Cela peut être utile dans les cas où il est beaucoup plus difficile de prouver que cela vaut la peine.

Abuser de cela est risqué, c'est précisément pourquoi la notation explicite à la baisse existe et est obligatoire; c'est un sel syntaxique destiné à attirer l'attention sur une opération non sécurisée. Le langage aurait pu être implémenté avec un downcasting implicite (où le type inféré est sans ambiguïté), mais cela masquerait cette opération non sécurisée, ce qui n'est pas souhaitable.

Alexandre
la source
Je suppose que je demande alors, pour quelques exemples, où / quand il est nécessaire ou souhaitable (c'est-à-dire une bonne pratique) de faire ce type "d'affirmation non prouvée".
ChrisW
1
Qu'en est-il du résultat d'une opération de réflexion? stackoverflow.com/a/3255716/3141234
Alexander
3

La décadence est considérée comme mauvaise pour plusieurs raisons. Principalement, je pense parce que c'est anti-POO.

OOP aimerait vraiment si vous n’avez jamais à baisser les bras, car sa raison d’être est que le polymorphisme signifie que vous n’êtes pas obligé de partir.

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

tu fais juste

x.DoThing()

et le code fait comme par magie la bonne chose.

Il y a des raisons «difficiles» à ne pas abaisser:

  • En C ++, c'est lent.
  • Vous obtenez une erreur d'exécution si vous choisissez un type incorrect.

Mais l’alternative au downcasting dans certains scénarios peut être assez laide.

L'exemple classique est le traitement des messages, dans lequel vous ne souhaitez pas ajouter la fonction de traitement à l'objet, mais la conserver dans le processeur de messages. J'ai alors une tonne de données MessageBaseClassdans un tableau à traiter, mais j'ai également besoin de chaque sous-type pour un traitement correct.

Vous pouvez utiliser Double Dispatch pour résoudre le problème ( https://en.wikipedia.org/wiki/Double_dispatch ) ... Mais cela a aussi ses problèmes. Vous pouvez également vous contenter d'un code simple au risque de ces raisons difficiles.

Mais c'était avant l'invention des génériques. Maintenant, vous pouvez éviter le downcasting en fournissant un type où les détails spécifiés ultérieurement.

MessageProccesor<T> peut modifier les paramètres de sa méthode et retourner des valeurs en fonction du type spécifié, tout en fournissant une fonctionnalité générique

Bien sûr, personne ne vous oblige à écrire du code POO et de nombreuses fonctionnalités linguistiques sont fournies, mais mal vues, telles que la réflexion.

Ewan
la source
1
Je doute que ce soit lent en C ++, surtout si vous static_castau lieu de dynamic_cast.
ChrisW
"Traitement du message" - Je pense que cela signifie, par exemple, un ciblage lParamsur quelque chose de spécifique. Je ne suis cependant pas sûr que ce soit un bon exemple en C #.
ChrisW
3
@ChrisW - eh bien, oui, static_castc'est toujours rapide ... mais il n'est pas garanti de ne pas échouer de manière indéfinie si vous commettez une erreur et que le type n'est pas ce que vous attendez.
Jules
@ Jules: C'est complètement à côté du sujet.
Mehrdad
@ Mehrdad, c'est exactement le point. faire l'équivalent c # cast en c ++ est lent. et c'est la raison traditionnelle contre le casting. le static_cast alternatif n'est pas équivalent et considéré comme le pire choix en raison du comportement indéfini
Ewan
0

En plus de tout ce qui a été dit précédemment, imaginez une propriété Tag de type objet. Il vous permet de stocker un objet de votre choix dans un autre objet pour une utilisation ultérieure, selon vos besoins. Vous avez besoin d'abaisser cette propriété.

En général, ne pas utiliser quelque chose jusqu'à ce jour n'est pas toujours une indication d'inutile ;-)

palet
la source
Oui, voir par exemple Quelle utilisation est la propriété Tag dans .net . Dans l'une des réponses, quelqu'un a écrit: je l'utilisais pour entrer des instructions à l'utilisateur dans les applications Windows Forms. Lorsque l'événement de contrôle GotFocus était déclenché, la propriété de ma propriété Tag de contrôle, qui contenait la chaîne d'instruction, était affectée à la propriété d'instructions Label.Text. Je suppose qu’une autre façon d’étendre le Control(au lieu d’utiliser une object Tagpropriété) serait de créer un répertoire Dictionary<Control, string>dans lequel stocker l’étiquette pour chaque contrôle.
ChrisW
1
J'aime cette réponse car elle Tagest une propriété publique d'une classe Framework .Net, ce qui montre que vous êtes (plus ou moins) censé l'utiliser.
ChrisW
@ChrisW: Pour ne pas dire que la conclusion est fausse (ou juste), mais notez que c'est un raisonnement bâclé, car de nombreuses fonctionnalités .NET publiques sont obsolètes (par exemple System.Collections.ArrayList) ou dépréciées (par exemple IEnumerator.Reset).
Mehrdad
0

L'utilisation la plus commune de la décroissance dans mon travail est due à un idiot qui a brisé le principe de substitution de Liskov.

Imaginez qu'il y ait une interface dans une bibliothèque tierce.

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

Et 2 classes qui l'implémentent. Baret Qux. Barest bien comporté.

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

Mais Quxne se comporte pas bien.

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

Eh bien ... maintenant je n'ai pas le choix. Je dois taper check et (éventuellement) downcast afin d'éviter que mon programme ne s'arrête brusquement.

Canard en caoutchouc
la source
1
Pas vrai pour cet exemple particulier. Vous pouvez simplement intercepter l'exception NotImplementedException lorsque vous appelez SayGoodbye.
Par von Zweigbergk le
C'est peut-être un exemple trop simpliste, mais travaillez avec certaines des anciennes API .Net et vous rencontrerez cette situation. Parfois, le moyen le plus simple d’écrire le code est de procéder à un casting sécurisé var qux = foo as Qux;.
RubberDuck
0

WPF est un autre exemple concret VisualTreeHelper. Il utilise downcast pour lancer DependencyObjectvers Visualet Visual3D. Par exemple, VisualTreeHelper.GetParent . Le downcast se produit dans 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
la source