¿Cuál es el uso correcto de la reducción de fondos?

64

La conversión descendente significa la conversión de una clase base (o interfaz) a una clase de subclase o hoja.

Un ejemplo de un downcast podría ser si realiza la conversión de System.Object a otro tipo.

La conversión descendente es impopular, tal vez un olor a código: la doctrina orientada a objetos es preferir, por ejemplo, definir y llamar a métodos virtuales o abstractos en lugar de a la baja.

  • ¿Qué son, en su caso, casos de uso correctos y adecuados para la reducción de emisiones? Es decir, ¿en qué circunstancia (s) es apropiado escribir el código que se descartan?
  • Si su respuesta es "ninguna", ¿por qué el lenguaje admite la reducción de emisiones?
pregunta ChrisW 26.02.2018 - 14:11

9 respuestas

8

Estos son algunos usos correctos de la reducción de emisiones.

Y respetuosamente con vehemencia no estoy de acuerdo con otros que dicen que el uso de downcasts es definitivamente un olor a 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 proporcionarles mejores soluciones (incluso si se evitan debido a inconvenientes). No creo que existan mejores soluciones.

    
respondido por el Mehrdad 27.02.2018 - 12:25
128
  

La reducción de emisiones es impopular, tal vez un olor a código

No estoy de acuerdo. Downcasting es extremadamente popular ; una gran cantidad de programas del mundo real contienen uno o más descartes. Y no es quizás un olor de código. Es definitivamente un olor de código. Es por eso que se requiere que la operación de reducción de emisiones se manifieste en el texto del programa . Es para que pueda notar más fácilmente el olor y gastar atención de revisión de código en él.

  

¿en qué circunstancia [s] es apropiado escribir el código que realiza la reducción?

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 expresión en tiempo de compilación, y
  • necesita aprovechar ese hecho para usar una capacidad del objeto que no está disponible en el tipo de tiempo de compilación, y
  • es un mejor uso del tiempo y el esfuerzo para escribir el reparto que refactorizar el programa para eliminar cualquiera de los dos primeros puntos.

Si puede refactorizar un programa de manera económica para que el compilador pueda deducir el tipo de tiempo de ejecución, o refactorizar el programa para que no necesite la capacidad del tipo más derivado, entonces hágalo. Se agregaron downcasts al idioma para aquellas circunstancias en las que es hard y costoso para refactorizar el programa.

  

¿Por qué el lenguaje soporta 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 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 la reducción se verificará en el tiempo de ejecución y, si no se puede verificar, el programa hace lo correcto y se bloquea. Esto es maravilloso; significa que si su comprensión correcta del 100% de la semántica de tipo resulta ser 99.99% correcta, entonces su programa funciona el 99.99% del tiempo y falla el resto del tiempo, en lugar de comportarse de manera impredecible y corromper los datos del usuario el 0.01% del tiempo .

EJERCICIO: hay al menos una forma de producir un downcast en C # sin utilizando 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 crees que intervinieron en el diseño de una característica que podría producir un desplome hacia abajo sin tener un reparto manifiesto en el código?

    
respondido por el Eric Lippert 26.02.2018 - 16:10
32

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

Por ejemplo, puede tener dos TextBox s y desea usar un solo delegado para manejar un evento de cada uno de ellos. Entonces, deberías convertir sender a TextBox para 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, puedes crear tu propia subclase de TextBox que hizo esto internamente. Aunque no siempre es práctico o posible.

    
respondido por el mmathis 26.02.2018 - 15:25
17

Necesitas downdowning cuando algo te da un supertipo y necesitas manejarlo de manera diferente según el 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, Donation tendría un método abstracto getValue y cada subtipo de implementación tendría una implementación adecuada.

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

    
respondido por el Philipp 26.02.2018 - 14:50
12

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

Con frecuencia, puede evitar la reducción de la inversión haciendo una factorización de una API para usar los genéricos. Pero los genéricos no se agregaron al idioma hasta la versión 2.0 . Por lo tanto, incluso si su propio código no necesita admitir versiones en idiomas antiguos, es posible que esté usando clases heredadas que no están parametrizadas. Por ejemplo, algunas clases pueden definirse para llevar una carga útil del tipo Object , que se ve obligado a convertir al tipo que realmente se conoce como.

    
respondido por el TKK 26.02.2018 - 18:48
4

Existe un compromiso entre los idiomas tipificados estática y dinámicamente. La escritura estática le da al compilador mucha información para poder ofrecer garantías bastante sólidas sobre la seguridad de (partes de) los 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 reclamaciones es más fácil que probarlas.

Hay construcciones "inseguras" que te ayudan a hacer afirmaciones no probadas al compilador. Por ejemplo, llamadas incondicionales a Nullable.Value , downcasts incondicionales, dynamic objetos, etc. Le permiten hacer un reclamo ("Yo afirmo que el objeto a es un String , si me equivoco, lanzo un InvalidCastException ") sin Necesidad de probarlo. Esto puede ser útil en los casos en que demostrarlo es mucho más difícil de lo que vale la pena.

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

    
respondido por el Alexander 26.02.2018 - 18:32
3

La reducción a la baja se considera mala por un par de razones. Principalmente pienso porque es anti-OOP.

OOP realmente me gustaría si nunca tuvieras que abatir, 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
}

simplemente haces

x.DoThing()

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

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

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

Pero la alternativa a la reducción de emisiones en algunos escenarios puede ser bastante fea.

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

Puedes usar Double Dispatch para solucionar el problema ( enlace ) ... Pero eso también tiene sus problemas. O simplemente puede abatirse por 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 derribo al proporcionar un tipo donde los detalles se especifican más adelante.

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

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

    
respondido por el Ewan 26.02.2018 - 15:18
0

Además de todo lo dicho anteriormente, imagine una propiedad Tag que sea de tipo objeto. Le proporciona una forma de almacenar un objeto de su elección en otro objeto para su uso posterior según lo necesite. Necesitas bajar esta propiedad.

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

    
respondido por el puck 26.02.2018 - 17:28
0

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

Imagina que hay una interfaz en una biblioteca de terceros.

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

Y 2 clases que lo implementan. Bar y Qux . Bar se comporta bien.

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

Pero Qux no se comporta bien.

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

Bueno ... ahora no tengo otra opción. Tengo para escribir check y (posiblemente) downcast para evitar que mi programa se detenga.

    
respondido por el RubberDuck 26.02.2018 - 18:45

Lea otras preguntas en las etiquetas