DoSomething(Car car);
DoSomething(Bike bike);

public class Car : Vehicle {}
public class Bike : Vehicle {}
public abstract class Vehicle {}


void run(Vehicle vehicle) {
    DoSomething(vehicle);
}

Esto parece un problema simple pero estoy teniendo problemas. DoSomething (Vehículo del vehículo) no existe, por lo que DoSomething (vehículo) arroja un error, a pesar de que el vehículo está "garantizado" para ser un automóvil o una bicicleta. ¿Cómo puedo convencer al compilador de que "vehículo" es Bike o Car, para que DoSomething pueda ejecutarse?

Por supuesto, podría tener otro método en la línea de

DoSomething(Vehicle vehicle)
{
    if(vehicle is Car) ... etc
}

¿Pero seguramente hay un enfoque más limpio?

EDITAR / CLARIDAD

La motivación para poner este código en una clase de administrador, en lugar de tener DoSomething () en la clase Vehículo, es que cada Vehículo necesita acceder a diferentes partes del programa. Por ejemplo:

DoSomething(Car car) {
    motorwayInfo.CheckMotorwayStatus();
}
DoSomething(Bike bike) {
    cycleInfo.CheckCyclePathStatus();
}

No estoy seguro de si esta analogía realmente está entendiendo mi problema particular tan bien, ja, pero básicamente no quiero que los autos tengan ninguna referencia a cycleInfo, ni que las bicicletas tengan ninguna referencia a motorWayInfo. Pero poner DoSomething en Vehicle básicamente significa que su parámetro debe ser:

DoSomething(CycleInfo cycleInfo, MotorwayInfo motorwayInfo)

O

DoSomething(InfoManager infoManager)

Ninguno de los cuales parece totalmente ideal, ya que sé que cada subtipo solo va a usar un objeto de información específico. ¿Voy a hacer todo esto mal?

5
Cerzi 25 feb. 2018 a las 19:09

5 respuestas

La mejor respuesta

La verdadera pregunta aquí es: ¿qué espera que suceda al parámetro del método ? Si eso, sea lo que sea, depende de un tipo (sub) concreto del parámetro, entonces el lugar adecuado para ese comportamiento es en el miembro virtual (o incluso abstracto) de la clase base del parámetro - Vehicle.

class Vehicle
{
    public abstract void Behave();
}

class Car : Vehicle
{
    public override void Behave()
    {
        // Do something specific to a car
    }
}

class Bike : Vehicle
{
    public override void Behave()
    {
        // Do something specific to a bike
    }
}
...
void Run(Vehicle vehicle)
{
    vehicle.Behave();
}

Como puede ver en este código, he revertido los roles. La función Run es no responsable de saber cómo debe comportarse un parámetro concreto. En cambio, cada objeto concreto pasado como el parámetro Vehicle tendría que saber cómo comportarse. Ese es el polimorfismo apropiado.

Con respecto al método Run, todas sus responsabilidades con respecto al argumento deben estar relacionadas con el tipo común de todos los objetos de parámetros, y esa es la clase base Vehicle. A ese respecto, el método podría acceder a los miembros definidos en la clase base, o insertar el objeto en una colección, etc.

void Run(Vehicle vehicle)
{
    vehicle.Behave();

    List<Vehicle> list = ...
    list.Add(vehicle);
}
5
Zoran Horvat 25 feb. 2018 a las 16:18

FWIW, escribí lo que acuñé como "DoubleDispatchObject" para ayudar a resolver este tipo de problema, en particular,

1) sin perder la seguridad del tipo (o recurrir al uso de la palabra clave dinámica, ya sea en los sitios de llamadas o en las calles),

2) sin carga en la elección de las clases base (en cambio, el patrón de composición simple es suficiente),

Y 3) incurrir solo en un par de líneas de repeticiones.

Así es como se ve su escenario:

public class VehicleManager
{
  // boilerplate start
  private DoubleDispatchObject dispatch;

  public void DoSomething(Vehicle vehicle) =>
    this.EnsureThreadSafe(ref dispatch)
    .Via(nameof(DoSomething), vehicle, () => throw new NotImplementedException());
  // boilerplate end

  public void Run(Vehicle vehicle) =>
    DoSomething(vehicle);

  public void DoSomething(Car car) =>
    Console.WriteLine("Doing something with a car...");

  public void DoSomething(Bike bike) =>
    Console.WriteLine("Doing something with a bike...");
}

public abstract class Vehicle { }
public class Car : Vehicle { }
public class Bike : Vehicle { }

class MainClass {
  public static void Main (string[] args) {
    var manager = new VehicleManager();
    manager.Run(new Car());
    manager.Run(new Bike());
    Console.WriteLine("Done.");
  }
}

En repl.it:

https://repl.it/@ysharp_design/SO48975551

Consulte también esta solicitud de extracción relacionada (para las pruebas unitarias):

https://github.com/waf/MultipleDispatchBenchmarks/pull/1/files

'HTH

0
YSharp 28 abr. 2019 a las 06:33

Me sorprende que ninguna de estas respuestas incluyera genéricos o interfaces. En lugar de anular los métodos de subclase por instancia, puede revertir esto y crear una única instancia que acepte cualquiera.

public abstract class Vehicle {}
public class Car : Vehicle {}
public class Bike : Vehicle {}

public void DoSomething<T>(T _vehicle) where T : Vehicle
{
    // _vehicle can be an instance of Car or Bike

    if (_vehicle is Car _car)
        motorwayInfo.CheckMotorwayStatus();

    if (_vehicle is Bike _bike)
        cycleInfo.CheckCyclePathStatus();
}

Podría llevar esto un paso más allá e implementar una interfaz IVehicle.

0
Null511 26 feb. 2018 a las 01:20

el vehículo está "garantizado" para ser automóvil o bicicleta

Eso no es cierto. Si todos los automóviles son vehículos y todas las bicicletas son vehículos, eso no significa que todos los vehículos sean bicicletas o automóviles. Subtipar no funciona de esa manera.

¿Cómo puedo convencer al compilador de que "vehículo" es Bike o Car, para que DoSomething pueda ejecutarse?

La única forma de hacerlo es mediante un casting, que ya mencionaste:

if (vehicle is Car) (vehicle as Car).CheckMotorwayStatus();
else if (vehicle is Bike) (vehicle as Bike).CheckCyclePathStatus();

¿Pero seguramente hay un enfoque más limpio?

El enfoque que sugirió la respuesta de Zoran es el enfoque más limpio.

No quiero que Car tenga ninguna referencia a cycleInfo, ni que Bikes tenga ninguna referencia a motorWayInfo.

Con un poco de pensamiento, puede usarlo incluso con su configuración un poco más compleja. Por ejemplo:

public abstract class Vehicle
{
    public abstract void PrintRouteStatus();
}

public class MotorwayInfo
{
}

public class CycleInfo
{
}

public class Car : Vehicle
{
    // probably pass this in via a constructor.
    public MotorwayInfo _motorwayInfo = new MotorwayInfo();
    public override void PrintRouteStatus()
    {
        Console.WriteLine(_motorwayInfo);
    }
}

public class Bike : Vehicle
{
    // probably pass this in via a constructor.
    public CycleInfo _cycleInfo = new CycleInfo();
    public override void PrintRouteStatus()
    {
        Console.WriteLine(_cycleInfo);
    }
}

Funciona sin que Car esté al tanto de toda la información de Bike y viceversa.

public static void Main()
{
    var p = new Program();
    p.DoSomething(new Car());
    p.DoSomething(new Bike());
}

public void DoSomething(Vehicle v)
{
    v.PrintRouteStatus();
}
1
Shaun Luttin 25 feb. 2018 a las 17:17

Lo que está buscando se llama Double Dispatch y el compilador C # no lo admite de forma nativa.

Sin embargo, puede lograrlo con algunas refactorizaciones:

public void DoSomething(Car car) {}
public void DoSomething(Bike bike) {}

public abstract class Vehicle
{
    // ...
    public abstract void CallDoSomething();
}

public class Car : Vehicle
{
    public override void CallDoSomething()
    {
        DoSomething(this);
    }
}

public class Bike : Vehicle
{
    public override void CallDoSomething()
    {
        DoSomething(this);
    }
}

O, si no le importa la penalización de rendimiento, puede usar la palabra clave dynamic que diferirá la resolución de sobrecarga hasta el tiempo de ejecución y luego elija el método más apropiado que coincida con el tipo:

void run(dynamic vehicle)
{
    DoSomething(vehicle);
}

Consulte MSDN

2
haim770 25 feb. 2018 a las 16:21