web-dev-qa-db-fra.com

Qu'est-ce qu'une utilisation appropriée de la conversion descendante?

La conversion descendante signifie la conversion d'une classe de base (ou interface) en une sous-classe ou une classe feuille.

Un exemple de downcast pourrait être si vous effectuez un cast à partir de System.Object vers un autre type.

Le downcasting est impopulaire, peut-être une odeur de code: Object Oriented doctrine est de préférer, par exemple, définir et appeler des méthodes virtuelles ou abstraites au lieu du downcasting.

  • Quels sont, le cas échéant, les cas d'utilisation bons et appropriés pour la conversion descendante? Autrement dit, dans quelle (s) circonstance (s) convient-il d'écrire du code qui rétrograde?
  • Si votre réponse est "aucune", alors pourquoi le downcasting est-il pris en charge par la langue?
71
ChrisW

Voici quelques utilisations appropriées du downcasting.

Et respectueusement ( avec véhémence en désaccord avec d'autres ici qui disent que l'utilisation des downcasts est définitivement une odeur de code, car je pense 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 voulez dire que ce sont des odeurs de code, vous devez leur fournir de meilleures solutions (même si elles sont évitées en raison de désagréments). Je ne crois pas qu'il existe de meilleures solutions.

13
user541686

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

Je ne suis pas d'accord. Le downcasting est extrêmement populaire ; un grand nombre de programmes du monde réel contiennent un ou plusieurs downcasts. Et ce n'est pas peut-être une odeur de code. C'est définitivement 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 y porter attention.

dans quelles circonstances convient-il d'écrire du code qui rétrograde?

En toute circonstance où:

  • vous avez une connaissance 100% correcte d'un fait sur le type d'exécution d'une expression qui est plus spécifique que le type de compilation de l'expression, et
  • vous devez tirer parti de ce fait afin d'utiliser une capacité de l'objet non disponible sur le type au moment de la compilation, et
  • c'est une meilleure utilisation du temps et des efforts pour écrire le casting que de refactoriser le programme pour éliminer l'un des deux premiers points.

Si vous pouvez refactoriser à moindre coût un programme afin que le type d'exécution puisse être déduit par le compilateur, ou pour refactoriser le programme afin que vous n'ayez pas besoin de la capacité du type le plus dérivé, faites-le. Des downcasts ont été ajoutés au langage pour les circonstances où il est difficile et cher pour refactoriser ainsi le programme.

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

C # a été inventé par des programmeurs pragmatiques qui ont du travail à faire, pour des programmeurs pragmatiques qui ont du travail à faire. Les concepteurs C # ne sont pas OO puristes. Et le système de type C # n'est pas parfait; de par sa conception, il sous-estime les restrictions qui peuvent être placées sur le 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 et s'il ne peut pas être vérifié, le programme fait ce qu'il faut et se bloque. C'est merveilleux; cela signifie que si votre compréhension à 100% de la sémantique de type 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 0,01% du temps .


EXERCICE: Il existe au moins une façon de produire un downcast en C # sans en utilisant un opérateur de cast explicite. Pouvez-vous penser à de tels scénarios? Étant donné que ce sont également des odeurs potentielles de code, quels sont les facteurs de conception qui, selon vous, ont été utilisés dans la conception d'une fonctionnalité qui pourrait produire un crash vers le bas sans avoir de fonte manifeste dans le code?

139
Eric Lippert

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, ou même sans utiliser sender du tout, mais dans d'autres sender doit être converti en plus -type spécifique pour gérer l'événement.

Par exemple, vous pouvez avoir deux TextBox et vous souhaitez utiliser un seul délégué pour gérer un événement de chacun d'eux. Ensuite, vous devrez convertir sender en TextBox afin 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 de TextBox qui le fait en interne. Pas toujours pratique ou possible, cependant.

33
mmathis

Vous avez besoin de downcasting quand quelque chose vous donne un supertype et vous devez le gérer différemment selon le 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, cela ne sent pas bon. Dans un monde parfait, Donation aurait une méthode abstraite getValue et chaque sous-type d'implémentation aurait une implémentation appropriée.

Mais que faire si ces classes proviennent d'une bibliothèque que vous ne pouvez pas ou ne voulez pas changer? Ensuite, il n'y a pas d'autre option.

17
Philipp

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

Vous pouvez souvent éviter le downcasting en refactorisant une API pour utiliser des génériques. Mais les génériques n'ont pas été ajoutés à la langue jusqu'à la version 2. . Ainsi, même si votre propre code n'a pas besoin de prendre en charge les versions de langues anciennes, vous pouvez vous retrouver à utiliser des classes héritées qui ne sont pas paramétrées. Par exemple, une classe peut être définie pour transporter une charge utile de type Object, que vous êtes obligé de convertir en le type que vous savez qu'elle est réellement.

12
StackOverthrow

Il existe un compromis entre les langages statiques et les langages typés dynamiquement. Le typage statique donne au compilateur beaucoup d'informations pour être en mesure de faire des garanties assez fortes sur la sécurité de (parties de) programmes. Cela a cependant 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 c'est le cas. En d'autres termes, il est plus facile de faire des réclamations que de les prouver.

Il existe des constructions "dangereuses" qui vous aident à faire des assertions non prouvées au compilateur. Par exemple, des appels inconditionnels à Nullable.Value , downcasts inconditionnels, dynamic objets, etc. Ils vous permettent d'affirmer une revendication ("J'affirme que cet objet a est un String, si je" m mal, lancez un InvalidCastException ") sans avoir besoin de le prouver. Cela peut être utile dans les cas où il est beaucoup plus difficile de prouver que cela en vaut la peine.

Abuser de cela est risqué, c'est exactement pourquoi la notation explicite vers le bas existe et est obligatoire; c'est sel syntaxique destiné à attirer l'attention sur une opération dangereuse. Le langage aurait pu avoir été implémenté avec un downcasting implicite (où le type inféré est sans ambiguïté), mais cela cacherait cette opération dangereuse, ce qui n'est pas souhaitable.

Le downcasting est considéré comme mauvais pour plusieurs raisons. Je pense principalement parce que c'est anti-OOP.

La POO aimerait vraiment que vous n'ayez jamais à abattre car sa `` raison d'être '' est que le polymorphisme signifie que vous n'avez pas à y aller

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 automatiquement la bonne chose.

Il existe des raisons "difficiles" de ne pas abattre:

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

Mais l'alternative au downcasting dans certains scénarios peut être assez moche.

L'exemple classique est le traitement des messages, où vous ne voulez pas ajouter la fonction de traitement à l'objet, mais la conservez dans le processeur de messages. J'ai ensuite une tonne de MessageBaseClass dans un tableau à traiter, mais j'ai également besoin de chacun par sous-type pour un traitement correct.

Vous pouvez utiliser Double Dispatch pour contourner le problème ( https://en.wikipedia.org/wiki/Double_dispatch ) ... Mais cela a aussi ses problèmes. Ou vous pouvez simplement abattre pour un code simple au risque de ces raisons difficiles.

Mais c'était avant l'invention des génériques. Désormais, vous pouvez éviter la rétrogradation en fournissant un type contenant les détails spécifiés ultérieurement.

MessageProccesor<T> peut faire varier ses paramètres de méthode et renvoyer des valeurs selon le type spécifié, mais fournit toujours des fonctionnalités génériques

Bien sûr, personne ne vous oblige à écrire du code OOP et de nombreuses fonctionnalités de langage sont fournies mais désapprouvées, comme la réflexion.

3
Ewan

En plus de tout ce qui précède, 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 devez abattre cette propriété.

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

0
puck

L’utilisation la plus courante du downcasting dans mon travail est due à un idiot qui brise le principe de substitution de Liskov.

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

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

Et 2 classes qui l'implémentent. Bar et Qux. Bar se comporte bien.

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

Mais Qux ne 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 ai pour taper check et (éventuellement) downcast afin d'éviter que mon programme ne s'arrête brutalement.

0
RubberDuck

Un autre exemple réel est VisualTreeHelper de WPF. Il utilise downcast pour convertir DependencyObject en Visual et 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;
}
0
zwcloud