多态重载 - 如何强制抽象类型“充当”其派生类型之一以实现重载参数?

Posted

技术标签:

【中文标题】多态重载 - 如何强制抽象类型“充当”其派生类型之一以实现重载参数?【英文标题】:Polymorphic overloading - how to force an abstract type to "act" as one of its derived types for the purposes of overloaded parameters? 【发布时间】:2018-08-05 03:28:38 【问题描述】:
DoSomething(Car car);
DoSomething(Bike bike);

public class Car : Vehicle 
public class Bike : Vehicle 
public abstract class Vehicle 


void run(Vehicle vehicle) 
    DoSomething(vehicle);

这感觉像是一个简单的问题,但我遇到了问题。 DoSomething(Vehicle vehicle) 不存在,因此 DoSomething(vehicle) 会引发错误,即使车辆“保证”为 Car 或 Bike。如何让编译器相信“车辆”是 Bike 或 Car,以便 DoSomething 可以运行?

当然我可以有另一种方法

DoSomething(Vehicle vehicle)

    if(vehicle is Car) ... etc

但肯定有更清洁的方法吗?

编辑/澄清

将此代码放在管理器类中而不是在 Vehicle 类中存在 DoSomething() 的动机是每个 Vehicle 需要访问程序的不同部分。例如:

DoSomething(Car car) 
    motorwayInfo.CheckMotorwayStatus();

DoSomething(Bike bike) 
    cycleInfo.CheckCyclePathStatus();

不确定这个类比是否真的很好地解决了我的特定问题,哈哈 - 但基本上我不希望 Car 对 cycleInfo 有任何引用,Bikes 也不希望对 motorWayInfo 有任何引用。但是,将 DoSomething 放入 Vehicle 基本上意味着它的参数需要是:

DoSomething(CycleInfo cycleInfo, MotorwayInfo motorwayInfo)

DoSomething(InfoManager infoManager)

这两者似乎都不完全理想,因为我知道每个子类型只会使用特定的信息对象。我是不是搞错了?

【问题讨论】:

【参考方案1】:

这里真正的问题是 - 您希望 方法的参数会发生什么?如果这取决于参数的具体(子)类型,那么该行为的正确位置是参数基类的虚拟(甚至抽象)成员 - 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();

从这段代码中可以看出,我已经恢复了角色。 Run 函数负责了解具体参数的行为方式。相反,作为Vehicle 参数传递的每个具体对象都必须知道如何表现。这才是正确的多态性。

关于Run方法,它与参数有关的所有职责都应该与所有参数对象的公共类型相关,即基类Vehicle。在这方面,该方法可以访问定义在基类上的成员,或者将对象插入到集合中等等。

void Run(Vehicle vehicle)

    vehicle.Behave();

    List<Vehicle> list = ...
    list.Add(vehicle);

【讨论】:

是的,这实际上是代码之前的布局方式,也许这就是它应该的方式。为了让 Vehicle 尽可能接近严格的数据容器,我将它们进行了切换,因为感觉我将逻辑和数据混合在一起有点太多了。但你是对的,它有点破坏了多态性的目的,然后在单独的管理类中处理它们的功能 @Cerzi OOP 的重点是将通用数据和逻辑放在一起。 检查我的编辑 - 试图澄清我所拥有的稍微复杂的设置以及它如何让我觉得通常的多态性方法不是那么干净。 这是选择两害相权取其轻的情况,还是有一种我不知道的巧妙方法? @Cerzi 我添加了一个更新,以表明 Zoran 的答案是正确的,并且适用于您更复杂的情况。【参考方案2】:

您要查找的内容称为 Double Dispatch,C# 编译器本身不支持。

您可以通过一些重构来实现它:

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);
    

或者,如果您不关心性能损失,您可以使用 dynamic 关键字,它将重载决议推迟到运行时,然后选择与类型匹配的最合适的方法:

void run(dynamic vehicle)

    DoSomething(vehicle);

见MSDN

【讨论】:

很好的答案。要实现双重调度,您还可以使用访问者模式。【参考方案3】:

车辆“保证”是汽车或自行车

那不是真的。如果所有汽车都是车辆并且所有自行车都是车辆,这并不意味着所有车辆都是自行车或汽车。子类型化不能那样工作。

如何让编译器相信“车辆”是 Bike 或 Car,以便 DoSomething 可以运行?

做到这一点的唯一方法是强制转换,您已经提到过:

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

但肯定有更清洁的方法吗?

Zoran 的回答建议的方法是更清洁的方法。

我不希望 Car 对 cycleInfo 有任何引用,Bikes 也不希望对 motorWayInfo 有任何引用。

稍加考虑,即使设置稍微复杂一些,您也可以使用它。 For example:

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);
    

它可以在 Car 完全不知道 Bike 的所有信息的情况下工作,反之亦然。

public static void Main()

    var p = new Program();
    p.DoSomething(new Car());
    p.DoSomething(new Bike());


public void DoSomething(Vehicle v)

    v.PrintRouteStatus();

【讨论】:

这似乎更像是一个评论而不是一个答案。 在您进行编辑之前,它只是纠正了 OP 所做的假设。 我觉得这只是将问题转移到其他地方。就像你说的,对(在这个例子中) MotorwayInfo 的引用必须在某个时候传入,可能在构造函数中。但是,当像我的代码那样动态加载对象时,这仍然意味着需要对类型进行显式测试。例如“if(VehicleToAdd() == typeof(Car)) new car(motowayinfo); else new bike(cycleinfo);” @Cerzi 如果不了解您要解决的数据建模问题,很难给出更具体的建议。我的感觉是 Zoran 对你原来的问题给出了很好的回答。可能值得在 *** 上提出一个包含更多设计约束的新问题。一种可能的解决方案是让 MotorwayInfo 和 CycleInfo 都继承自抽象 RouteInfo 类。目前尚不清楚这是否适用于您的设计约束。【参考方案4】:

我很惊讶这些答案都没有包含泛型或接口。您可以将其反转并创建一个接受任一实例的单个实例,而不是覆盖每个实例的子类方法。

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();

您可以更进一步,实现IVehicle 接口。

【讨论】:

【参考方案5】:

FWIW,我写了我创造的“DoubleDispatchObject”来帮助解决这类问题 - 特别是,

1) 不失去类型安全性(或诉诸使用动态关键字,无论是在调用站点还是被调用者),

2) 没有选择基类的负担(相反,简单的组合模式就足够了),

和 3) 只产生几行样板代码。

您的场景如下所示:

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.");
  

在 repl.it 上:

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

另请参阅此相关的拉取请求(用于单元测试):

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

'HTH

【讨论】:

以上是关于多态重载 - 如何强制抽象类型“充当”其派生类型之一以实现重载参数?的主要内容,如果未能解决你的问题,请参考以下文章

C++笔记之多态

C++笔记之多态

C++笔记之多态

3.多态性

多态与重载

java 面向对象之多态