¿Por qué debería evitarse el casting? [cerrado]

97

Por lo general, evito los tipos de casting tanto como sea posible, ya que tengo la impresión de que es una mala práctica de codificación y puede incurrir en una penalización de rendimiento.

Pero si alguien me pidiera que explicara exactamente por qué es así, probablemente los miraría como un ciervo en los faros.

Entonces, ¿por qué / cuándo está mal el casting?

¿Es general para java, c #, c ++ o cada entorno de ejecución diferente lo maneja en sus propios términos?

Los detalles para cualquier idioma son bienvenidos, por ejemplo, ¿por qué es malo en C ++?

FuerteNPosiblemente incorrecto
fuente
3
¿De dónde sacaste esta impresión?
Oded el
8
Tuve esta impresión porque nunca leí un libro o conocí a un programador que dijera "¡¡¡EMISIÓN SOOOO GOOOOD !!!"
LoudNPossiblyWrong
15
La respuesta para C ++ es inevitablemente diferente de la respuesta para C #. Esto es muy específico del idioma. Las respuestas hasta ahora responden a esta pregunta para idiomas específicos y, en algunos casos, no indican de qué idioma están hablando.
James McNellis
2
Malo es un término relativo. Evitar la transmisión es una práctica recomendada, pero a veces un programador tiene que hacer lo que tiene que hacer un programador. (especialmente si está escribiendo un programa java 1.5+ que usa una biblioteca escrita para 1.4) Quizás cambie el nombre de la pregunta, "¿Por qué debería evitarse la conversión?"
Mike Miller
2
Esta es una gran pregunta ... ¡La cerraron 5 usuarios que tienen cada uno menos de 10k de reputación! Un uso excesivo de nuevos poderes, claramente. Han votado para reabrir.
reach4thelasers

Respuestas:

141

Ha etiquetado esto con tres idiomas, y las respuestas son muy diferentes entre los tres. La discusión de C ++ implica más o menos también la discusión de las conversiones de C, y eso da (más o menos) una cuarta respuesta.

Dado que es el que no mencionaste explícitamente, comenzaré con C. Los yesos C tienen varios problemas. Una es que pueden hacer varias cosas diferentes. En algunos casos, el elenco no hace más que decirle al compilador (en esencia): "Cállate, sé lo que estoy haciendo", es decir, asegura que incluso cuando realizas una conversión que podría causar problemas, el compilador no le advertirá sobre esos problemas potenciales. Solo por ejemplo char a=(char)123456;,. El resultado exacto de esta implementación definida (depende del tamaño y firma dechar), y excepto en situaciones bastante extrañas, probablemente no sea útil. Las conversiones de C también varían en cuanto a si son algo que sucede solo en tiempo de compilación (es decir, solo le está diciendo al compilador cómo interpretar / tratar algunos datos) o algo que sucede en tiempo de ejecución (por ejemplo, una conversión real de doble a largo).

C ++ intenta lidiar con eso al menos en cierta medida agregando un número de operadores de conversión "nuevos", cada uno de los cuales está restringido a solo un subconjunto de las capacidades de una conversión de C. Esto hace que sea más difícil (por ejemplo) hacer accidentalmente una conversión que realmente no pretendía: si solo tiene la intención de deshacerse de la consistencia en un objeto, puede usar const_casty estar seguro de que lo único que puede afectar es si un objeto es const, volatileo no. Por el contrario, static_castno se permite que a afecte si un objeto es constovolatile. En resumen, tiene la mayoría de los mismos tipos de capacidades, pero están categorizados de modo que un elenco generalmente solo puede hacer un tipo de conversión, donde un solo elenco de estilo C puede hacer dos o tres conversiones en una operación. La principal excepción es que puede usar un dynamic_casten lugar de un static_casten al menos algunos casos y, a pesar de estar escrito como un dynamic_cast, realmente terminará como un static_cast. Por ejemplo, puede utilizar dynamic_castpara recorrer una jerarquía de clases hacia arriba o hacia abajo, pero un elenco "hacia arriba" en la jerarquía siempre es seguro, por lo que se puede hacer de forma estática, mientras que un elenco "hacia abajo" en la jerarquía no es necesariamente seguro, por lo que es hecho dinámicamente.

Java y C # son mucho más similares entre sí. En particular, con ambos, el casting es (¿virtualmente?) Siempre una operación en tiempo de ejecución. En términos de los operadores de dynamic_castconversión de C ++, generalmente está más cerca de lo que realmente se hace, es decir, cuando intenta lanzar un objeto a algún tipo de destino, el compilador inserta una verificación en tiempo de ejecución para ver si esa conversión está permitida y lanzar una excepción si no lo es. Los detalles exactos (p. Ej., El nombre utilizado para la excepción de "conversión incorrecta") varían, pero el principio básico sigue siendo en su mayor parte similar (aunque, si la memoria no funciona, Java hace que las conversiones se apliquen a los pocos tipos que no son objetos como intmucho más cerca de C casts, pero estos tipos se usan tan raramente que 1) no lo recuerdo con seguridad, y 2) incluso si es cierto, no importa mucho de todos modos).

Mirando las cosas de manera más general, la situación es bastante simple (al menos en mi opinión): un elenco (obviamente) significa que estás convirtiendo algo de un tipo a otro. Cuando / si haces eso, surge la pregunta "¿Por qué?" Si realmente quieres que algo sea de un tipo en particular, ¿por qué no lo definiste como ese para empezar? Eso no quiere decir que nunca haya una razón para hacer tal conversión, pero cada vez que suceda, debería plantear la pregunta de si podría rediseñar el código para que se use el tipo correcto en todo momento. Incluso las conversiones aparentemente inocuas (por ejemplo, entre números enteros y punto flotante) deben examinarse mucho más de cerca de lo que es común. A pesar de su aparentesimilitud, los números enteros realmente deberían usarse para tipos de cosas "contados" y el punto flotante para tipos de cosas "medidos". Ignorar la distinción es lo que lleva a algunas de las declaraciones locas como "la familia estadounidense promedio tiene 1.8 hijos". Aunque todos podemos ver cómo sucede eso, el hecho es que ninguna familia tiene 1.8 hijos. Pueden tener 1 o 2 o pueden tener más que eso, pero nunca 1.8.

Jerry Coffin
fuente
10
Parece que estás sufriendo 'muerte por capacidad de atención' aquí, esta es una buena respuesta en mi opinión.
Steve Townsend
2
Una respuesta contundente, pero algunas de las porciones de "No sé" podrían ajustarse para que sea más completa. @ Dragontamer5788 es una buena respuesta, pero no completa.
M2tM
5
Un niño completo y un niño con una pierna faltante serían 1.8 niños. Pero muy buena respuesta no obstante. :)
Oz.
1
Una respuesta muy agradable que no se ve obstaculizada en lo más mínimo por la exclusión de parejas sin hijos.
@Roger: sin excluir, solo ignorando.
Jerry Coffin
48

Muchas buenas respuestas aquí. Así es como lo veo (desde una perspectiva de C #).

El casting generalmente significa una de dos cosas:

  • Conozco el tipo de tiempo de ejecución de esta expresión, pero el compilador no lo sabe. Compilador, les digo, en tiempo de ejecución, el objeto que corresponde a esta expresión realmente será de este tipo. A partir de ahora, sabe que esta expresión debe tratarse como de este tipo. Generar código que asuma que el objeto será del tipo dado, o lanzar una excepción si me equivoco.

  • Tanto el compilador como el desarrollador conocen el tipo de tiempo de ejecución de la expresión. Hay otro valor de un tipo diferente asociado con el valor que tendrá esta expresión en tiempo de ejecución. Genere código que produzca el valor del tipo deseado a partir del valor del tipo dado; si no puede hacerlo, lance una excepción.

Note que esos son opuestos . ¡Hay dos tipos de yesos! Hay conversiones en las que le está dando una pista al compilador sobre la realidad , oye, esta cosa del tipo de objeto es en realidad de tipo Cliente, y hay conversiones en las que le está diciendo al compilador que realice una asignación de un tipo a otro , oye, Necesito el int que corresponde a este doble.

Ambos tipos de yesos son señales de alerta. El primer tipo de reparto plantea la pregunta "¿por qué exactamente el desarrollador sabe algo que el compilador no sabe?" Si se encuentra en esa situación, entonces el mejor que puede hacer es por lo general para cambiar el programa para que el compilador no tiene una manija en la realidad. Entonces no necesita el yeso; el análisis se realiza en tiempo de compilación.

El segundo tipo de conversión plantea la pregunta "¿por qué no se está realizando la operación en el tipo de datos de destino en primer lugar?" Si necesita un resultado en ints, ¿por qué tiene un doble en primer lugar? ¿No deberías tener un int?

Algunos pensamientos adicionales aquí:

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

Eric Lippert
fuente
2
No creo que esas sean inherentemente banderas rojas. Si tiene un Fooobjeto que hereda Bary lo almacena en a List<Bar>, entonces necesitará conversiones si lo desea Foo. Quizás indique un problema a nivel arquitectónico (¿por qué estamos almacenando Bars en lugar de Foos?), Pero no necesariamente. Y, si eso Footambién tiene un elenco válido int, también se ocupa de su otro comentario: está almacenando un Foo, no un int, porque un intno siempre es apropiado.
Mike Caron
6
@Mike Caron - No puedo responder por Eric, obviamente, pero para mí una bandera roja significa "esto es algo en lo que pensar", no "esto es algo mal". Y no hay problemas para almacenar un Fooen un List<Bar>, pero en el punto del reparto estás intentando hacer algo Fooque no es apropiado para un Bar. Eso significa que el comportamiento diferente para los subtipos se realiza a través de un mecanismo diferente al polimorfismo incorporado proporcionado por los métodos virtuales. Quizás esa sea la solución correcta, pero más a menudo es una señal de alerta.
Jeffrey L Whitledge
13
En efecto. Si estás sacando cosas de una lista de animales y luego necesitas decirle al compilador, oh, por cierto, sé que el primero es un tigre, el segundo es un león y el tercero es un oso, entonces deberías haber estado usando un Tuple <León, Tigre, Oso>, no una Lista <Animal>.
Eric Lippert
5
Estoy de acuerdo con usted, pero creo que sin un mejor soporte sintáctico para las Tuple<X,Y,...>tuplas, es poco probable que veamos un uso generalizado en C #. Este es un lugar donde el lenguaje podría hacer un mejor trabajo para empujar a la gente hacia el "pozo del éxito".
kvb
4
@kvb: Estoy de acuerdo. Consideramos adoptar una sintaxis de tupla para C # 4, pero no encajaba en el presupuesto. Quizás en C # 5; aún no hemos elaborado el conjunto completo de funciones. Demasiado ocupado juntando el CTP para async. O quizás en una hipotética versión futura.
Eric Lippert
36

Los errores de conversión siempre se informan como errores en tiempo de ejecución en java. El uso de genéricos o plantillas convierte estos errores en errores en tiempo de compilación, lo que hace que sea mucho más fácil detectar cuándo ha cometido un error.

Como dije anteriormente. Esto no quiere decir que todo el casting sea malo. Pero si es posible evitarlo, es mejor hacerlo.

Mike Miller
fuente
4
Esto supone que todo el casting es "malo"; sin embargo, esto no es del todo cierto. Tome C #, por ejemplo, con soporte de conversión implícito y explícito. El problema surge cuando se realiza un yeso que (accidentalmente) elimina información o seguridad de tipo (esto difiere según el idioma).
5
En realidad, esto está completamente mal en C ++. Quizás una edición para incluir el (los) idioma (s) objetivo con esta información esté en orden.
Steve Townsend
@Erick: lo haría, pero no sé nada sobre Java, ni suficientes detalles de C # para estar seguro de que la información es correcta.
Steve Townsend
Lo siento, soy un tipo de Java, por lo que mi conocimiento de C ++ es lo suficientemente limitado. Podría editar la palabra "plantillas", ya que se pretendía que fuera vaga.
Mike Miller
No tan. El compilador de Java detecta algunos errores de transmisión, y no solo los genéricos. Considere (Cadena) 0;
Marqués de Lorne
18

El casting no es intrínsecamente malo, es solo que a menudo se usa incorrectamente como un medio para lograr algo que realmente no debería hacerse en absoluto o hacerse de manera más elegante.

Si fuera universalmente malo, los idiomas no lo admitirían. Como cualquier otra característica del idioma, tiene su lugar.

Mi consejo sería que se concentre en su idioma principal y comprenda todos sus elementos y las mejores prácticas asociadas. Eso debería informar las excursiones a otros idiomas.

Los documentos de C # relevantes están aquí .

Hay un gran resumen sobre las opciones de C ++ en una pregunta SO anterior aquí .

Steve Townsend
fuente
9

Estoy hablando principalmente de C ++ aquí, pero la mayor parte de esto probablemente se aplique también a Java y C #:

C ++ es un lenguaje de tipado estático . Hay algunos márgenes que el lenguaje le permite en esto (funciones virtuales, conversiones implícitas), pero básicamente el compilador conoce el tipo de cada objeto en tiempo de compilación. La razón para utilizar un lenguaje de este tipo es que los errores pueden detectarse en tiempo de compilación . Si el compilador conoce los tipos de ay b, lo detectará en el momento de la compilación cuando sepa a=bdónde aes un número complejo y bes una cadena.

Siempre que hagas un casting explícito, le dices al compilador que se calle, porque crees que sabes más . En caso de que se equivoque, normalmente solo lo sabrá en tiempo de ejecución . Y el problema de averiguarlo en tiempo de ejecución es que esto podría ser en casa de un cliente.

sbi
fuente
5

Java, c # y c ++ son lenguajes fuertemente tipados, aunque los lenguajes fuertemente tipados pueden verse como inflexibles, tienen la ventaja de realizar una verificación de tipos en el momento de la compilación y lo protegen contra los errores de tiempo de ejecución causados ​​por tener el tipo incorrecto para ciertas operaciones.

Básicamente, hay dos tipos de elencos: un elenco para un tipo más general o un elenco para otros tipos (más específicos). La conversión a un tipo más general (conversión a un tipo principal) dejará intactas las comprobaciones de tiempo de compilación. Pero la conversión a otros tipos (tipos más específicos) deshabilitará la verificación de tipo en tiempo de compilación y será reemplazada por el compilador por una verificación en tiempo de ejecución. Esto significa que tiene menos certeza de que el código compilado se ejecutará correctamente. También tiene un impacto insignificante en el rendimiento, debido a la verificación adicional del tipo de tiempo de ejecución (¡la API de Java está llena de transmisiones!).

Kdeveloper
fuente
5
No todas las conversiones "omiten" la seguridad de tipo en C ++.
James McNellis
4
No todas las conversiones "omiten" la seguridad de tipo en C #.
5
No todas las conversiones "omiten" la seguridad de tipo en Java.
Erick Robertson
11
No todos los lanzamientos "omiten" la seguridad del tipo en Casting. Oh, espera, esa etiqueta no se refiere a un idioma ...
sbi
2
En casi todos los casos en C # y Java, la conversión le dará una degradación del rendimiento, ya que el sistema realizará una verificación de tipo en tiempo de ejecución (que no es gratuita). En C ++, dynamic_castes generalmente más lento que static_cast, ya que generalmente tiene que hacer una verificación de tipos en tiempo de ejecución (con algunas advertencias: la conversión a un tipo base es barata, etc.).
Travis Gockel
4

Algunos tipos de fundición son tan seguros y eficientes que a menudo ni siquiera se consideran fundiciones.

Si lanza de un tipo derivado a un tipo base, esto generalmente es bastante barato (a menudo, dependiendo del idioma, la implementación y otros factores, es de costo cero) y es seguro.

Si lanza de un tipo simple como un int a un tipo más amplio como un int largo, entonces, nuevamente, a menudo es bastante barato (generalmente no mucho más caro que asignar el mismo tipo que ese lanzamiento) y nuevamente es seguro.

Otros tipos son más cargados y / o más caros. En la mayoría de los lenguajes, la conversión de un tipo base a un tipo derivado es barata pero tiene un alto riesgo de error grave (en C ++, si hace static_cast de base a derivado, será barato, pero si el valor subyacente no es del tipo derivado, el comportamiento no está definido y puede ser muy extraño) o relativamente caro y con el riesgo de generar una excepción (dynamic_cast en C ++, conversión explícita de base a derivada en C #, etc.). El boxing en Java y C # es otro ejemplo de esto, y un gasto aún mayor (considerando que están cambiando más que cómo se tratan los valores subyacentes).

Otros tipos de conversión pueden perder información (un tipo de entero largo a un tipo de entero corto).

Estos casos de riesgo (ya sea de excepción o de error más grave) y de gasto son motivos para evitar el casting.

Una razón más conceptual, pero quizás más importante, es que cada caso de conversión es un caso en el que su capacidad para razonar sobre la corrección de su código se ve obstaculizada: cada caso es otro lugar donde algo puede salir mal y las formas en que puede salir mal y aumentar la complejidad de deducir si el sistema en su conjunto funcionará mal. Incluso si el yeso es seguro cada vez, probar esto es una parte adicional del razonamiento.

Finalmente, el uso intensivo de moldes puede indicar una falla en considerar bien el modelo de objeto, ya sea al crearlo, usarlo, o ambos: Transmitir hacia adelante y hacia atrás entre los mismos pocos tipos con frecuencia casi siempre es una falla al considerar las relaciones entre los tipos usado. Aquí no es tanto que los yesos sean malos, sino que son un signo de algo malo.

Jon Hanna
fuente
2

Existe una tendencia creciente de los programadores a aferrarse a las reglas dogmáticas sobre el uso de las funciones del lenguaje ("¡nunca use XXX!", "XXX se considera dañino", etc.), donde XXX va desde gotos hasta punteros a protectedmiembros de datos a singletons para pasar objetos por valor.

Seguir tales reglas, en mi experiencia, asegura dos cosas: no serás un programador terrible, ni serás un gran programador.

Un mejor enfoque consiste en excavar y descubrir el núcleo de verdad detrás de estas prohibiciones generales, y luego utilizar las funciones con criterio, con el entendimiento de que son muchas las situaciones para las cuales ellos son la mejor herramienta para el trabajo.

"Generalmente evito los tipos de casting tanto como sea posible" es un buen ejemplo de una regla tan generalizada. Los yesos son esenciales en muchas situaciones comunes. Algunos ejemplos:

  1. Al interoperar con código de terceros (especialmente cuando ese código está plagado de typedefs). (Ejemplo: GLfloat<--> double<--> Real.)
  2. Conversión desde un puntero / referencia de clase derivada a base: esto es tan común y natural que el compilador lo hará implícitamente. Si hacerlo explícito aumenta la legibilidad, el elenco es un paso hacia adelante, ¡no hacia atrás!
  3. Conversión desde una base a un puntero / referencia de clase derivada: también es común, incluso en código bien diseñado. (Ejemplo: contenedores heterogéneos).
  4. Serialización / deserialización binaria interna u otro código de bajo nivel que necesita acceso a los bytes sin procesar de los tipos integrados.
  5. En cualquier momento en que sea más natural, conveniente y legible usar un tipo diferente. (Ejemplo: std::size_type-> int.)

Ciertamente, hay muchas situaciones en las que no es apropiado usar un yeso, y es importante aprenderlas también; No entraré en demasiados detalles ya que las respuestas anteriores han hecho un buen trabajo señalando algunas de ellas.

user168715
fuente
1

Para desarrollar la respuesta de KDeveloper , no es intrínsecamente seguro para los tipos. Con la transmisión, no hay garantía de que lo que está transmitiendo y desde el que está transmitiendo coincida, y si eso ocurre, obtendrá una excepción de tiempo de ejecución, lo que siempre es algo malo.

Con respecto a C #, dado que incluye los operadores isy as, usted tiene la oportunidad de (en su mayor parte) tomar la determinación de si un lanzamiento tendrá éxito o no. Debido a esto, debe tomar las medidas adecuadas para determinar si la operación se realizará correctamente y procederá de manera adecuada.

casperOne
fuente
1

En el caso de C #, uno debe tener más cuidado al lanzar debido a los gastos generales de boxeo / unboxing involucrados al tratar con tipos de valor.

Manish Basantani
fuente
1

No estoy seguro de si alguien ya mencionó esto, pero en C # la conversión se puede usar de una manera bastante segura y, a menudo, es necesaria. Suponga que recibe un objeto que puede ser de varios tipos. Usando la ispalabra clave, primero puede confirmar que el objeto es del tipo al que está a punto de lanzarlo, y luego lanzar el objeto a ese tipo directamente. (No trabajé mucho con Java, pero estoy seguro de que también hay una forma muy sencilla de hacerlo).

user472875
fuente
1

Solo lanzas un objeto a algún tipo, si se cumplen 2 condiciones:

  1. sabes que es de ese tipo
  2. el compilador no

Esto significa que no toda la información que tiene está bien representada en la estructura de tipos que utiliza. Esto es malo, porque su implementación debe comprender semánticamente su modelo, lo que claramente no lo hace en este caso.

Ahora bien, cuando haces un yeso, esto puede tener 2 razones diferentes:

  1. Hiciste un mal trabajo al expresar las relaciones de tipo.
  2. el sistema de tipos de lenguajes simplemente no es lo suficientemente expresivo para expresarlos.

En la mayoría de los idiomas, muchas veces te encuentras con la segunda situación. Los genéricos como en Java ayudan un poco, el sistema de plantillas C ++ aún más, pero es difícil de dominar e incluso entonces algunas cosas pueden ser imposibles o simplemente no valen la pena.

Así que podría decirse que un elenco es un truco sucio para eludir sus problemas y expresar algún tipo de relación específico en un idioma específico. Deben evitarse los trucos sucios. Pero nunca puedes vivir sin ellos.

back2dos
fuente
0

En general, las plantillas (o genéricos) son más seguras para los tipos que las conversiones. En ese sentido, diría que un problema con el casting es la seguridad de tipos. Sin embargo, hay otro problema más sutil asociado especialmente con el abatimiento: el diseño. Desde mi perspectiva al menos, el abatimiento es un olor a código, una indicación de que algo podría estar mal con mi diseño y debería investigar más. Por qué es simple: si "obtiene" las abstracciones correctamente, ¡simplemente no las necesita! Buena pregunta por cierto ...

¡Salud!

Lefteris Laskaridis
fuente
0

Para ser realmente conciso, una buena razón es la portabilidad. Diferentes arquitecturas que se adaptan al mismo idioma pueden tener, por ejemplo, entradas de diferentes tamaños. Entonces, si migro de ArchA a ArchB, que tiene un int más estrecho, podría ver un comportamiento extraño en el mejor de los casos, y seg fallar en el peor.

(Claramente estoy ignorando el código de bytes independiente de la arquitectura y el IL).

kmarks2
fuente