Pourquoi éviter le casting? [fermé]

97

J'évite généralement autant que possible le casting de types car j'ai l'impression que c'est une mauvaise pratique de codage et que cela peut entraîner une pénalité de performance.

Mais si quelqu'un me demandait d'expliquer pourquoi exactement, je les regarderais probablement comme un cerf dans les phares.

Alors pourquoi / quand le casting est-il mauvais?

Est-ce général pour java, c #, c ++ ou est-ce que chaque environnement d'exécution le gère selon ses propres conditions?

Les spécificités d'un langage any sont les bienvenues, par exemple pourquoi est-il mauvais en C ++?

FortNPeut-être Mauvais
la source
3
Où avez-vous eu cette impression?
Oded
8
J'ai eu cette impression car je n'ai jamais lu un livre ou rencontré un programmeur qui disait "CASTING SOOOO GOOOOD !!!"
LoudNPossiblementWrong
15
La réponse pour C ++ est inévitablement différente de la réponse pour C #. C'est très spécifique à la langue. Les réponses à ce jour répondent à cette question pour des langues spécifiques et, dans certains cas, n'indiquent pas de quelle langue elles parlent.
James McNellis
2
Mauvais est un terme relatif. Éviter le casting est une bonne pratique, mais parfois un programmeur doit faire ce qu'un programmeur doit faire. (surtout si vous écrivez un programme java 1.5+ qui utilise une bibliothèque écrite pour 1.4) Renommez peut-être la question, "Pourquoi devrait-on éviter le casting?"
Mike Miller
2
C'est une excellente question .... Elle a été fermée par 5 utilisateurs qui ont chacun moins de 10k de réputation !! Une utilisation trop zélée de nouveaux pouvoirs, clairement. Ont voté pour la réouverture.
reach4thelasers

Réponses:

141

Vous l'avez étiqueté avec trois langues, et les réponses sont vraiment très différentes entre les trois. La discussion de C ++ implique plus ou moins une discussion sur les castes en C, ce qui donne (plus ou moins) une quatrième réponse.

Comme c'est celui que vous n'avez pas mentionné explicitement, je vais commencer par C. Les castings ont un certain nombre de problèmes. La première est qu'ils peuvent faire n'importe laquelle de plusieurs choses différentes. Dans certains cas, le casting ne fait rien de plus que de dire au compilateur (en substance): "tais-toi, je sais ce que je fais" - c'est-à-dire qu'il garantit que même lorsque vous effectuez une conversion qui pourrait causer des problèmes, le compilateur ne vous avertira pas de ces problèmes potentiels. Juste par exemple char a=(char)123456;,. Le résultat exact de cette implémentation défini (dépend de la taille et de la signature dechar), et sauf dans des situations plutôt étranges, n'est probablement pas utile. Les casts C varient également selon qu'il s'agit de quelque chose qui ne se produit qu'au moment de la compilation (c'est-à-dire que vous dites simplement au compilateur comment interpréter / traiter certaines données) ou quelque chose qui se produit au moment de l'exécution (par exemple, une conversion réelle de double à longue).

C ++ tente de traiter cela au moins dans une certaine mesure en ajoutant un certain nombre de «nouveaux» opérateurs de conversion, dont chacun est limité à seulement un sous-ensemble des capacités d'un cast C. Cela rend plus difficile (par exemple) de faire accidentellement une conversion que vous n'aviez vraiment pas l'intention - si vous avez seulement l' intention de rejeter la constness sur un objet, vous pouvez l'utiliser const_cast, et assurez-vous que la seule chose que cela puisse affecter est de savoir si un objet est const, volatileou non. Inversement, a static_castn'est pas autorisé à affecter si un objet est constouvolatile. En bref, vous avez la plupart des mêmes types de capacités, mais elles sont catégorisées de sorte qu'une distribution ne peut généralement effectuer qu'un seul type de conversion, où une seule distribution de style C peut effectuer deux ou trois conversions en une seule opération. La principale exception est que vous pouvez utiliser un dynamic_castà la place d'un static_castdans au moins certains cas et bien qu'il soit écrit en tant que dynamic_cast, il finira vraiment par devenir un static_cast. Par exemple, vous pouvez utiliser dynamic_castpour parcourir une hiérarchie de classes vers le haut ou vers le bas - mais une conversion «vers le haut» de la hiérarchie est toujours sûre, donc cela peut être fait de manière statique, tandis qu'une conversion «vers le bas» de la hiérarchie n'est pas nécessairement sûre, donc c'est fait dynamiquement.

Java et C # sont beaucoup plus similaires l'un à l'autre. En particulier, avec les deux, la coulée est (pratiquement?) Toujours une opération d'exécution. En termes d'opérateurs de cast C ++, il est généralement plus proche de a dynamic_casten termes de ce qui est vraiment fait - c'est-à-dire que lorsque vous essayez de convertir un objet en un type de cible, le compilateur insère une vérification à l'exécution pour voir si cette conversion est autorisée , et lancez une exception si ce n'est pas le cas. Les détails exacts (par exemple, le nom utilisé pour l'exception de «mauvaise distribution») varient, mais le principe de base reste essentiellement similaire (bien que, si la mémoire est intbonne , Java effectue des transtypages appliqués aux quelques types non-objets comme beaucoup plus proches de C casts - mais ces types sont assez rarement utilisés pour que 1) je ne m'en souviens pas avec certitude, et 2) même si c'est vrai, cela n'a pas beaucoup d'importance de toute façon).

En regardant les choses plus généralement, la situation est assez simple (du moins IMO): un casting (évidemment assez) signifie que vous convertissez quelque chose d'un type à un autre. Quand / si vous faites cela, cela soulève la question "Pourquoi?" Si vous voulez vraiment que quelque chose soit d'un type particulier, pourquoi ne l'avez-vous pas défini comme étant ce type pour commencer? Cela ne veut pas dire qu'il n'y a jamais de raison de faire une telle conversion, mais chaque fois que cela se produit, cela devrait poser la question de savoir si vous pouvez reconcevoir le code pour que le type correct soit utilisé partout. Même les conversions apparemment anodines (par exemple, entre un nombre entier et une virgule flottante) devraient être examinées beaucoup plus attentivement que ce qui est courant. Malgré leur apparencesimilitude, les entiers devraient vraiment être utilisés pour les types de choses «comptés» et la virgule flottante pour les types de choses «mesurées». Ignorer la distinction est ce qui conduit à certaines des déclarations folles comme «la famille américaine moyenne a 1,8 enfants». Même si nous pouvons tous voir comment cela se produit, le fait est qu'aucune famille n'a 1,8 enfant. Ils peuvent avoir 1 ou 2 ou ils peuvent en avoir plus - mais jamais 1.8.

Jerry Coffin
la source
10
On dirait que vous souffrez de «mort par durée d'attention» ici, c'est une bonne réponse imo.
Steve Townsend
2
Une réponse forte, mais quelques-unes des parties «Je ne sais pas» pourraient être resserrées pour la rendre complète. @ Dragontamer5788 c'est une bonne réponse, mais pas exhaustive.
M2tM
5
Un enfant entier et un enfant avec une jambe manquante seraient 1,8 enfants. Mais très belle réponse néanmoins. :)
Oz.
1
Une très belle réponse pas gênée du tout en excluant les couples sans enfants.
@Roger: ne pas exclure, juste ignorer.
Jerry Coffin
48

Beaucoup de bonnes réponses ici. Voici ma façon de voir les choses (du point de vue C #).

Casting signifie généralement l'une des deux choses suivantes:

  • Je connais le type d'exécution de cette expression mais le compilateur ne le sait pas. Compilateur, je vous le dis, à l'exécution, l'objet qui correspond à cette expression va vraiment être de ce type. À partir de maintenant, vous savez que cette expression doit être traitée comme étant de ce type. Générez du code qui suppose que l'objet sera du type donné, ou lancez une exception si je me trompe.

  • Le compilateur et le développeur connaissent le type d'exécution de l'expression. Il existe une autre valeur d'un type différent associée à la valeur que cette expression aura au moment de l'exécution. Générer du code qui produit la valeur du type souhaité à partir de la valeur du type donné; si vous ne pouvez pas le faire, lancez une exception.

Notez que ce sont des opposés . Il existe deux types de moulages! Il y a des casts où vous donnez un indice au compilateur sur la réalité - hé, cette chose de type objet est en fait de type Client - et il y a des casts où vous dites au compilateur d' effectuer un mappage d'un type à un autre - hé, J'ai besoin de l'int qui correspond à ce double.

Les deux types de moulages sont des drapeaux rouges. Le premier type de distribution soulève la question "pourquoi exactement le développeur sait-il quelque chose que le compilateur ne sait pas?" Si vous êtes dans cette situation , alors la meilleure chose à faire est généralement de modifier le programme afin que le compilateur ne possède une poignée sur la réalité. Alors vous n'avez pas besoin du casting; l'analyse est effectuée au moment de la compilation.

Le deuxième type de conversion soulève la question "pourquoi l'opération n'est-elle pas effectuée dans le type de données cible en premier lieu?" Si vous avez besoin d'un résultat en ints, pourquoi détenez-vous un double en premier lieu? Ne devriez-vous pas tenir un int?

Quelques réflexions supplémentaires ici:

http://blogs.msdn.com/b/ericlippert/archive/tags/cast+operator/

Eric Lippert
la source
2
Je ne pense pas que ce soient des signaux d'alarme intrinsèques. Si vous avez un Fooobjet qui hérite de Bar, et que vous le stockez dans un List<Bar>, vous aurez besoin de transtypages si vous le souhaitez Foo. Cela indique peut-être un problème au niveau architectural (pourquoi stockons-nous Bars au lieu de Foos?), Mais pas nécessairement. Et, si cela a Fooégalement un cast valide int, il traite également de votre autre commentaire: vous stockez un Foo, pas un int, car an intn'est pas toujours approprié.
Mike Caron
6
@Mike Caron - Je ne peux pas répondre pour Eric, évidemment, mais pour moi, un drapeau rouge signifie "c'est quelque chose à penser", pas "c'est quelque chose qui ne va pas". Et il n'y a aucun problème à stocker un Foodans un List<Bar>, mais au moment de la distribution, vous essayez de faire quelque chose Fooqui n'est pas approprié pour un Bar. Cela signifie que le comportement différent pour les sous-types est effectué via un mécanisme autre que le polymorphisme intégré fourni par les méthodes virtuelles. C'est peut-être la bonne solution, mais le plus souvent, c'est un signal d'alarme.
Jeffrey L Whitledge
14
En effet. Si vous retirez des choses d'une liste d'animaux et que vous devez plus tard dire au compilateur, oh, au fait, je sais que le premier est un tigre, le second est un lion et le troisième est un ours, alors vous auriez dû utiliser un Tuple <Lion, Tiger, Bear>, pas un List <Animal>.
Eric Lippert
5
Je suis d'accord avec vous, mais je pense que sans un meilleur support syntaxique pour les Tuple<X,Y,...>tuples, il est peu probable que l'utilisation généralisée en C # soit généralisée. C'est un endroit où la langue pourrait faire un meilleur travail pour pousser les gens vers le «gouffre du succès».
kvb
4
@kvb: Je suis d'accord. Nous avons envisagé d'adopter une syntaxe de tuple pour C # 4 mais cela ne cadrait pas avec le budget. Peut-être en C # 5; nous n'avons pas encore élaboré l'ensemble complet des fonctionnalités. Trop occupé à rassembler le CTP pour asynchrone. Ou peut-être dans une version future hypothétique.
Eric Lippert
36

Les erreurs de diffusion sont toujours signalées comme des erreurs d'exécution dans java. L'utilisation de génériques ou de modèles transforme ces erreurs en erreurs de compilation, ce qui facilite grandement la détection lorsque vous avez commis une erreur.

Comme je l'ai dit plus haut. Cela ne veut pas dire que tout casting est mauvais. Mais s'il est possible de l'éviter, il vaut mieux le faire.

Mike Miller
la source
4
Cela suppose que tout casting est "mauvais"; cependant, ce n'est pas entièrement vrai. Prenez C #, par exemple, avec une prise en charge à la fois implicite et explicite de la distribution. Le problème survient lorsqu'un casting est effectué qui (accidentellement) supprime des informations ou la sécurité de type (cela diffère selon la langue).
5
En fait, c'est complètement faux en C ++. Peut-être qu'une modification pour inclure la ou les langues ciblées avec ces informations est en ordre.
Steve Townsend
@Erick - Je le ferais, mais je ne connais pas la première chose à propos de Java, ni assez de détails C # pour être sûr que les informations sont correctes.
Steve Townsend
Désolé, je suis un gars Java, donc mes connaissances en C ++ sont assez limitées. Je pourrais éditer le mot «modèle» en dehors de celui-ci, car il était censé être vague.
Mike Miller
Non. Certaines erreurs de conversion sont interceptées par le compilateur Java, et pas seulement par des erreurs génériques. Considérez (String) 0;
Marquis de Lorne
18

Le casting n'est pas intrinsèquement mauvais, c'est juste qu'il est souvent utilisé à mauvais escient comme un moyen de réaliser quelque chose qui ne devrait vraiment pas être fait du tout ou de manière plus élégante.

S'il était universellement mauvais, les langues ne le prendraient pas en charge. Comme toute autre fonctionnalité du langage, il a sa place.

Mon conseil serait de vous concentrer sur votre langue principale et de comprendre tous ses moulages et les meilleures pratiques associées. Cela devrait informer les excursions dans d'autres langues.

Les documents C # pertinents sont ici .

Il y a un excellent résumé sur les options C ++ lors d'une précédente question SO ici .

Steve Townsend
la source
9

Je parle principalement de C ++ ici, mais la plupart de cela s'applique probablement aussi à Java et C #:

C ++ est un langage typé statiquement . Il y a quelques marges de manœuvre que le langage vous permet dans ce domaine (fonctions virtuelles, conversions implicites), mais fondamentalement, le compilateur connaît le type de chaque objet au moment de la compilation. La raison d'utiliser un tel langage est que des erreurs peuvent être détectées au moment de la compilation . Si le compilateur connaît les types de aet b, il vous attrapera au moment de la compilation lorsque vous faites a=baest un nombre complexe et best une chaîne.

Chaque fois que vous effectuez un casting explicite, vous dites au compilateur de se taire, car vous pensez que vous savez mieux . Au cas où vous vous tromperiez, vous ne le découvrirez généralement qu'au moment de l'exécution . Et le problème avec la découverte au moment de l'exécution est que cela peut être chez un client.

sbi
la source
5

Java, c # et c ++ sont des langages fortement typés, bien que les langages fortement typés puissent être considérés comme inflexibles, ils ont l'avantage d'effectuer une vérification de type au moment de la compilation et vous protègent contre les erreurs d'exécution causées par le mauvais type pour certaines opérations.

Il existe essentiellement deux types de moulages: un casting vers un type plus général ou un casting vers un autre type (plus spécifique). La conversion vers un type plus général (conversion vers un type parent) laissera les vérifications de la compilation intactes. Mais le cast vers d'autres types (types plus spécifiques) désactivera la vérification du type à la compilation et sera remplacé par le compilateur par une vérification à l'exécution. Cela signifie que vous avez moins de certitude que le code compilé fonctionnera correctement. Il a également un impact négligeable sur les performances, en raison de la vérification supplémentaire du type d'exécution (l'API Java est pleine de casts!).

Kdeveloper
la source
5
Tous les caste la sécurité de type "contournement" en C ++.
James McNellis
4
Ce ne sont pas tous les cas de sécurité de type «contournement» en C #.
5
Ce ne sont pas tous les casts de sécurité de type «bypass» en Java.
Erick Robertson
11
Pas tous les lancers de sécurité de type «bypass» dans Casting. Oh attendez, cette balise ne fait pas référence à une langue ...
sbi
2
Dans presque tous les cas en C # et Java, le cast vous donnera une dégradation des performances, car le système effectuera une vérification de type à l'exécution (ce qui n'est pas gratuit). En C ++, dynamic_castest généralement plus lent que static_cast, car il doit généralement effectuer une vérification de type au moment de l'exécution (avec quelques mises en garde: la conversion vers un type de base est bon marché, etc.).
Travis Gockel
4

Certains types de moulage sont si sûrs et efficaces qu'ils ne sont souvent même pas considérés comme des moulages.

Si vous transtypez d'un type dérivé vers un type de base, cela est généralement assez bon marché (souvent - en fonction de la langue, de l'implémentation et d'autres facteurs - cela ne coûte rien) et est sûr.

Si vous transtypez d'un type simple comme un int vers un type plus large comme un long int, alors encore une fois, il est souvent assez bon marché (généralement pas beaucoup plus cher que d'attribuer le même type que celui auquel il est casté) et est encore une fois sûr.

D'autres types sont plus compliqués et / ou plus chers. Dans la plupart des langages, la conversion d'un type de base en un type dérivé est soit bon marché, mais présente un risque élevé d'erreur grave (en C ++, si vous diffusez de manière statique de la base vers un dérivé, elle sera bon marché, mais si la valeur sous-jacente n'est pas du type dérivé, le comportement est indéfini et peut être très étrange) ou relativement coûteux et avec un risque de déclencher une exception (dynamic_cast en C ++, conversion explicite de base en dérivée en C #, etc.). La boxe en Java et C # en est un autre exemple, et une dépense encore plus élevée (étant donné qu'elles changent plus que la façon dont les valeurs sous-jacentes sont traitées).

D'autres types de conversion peuvent perdre des informations (un type entier long en un type entier court).

Ces cas de risque (qu'il s'agisse d'une exception ou d'une erreur plus grave) et de frais sont autant de raisons pour éviter le casting.

Une raison plus conceptuelle, mais peut-être plus importante, est que chaque cas de casting est un cas où votre capacité à raisonner sur l'exactitude de votre code est bloquée: chaque cas est un autre endroit où quelque chose peut mal tourner, et la manière dont cela peut se tromper, ajouter à la complexité de déduire si le système dans son ensemble va mal tourner. Même si le casting est prouvé en sécurité à chaque fois, prouver que c'est une partie supplémentaire du raisonnement.

Enfin, l'utilisation intensive des moulages peut indiquer un échec à bien prendre en compte le modèle d'objet, que ce soit lors de sa création, de son utilisation ou des deux: la conversion fréquente entre les mêmes types est presque toujours un échec dans la prise en compte des relations entre les types utilisé. Ici, ce n'est pas tant que les lancers sont mauvais, mais plutôt le signe de quelque chose de mauvais.

Jon Hanna
la source
2

Les programmeurs ont de plus en plus tendance à s'accrocher aux règles dogmatiques sur l'utilisation des fonctionnalités du langage ("ne jamais utiliser XXX!", "XXX considéré comme dangereux", etc.), où XXX va de gotos aux pointeurs vers les protecteddonnées membres en passant par les singletons et les objets passant par valeur.

Suivre de telles règles, d'après mon expérience, garantit deux choses: vous ne serez pas un programmeur terrible, ni un grand programmeur.

Une bien meilleure approche consiste à creuser et à découvrir le noyau de vérité derrière ces interdictions générales, puis à utiliser les fonctionnalités judicieusement, en sachant qu'il existe de nombreuses situations pour lesquelles elles sont le meilleur outil pour le travail.

"J'évite généralement autant que possible de lancer des types" est un bon exemple d'une règle aussi généralisée. Les moulages sont essentiels dans de nombreuses situations courantes. Quelques exemples:

  1. Lors de l'interopérabilité avec un code tiers (en particulier lorsque ce code regorge de typedefs). (Exemple: GLfloat<--> double<--> Real.)
  2. Conversion d'un pointeur / référence de classe dérivée vers une classe de base: Ceci est si courant et naturel que le compilateur le fera implicitement. Si le rendre explicite augmente la lisibilité, le casting est un pas en avant, pas en arrière!
  3. Conversion d'une base en pointeur / référence de classe dérivée: également courant, même dans un code bien conçu. (Exemple: conteneurs hétérogènes.)
  4. À l'intérieur de la sérialisation / désérialisation binaire ou de tout autre code de bas niveau qui a besoin d'accéder aux octets bruts des types intégrés.
  5. À tout moment où il est tout simplement plus naturel, pratique et lisible d'utiliser un type différent. (Exemple: std::size_type-> int.)

Il y a certainement de nombreuses situations où il n'est pas approprié d'utiliser un plâtre, et il est important de les apprendre également; Je n'entrerai pas trop dans les détails car les réponses ci-dessus ont fait du bon travail en signalant certaines d'entre elles.

user168715
la source
1

Pour élaborer sur la réponse de KDeveloper , ce n'est pas intrinsèquement sûr de type. Avec la diffusion, il n'y a aucune garantie que ce que vous lancez et vers lequel vous lancez correspondra, et si cela se produit, vous obtiendrez une exception d'exécution, ce qui est toujours une mauvaise chose.

En ce qui concerne spécifiquement C #, car il inclut le isas opérateurs et , vous avez la possibilité (pour la plupart) de déterminer si une conversion réussira ou non. Pour cette raison, vous devez prendre les mesures appropriées pour déterminer si l'opération réussira ou non et se déroulera correctement.

casperOne
la source
1

Dans le cas de C #, il faut être plus prudent lors du casting en raison des frais généraux de boxe / déballage impliqués lors du traitement des types de valeur.

Manish Basantani
la source
1

Je ne sais pas si quelqu'un l'a déjà mentionné, mais en C #, le casting peut être utilisé de manière assez sûre et est souvent nécessaire. Supposons que vous receviez un objet qui peut être de plusieurs types. À l'aide du ismot - clé, vous pouvez d'abord confirmer que l'objet est bien du type vers lequel vous êtes sur le point de le convertir, puis convertir directement l'objet en ce type. (Je n'ai pas beaucoup travaillé avec Java, mais je suis sûr qu'il existe également un moyen très simple de le faire).

user472875
la source
1

Vous ne transtypez un objet vers un type que si 2 conditions sont remplies:

  1. tu sais que c'est de ce type
  2. le compilateur ne le fait pas

Cela signifie que toutes les informations dont vous disposez ne sont pas bien représentées dans la structure de type que vous utilisez. C'est mauvais, car votre implémentation doit comprendre sémantiquement votre modèle, ce qui n'est clairement pas le cas dans ce cas.

Maintenant, lorsque vous faites un casting, cela peut avoir 2 raisons différentes:

  1. Vous avez fait un mauvais travail en exprimant les relations de type.
  2. le système de typage des langues n'est tout simplement pas assez expressif pour les formuler.

Dans la plupart des langues, vous vous retrouvez souvent dans la deuxième situation. Les génériques comme Java aident un peu, le système de modèles C ++ encore plus, mais il est difficile à maîtriser et même dans ce cas, certaines choses peuvent être impossibles ou ne pas valoir la peine.

Vous pourriez donc dire qu'un casting est un sale hack pour contourner vos problèmes pour exprimer une relation de type spécifique dans une langue spécifique. Les hacks sales doivent être évités. Mais vous ne pouvez jamais vivre sans eux.

back2dos
la source
0

En général, les modèles (ou génériques) sont plus sûrs que les casts. À cet égard, je dirais qu'un problème avec le moulage est la sécurité de type. Cependant, il existe un autre problème plus subtil associé en particulier au downcasting: le design. De mon point de vue au moins, le downcasting est une odeur de code, une indication que quelque chose ne va pas avec ma conception et que je devrais enquêter davantage. Pourquoi est-ce simple: si vous «obtenez» les abstractions correctement, vous n'en avez tout simplement pas besoin! Belle question au fait ...

À votre santé!

Lefteris Laskaridis
la source
0

Pour être vraiment concis, une bonne raison est la portabilité. Une architecture différente qui accepte tous les deux la même langue peut avoir, par exemple, des ints de tailles différentes. Donc, si je migre d'ArchA vers ArchB, qui a un int plus étroit, je pourrais voir un comportement étrange au mieux, et un seg défaillant au pire.

(J'ignore clairement le bytecode et l'IL indépendants de l'architecture.)

kmarks2
la source