¿Cuál es el uso adecuado de la abatición?


68

Downcasting significa transmitir desde una clase base (o interfaz) a una subclase o clase hoja.

Un ejemplo de abatido podría ser si realiza un envío desde System.Objectotro tipo.

Downcasting es impopular, tal vez un olor a código: la doctrina orientada a objetos es preferible, por ejemplo, definir y llamar a métodos virtuales o abstractos en lugar de downcasting.

  • ¿Cuáles son, si los hay, casos de uso adecuados y adecuados para la conversión? Es decir, ¿en qué circunstancia (s) es apropiado escribir código que no sea válido?
  • Si su respuesta es "ninguna", entonces ¿por qué el idioma admite el downcasting?

55
Lo que usted describe generalmente se llama "downcasting". Armado con ese conocimiento, ¿podría investigar un poco más primero y explicar por qué las razones existentes que encontró no son suficientes? TL; DR: el downcasting rompe la escritura estática, pero a veces un sistema de tipos es demasiado restrictivo. Por lo tanto, es sensato que un idioma ofrezca un downcast seguro.
amon

¿Te refieres a abatir? La conversión ascendente sería a través de la jerarquía de clases, es decir, de subclase a superclase, en oposición a la degradación, de superclase a subclase ...
Andrei Socaciu

Mis disculpas: tienes razón. Edité la pregunta para cambiar el nombre de "upcast" a "downcast".
ChrisW

@amon en.wikipedia.org/wiki/Downcasting ofrece "convertir a cadena" como ejemplo (que .Net admite el uso de un virtual ToString); su otro ejemplo es "contenedores Java" porque Java no admite genéricos (que C # sí). stackoverflow.com/questions/1524197/downcast-and-upcast dice qué es downcasting , pero sin ejemplos de cuándo es apropiado .
ChrisW

44
@ChrisW Eso es before Java had support for generics, no because Java doesn't support generics. Y amon prácticamente te dio la respuesta: cuando el sistema de tipos con el que estás trabajando es demasiado restrictivo y no estás en condiciones de cambiarlo.
Ordous

Respuestas:


12

Estos son algunos de los usos adecuados del downcasting.

Y con todo respeto vehementemente en desacuerdo con otros aquí que dicen que el uso de downcasts es sin duda un olor código, porque creo que no hay otra forma razonable de resolver los problemas correspondientes.

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 va a decir que estos son olores de código, debe proporcionar mejores soluciones para ellos (incluso si se evitan por inconvenientes). No creo que existan mejores soluciones.


9
"Código de olor" no significa "este diseño es definitivamente malo ", significa "este diseño es sospechoso ". Que hay características del lenguaje que son inherentemente sospechosas es una cuestión de pragmatismo por parte del autor del lenguaje
Caleth

55
@Caleth: Y mi punto es que estos diseños surgen todo el tiempo, y que no hay absolutamente nada intrínsecamente sospechoso sobre los diseños que he mostrado, y por lo tanto, el casting no es un olor a código.
Mehrdad

44
@Caleth no estoy de acuerdo. El olor del código, para mí, significa que el código es malo o, al menos, podría mejorarse.
Konrad Rudolph

66
@Mehrdad Su opinión parece ser que los olores de código tienen que ser culpa de los programadores de aplicaciones. ¿Por qué? No todos los olores de código tienen que ser un problema real o tener una solución. Un olor a código según Martin Fowler es simplemente "una indicación de superficie que generalmente corresponde a un problema más profundo en el sistema" se object.Equalsajusta bastante bien a esa descripción (intente proporcionar correctamente un contrato igual en una superclase que permita que las subclases lo implementen correctamente; es absolutamente no trivial). Que como programadores de aplicaciones no podemos resolverlo (en algunos casos podemos evitarlo) es un tema diferente.
Voo

55
@Mehrdad Bueno, veamos qué dice uno de los diseñadores de idiomas originales sobre Equals ... Object.Equals is horrid. Equality computation in C# is completely messed up(comentario de Eric sobre su respuesta). Es curioso cómo no quieres reconsiderar tu opinión, incluso cuando uno de los diseñadores principales del lenguaje en sí te dice que estás equivocado.
Voo

136

Downcasting es impopular, tal vez un olor a código

Estoy en desacuerdo. Downcasting es extremadamente popular ; Una gran cantidad de programas del mundo real contienen uno o más programas descartados. Y tal vez no sea un olor a código. Definitivamente es un olor a código. Es por eso que se requiere que la operación de downcasting se manifieste en el texto del programa . Es para que pueda notar más fácilmente el olor y dedicar atención a revisar el código.

¿En qué circunstancias es apropiado escribir código que no sea válido?

En cualquier circunstancia donde:

  • tiene un conocimiento 100% correcto de un hecho sobre el tipo de tiempo de ejecución de una expresión que es más específico que el tipo de tiempo de compilación de la expresión, y
  • necesita aprovechar ese hecho para utilizar una capacidad del objeto no disponible en el tipo de tiempo de compilación, y
  • Es mejor utilizar el tiempo y el esfuerzo para escribir el elenco que refactorizar el programa para eliminar cualquiera de los dos primeros puntos.

Si puede refactorizar un programa a bajo costo para que el compilador deduzca el tipo de tiempo de ejecución, o para refactorizar el programa de modo que no necesite la capacidad del tipo más derivado, hágalo. Se agregaron downcasts al lenguaje para aquellas circunstancias en las que es difícil y costoso refactorizar el programa.

¿Por qué el lenguaje admite el downcasting?

C # fue inventado por programadores pragmáticos que tienen trabajos que hacer, para programadores pragmáticos que tienen trabajos que hacer. Los diseñadores de C # no son puristas de OO. Y el sistema de tipo C # no es perfecto; por diseño, subestima las restricciones que se pueden colocar en el tipo de tiempo de ejecución de una variable.

Además, el downcasting es muy seguro en C #. Tenemos una fuerte garantía de que el downcast se verificará en tiempo de ejecución y, si no se puede verificar, el programa hace lo correcto y falla. Esto es maravilloso; significa que si su comprensión 100% correcta de la semántica de tipos resulta ser 99.99% correcta, entonces su programa funciona el 99.99% del tiempo y se bloquea el resto del tiempo, en lugar de comportarse de manera impredecible y corromper los datos del usuario 0.01% del tiempo .


EJERCICIO: Hay al menos una forma de producir un downcast en C # sin usar un operador de conversión explícito. ¿Puedes pensar en tales escenarios? Dado que estos también son olores potenciales de código, ¿qué factores de diseño cree que entraron en el diseño de una característica que podría producir un bloqueo abatido sin tener un reparto manifiesto en el código?


101
Recuerde esos carteles en la escuela secundaria en las paredes "Lo que es popular no siempre es correcto, lo que es correcto no siempre es popular". Pensabas que intentaban evitar que consumieras drogas o quedaras embarazada en la escuela secundaria, pero en realidad eran advertencias sobre los peligros de la deuda técnica.
corsiKa

14
@EricLippert, la declaración foreach, por supuesto. Esto se hizo antes de que IEnumerable fuera genérico, ¿verdad?
Arturo Torres Sánchez

18
Esta explicación me alegró el día. Como docente, a menudo me preguntan por qué C # está diseñado de una manera u otra, y mis respuestas a menudo se basan en motivaciones que usted y sus colegas han compartido aquí y en sus blogs. A menudo es un poco más difícil responder la pregunta de seguimiento "¿Pero por qué ? Y aquí encuentro la respuesta de seguimiento perfecta: "C # fue inventado por programadores pragmáticos que tienen trabajos que hacer, para programadores pragmáticos que tienen trabajos que hacer". :-) ¡Gracias @EricLippert!
Zano

12
Aparte: Obviamente, en mi comentario anterior, quise decir que tenemos un downcast de A a B. Realmente no me gusta el uso de "arriba" y "abajo" y "padre" e "hijo" cuando navego por una jerarquía de clases y los entiendo mal todo el tiempo en mi escritura. La relación entre los tipos es una relación de superconjunto / subconjunto, así que no sé por qué debería haber una dirección hacia arriba o hacia abajo asociada. Y ni siquiera me hagas empezar con "padre". Los padres de la jirafa no son mamíferos, los padres de la jirafa son el señor y la señora jirafa.
Eric Lippert

9
Object.Equalses horrible . El cálculo de igualdad en C # está completamente desordenado , demasiado desordenado para explicarlo en un comentario. Hay muchas formas de representar la igualdad; es muy fácil hacerlos inconsistentes; es muy fácil, de hecho se requiere violar las propiedades requeridas de un operador de igualdad (simetría, reflexividad y transitividad). Si estuviera diseñando un sistema de tipos desde cero hoy, no habría ningún Equals(o GetHashcodeo ToString) encendido Object.
Eric Lippert

33

Los controladores de eventos generalmente tienen la firma MethodName(object sender, EventArgs e). En algunos casos, es posible manejar el evento sin tener en cuenta qué tipo senderes, o incluso sin usarlo sender, pero en otros se senderdebe convertir a un tipo más específico para manejar el evento.

Por ejemplo, puede tener dos TextBoxsy desea utilizar un solo delegado para controlar un evento de cada uno de ellos. Entonces necesitaría convertir sendera a TextBoxpara poder acceder a las propiedades necesarias para manejar el evento:

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

Sí, en este ejemplo, podría crear su propia subclase de la TextBoxcual hizo esto internamente. Sin embargo, no siempre es práctico o posible.


2
Gracias, ese es un buen ejemplo. El TextChangedevento es miembro de Control. Me pregunto por qué senderno es de tipo en Controllugar de tipo object. También me pregunto si (en teoría) el marco podría haber redefinido el evento para cada subclase (de modo que, por ejemplo, TextBoxpodría tener una nueva versión del evento, utilizando TextBoxcomo tipo de remitente) ... que podría (o no) requerir un downcast ( to TextBox) dentro de la TextBoximplementación (para implementar el nuevo tipo de evento), pero evitaría requerir un downcast en el controlador de eventos del código de la aplicación.
ChrisW

3
Esto se considera aceptable porque es difícil generalizar el manejo de eventos si cada evento tiene su propia firma de método. Aunque supongo que es una limitación aceptada en lugar de algo que se considera la mejor práctica porque la alternativa es en realidad más problemática. Si fuera posible comenzar con un código en TextBox senderlugar de uno object sendersin complicar demasiado el código, estoy seguro de que se haría.
Neil

66
@Neil Los sistemas de eventos están diseñados específicamente para permitir el envío de fragmentos de datos arbitrarios a objetos arbitrarios. Eso es casi imposible de hacer sin verificaciones de tiempo de ejecución para la corrección de tipo.
Joker_vD

@Joker_vD Idealmente, la API, por supuesto, admitiría firmas de devolución de llamada fuertemente tipadas utilizando contravarianza genérica. El hecho de que .NET (abrumadoramente) no haga esto se debe simplemente al hecho de que los genéricos y la covarianza se introdujeron más tarde. - En otras palabras, se requiere un downcast aquí (y, en general, en cualquier otro lugar) debido a deficiencias en el lenguaje / API, no porque sea fundamentalmente una buena idea.
Konrad Rudolph

@KonradRudolph Claro, pero ¿cómo almacenaría eventos de tipos arbitrarios en la cola de eventos? ¿Acabas de deshacerte de las colas y te despachas directamente?
Joker_vD

17

Necesitas un downcasting cuando algo te da un supertipo y necesitas manejarlo de manera diferente dependiendo del subtipo.

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, esto no huele bien. En un mundo perfecto Donationtendría un método abstracto getValuey la implementación de cada subtipo tendría una implementación adecuada.

Pero, ¿qué pasa si estas clases son de una biblioteca que no puede o no quiere cambiar? Entonces no hay otra opción.


Este parece ser un buen momento para usar el patrón de visitante. O la dynamicpalabra clave que logra el mismo resultado.
user2023861

3
@ user2023861 No, creo que el patrón de visitante depende de la cooperación de las subclases de Donación, es decir, Donación necesita declarar un resumen void accept(IDonationVisitor visitor), que sus subclases implementan llamando a métodos específicos (por ejemplo, sobrecargados) IDonationVisitor.
ChrisW

@ ChrisW, tienes razón en eso. Sin cooperación, aún podría hacerlo utilizando la dynamicpalabra clave.
user2023861

En este caso particular, puede agregar un método de valor estimado a Donación.
user253751

@ user2023861: dynamicsigue bajando , solo que más lento.
Mehrdad

12

Para agregar a la respuesta de Eric Lippert ya que no puedo comentar ...

A menudo puede evitar el downcasting refactorizando una API para usar genéricos. Pero los genéricos no se agregaron al lenguaje hasta la versión 2.0 . Por lo tanto, incluso si su propio código no necesita admitir versiones en idiomas antiguos, puede encontrarse utilizando clases heredadas que no están parametrizadas. Por ejemplo, alguna clase puede definirse para llevar una carga útil de tipo Object, que se ve obligado a lanzar al tipo que realmente sabe que es.


De hecho, se podría decir que los genéricos en Java o C # son esencialmente un envoltorio seguro para almacenar objetos de superclase y descender a la subclase correcta (comprobada por el compilador) antes de volver a usar los elementos. (A diferencia de las plantillas de C ++, que reescribir el código para utilizar siempre la subclase sí mismo y así evitar tener que abatido - que puede aumentar el rendimiento de la memoria, aunque a menudo a expensas de tiempo de compilación y el tamaño del ejecutable.)
leftaroundabout

4

Existe una compensación entre los idiomas estáticos y dinámicamente escritos. La escritura estática le da al compilador mucha información para poder hacer garantías bastante sólidas sobre la seguridad de (partes de) programas. Sin embargo, esto tiene un costo, porque no solo necesita saber que su programa es correcto, sino que debe escribir el código apropiado para convencer al compilador de que este es el caso. En otras palabras, hacer reclamos es más fácil que probarlos.

Hay construcciones "inseguras" que le ayudan a hacer afirmaciones no comprobadas al compilador. Por ejemplo, llamadas incondicionales Nullable.Value, rechazos incondicionales, dynamicobjetos, etc. Le permiten hacer valer un reclamo ("Afirmo que el objeto aes un String, si me equivoco, arrojar un InvalidCastException") sin necesidad de probarlo. Esto puede ser útil en casos en los que demostrar que es significativamente más difícil de lo que vale la pena.

Abusar de esto es arriesgado, razón por la cual existe la notación explícita de downcast y es obligatoria; Es una sal sintáctica destinada a llamar la atención sobre una operación insegura. El lenguaje podría haberse implementado con un downcast implícito (donde el tipo inferido no es ambiguo), pero eso ocultaría esta operación insegura, lo que no es deseable.


Supongo que estoy preguntando, entonces, por algún ejemplo [s] de dónde / cuándo es necesario o deseable (es decir, una buena práctica) hacer este tipo de "afirmación no probada".
ChrisW

1
¿Qué hay de emitir el resultado de una operación de reflexión? stackoverflow.com/a/3255716/3141234
Alexander

3

Downcasting se considera malo por un par de razones. Principalmente pienso porque es anti-OOP.

OOP realmente me gustaría si nunca tuvieras que abatirlo ya que su "razón de ser" es que el polimorfismo significa que no tienes que ir

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

solo lo haces

x.DoThing()

y el código automáticamente hace lo correcto.

Hay algunas razones "difíciles" para no rechazar:

  • En C ++ es lento.
  • Obtiene un error de tiempo de ejecución si elige el tipo incorrecto.

Pero la alternativa al downcasting en algunos escenarios puede ser bastante fea.

El ejemplo clásico es el procesamiento de mensajes, donde no desea agregar la función de procesamiento al objeto, sino mantenerla en el procesador de mensajes. Luego tengo una tonelada MessageBaseClassen una matriz para procesar, pero también necesito cada una por subtipo para el procesamiento correcto.

Puede usar Double Dispatch para solucionar el problema ( https://en.wikipedia.org/wiki/Double_dispatch ) ... Pero eso también tiene sus problemas. O simplemente puede rechazar un código simple a riesgo de esas razones difíciles.

Pero esto fue antes de que se inventaran los genéricos. Ahora puede evitar el downcasting al proporcionar un tipo donde los detalles se especifiquen más adelante.

MessageProccesor<T> puede variar sus parámetros de método y valores de retorno según el tipo especificado, pero aún así proporcionar funcionalidad genérica

Por supuesto, nadie te está obligando a escribir código OOP y hay muchas características de lenguaje que se proporcionan pero que están mal vistas, como la reflexión.


1
Dudo que sea lento en C ++, especialmente si en static_castlugar de hacerlo dynamic_cast.
ChrisW

"Procesamiento de mensajes": lo considero un significado, por ejemplo, transmitir lParamalgo específico. Sin embargo, no estoy seguro de que sea un buen ejemplo de C #.
ChrisW

3
@ChrisW: bueno, sí, static_castsiempre es rápido ... pero no se garantiza que no falle de forma indefinida si comete un error y el tipo no es lo que espera.
Jules

@Jules: Eso es completamente irrelevante.
Mehrdad

@Mehrdad, es exactamente el punto. hacer el equivalente c # cast en c ++ es lento. y esta es la razón tradicional contra el casting. el static_cast alternativo no es equivalente y se considera la peor opción debido al comportamiento indefinido
Ewan

0

Además de todo lo anterior, imagine una propiedad Tag que sea de tipo objeto. Proporciona una manera de almacenar un objeto de su elección en otro objeto para su uso posterior según lo necesite. Necesita abatir esta propiedad.

En general, no usar algo hasta hoy no siempre es una indicación de algo inútil ;-)


Sí, por ejemplo, ¿ para qué sirve la propiedad Tag en .net ? En una de las respuestas, alguien escribió : Solía ​​usarlo para ingresar instrucciones para el usuario en las aplicaciones de Windows Forms. Cuando se activó el evento GotFocus de control, a la propiedad Label.Text de instrucciones se le asignó el valor de mi propiedad Tag de control que contenía la cadena de instrucciones. Supongo que una forma alternativa de 'extender' la Control(en lugar de usar la object Tagpropiedad) sería crear una Dictionary<Control, string>en la que almacenar la etiqueta para cada control.
ChrisW

1
Me gusta esta respuesta porque Tages una propiedad pública de una clase de marco .Net, que muestra que se supone que (más o menos) debe usarla.
ChrisW

@ChrisW: No quiere decir que la conclusión sea incorrecta (o correcta), pero tenga en cuenta que es un razonamiento descuidado, porque hay muchas funciones públicas de .NET que están obsoletas (digamos System.Collections.ArrayList) o desaprobadas (digamos IEnumerator.Reset).
Mehrdad

0

El uso más común de downcasting en mi trabajo se debe a que un idiota rompe el principio de sustitución de Liskov.

Imagine que hay una interfaz en alguna biblioteca de terceros.

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

Y 2 clases que lo implementan. Bary Qux. BarSe porta bien.

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

Pero Quxno se porta bien.

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

Bueno ... ahora no tengo otra opción. Me tengo que escribir cheque y (posiblemente) abatido con el fin de evitar tener mi programa vienen a un alto que se estrella.


1
No es cierto para este ejemplo en particular. Podrías atrapar la excepción NotImplementedException al llamar a SayGoodbye.
Per von Zweigbergk

Quizás sea ​​un ejemplo demasiado simplificado, pero trabaje un poco con algunas de las API .Net más antiguas y se encontrará con esta situación. A veces, la forma más fácil de escribir el código es emitir ala de forma segura var qux = foo as Qux;.
RubberDuck

0

Otro ejemplo del mundo real es el de WPF VisualTreeHelper. Utiliza abatido a emitir DependencyObjecta Visualy Visual3D. Por ejemplo, VisualTreeHelper.GetParent . El downcast ocurre en 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;
}
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.