C#: 在运行时确定具体类型并返回对象的方法?

5

我正在考虑设计一种方法,该方法将返回一个实现了接口的对象,但其具体类型直到运行时才能确定。例如,假设:

ICar
Ford implements ICar
Bmw implements ICar
Toyota implements ICar

public ICar GetCarByPerson(int personId)

直到运行时我们才知道我们会得到什么样的汽车。

a) 我想知道这个人拥有什么类型的汽车。

b) 根据我们得到的具体汽车类型,我们将调用不同的方法(因为某些方法只对该类有意义)。所以客户端代码将执行以下操作。

ICar car = GetCarByPerson(personId);

if ( car is Bmw )
{
  ((Bmw)car).BmwSpecificMethod();
}
else if (car is Toyota)
{
  ((Toyota)car).ToyotaSpecificMethod();
}

这是一个好的设计吗?有没有代码异味?是否有更好的方法来做到这一点?

我对返回接口的方法感到满意,如果客户端代码正在调用接口方法,那么这将是可以接受的。但我的担忧是,客户端代码是否将具体类型转换为好的设计。


每个 ICar 都有这个方法吗?它不能成为存在于 ICar 接口上的 DoSomething() 的原因是什么? - Nick Craver
2
这是一种代码异味。虽然你不是第一个使用这种方法的人,因为在某些情况下它是有意义的。如果你想要更好的答案,你可能需要提供更多细节......很多这样的决定都涉及到权衡,而一个简单的虚构汽车例子无法揭示这些权衡。 - Stephen
4个回答

11

在C#中使用is关键字(如上所示)几乎总是存在代码异味,而且很难闻。

问题在于,本应只知道一个ICar的东西现在需要跟踪多个实现ICar的类。虽然这样做可以工作(可以生成可操作的代码),但这是设计不良。您将从最初只有几辆汽车开始......

class Driver
{
    private ICar car = GetCarFromGarage();

    public void FloorIt()
    {
        if (this.car is Bmw)
        {
            ((Bmw)this.car).AccelerateReallyFast();
        }
        else if (this.car is Toyota)
        {
            ((Toyota)this.car).StickAccelerator();
        }
        else
        {
            this.car.Go();
        }
    }
}
并且接下来,另一辆车在你踩下油门时将会执行一些特殊操作。你将把这个功能添加到 Driver 中,还要考虑其他需要处理的特殊情况,并浪费二十分钟跟踪代码库中到处都是的 if(car is Foo),因为它现在分散在 DriverGarageParkingLot 中。(我在处理遗留代码时得出了这样的经验)。
当你发现自己像这样写 if (instance is SomeObject) 语句时,停下来问问自己为什么需要在这里处理这个特殊行为。大部分情况下,这可以成为接口/抽象类中的一个新方法,并且你可以为那些不是“特殊”的类提供默认实现。
这并不是说你绝对不应该使用 is 检查类型;然而,你必须非常小心地进行这个操作,因为它很容易失控并被滥用,除非加以限制。
现在,假设你已经确定必须对你的 ICar 进行类型检查。使用 is 的问题在于,在你进行两次转换时,静态代码分析工具会向你发出警告。
if (car is Bmw)
{
   ((Bmw)car).ShiftLanesWithoutATurnSignal();
}

除非这个代码块在内部循环中,否则性能影响可能是可以忽略不计的。但是首选的编写方式是:

var bmw = car as Bmw;
if (bmw != null) // careful about overloaded == here
{
    bmw.ParkInThreeSpotsAtOnce();
}

这只需要一个转换(内部)而不是两个。

如果你不想走那条路,另一个干净的方法是简单地使用枚举:

enum CarType
{
    Bmw,
    Toyota,
    Kia
}

interface ICar
{
    void Go();

    CarType Make
    {
        get;
    }
}

紧随其后

if (car.Make == CarType.Kia)
{
   ((Kia)car).TalkOnCellPhoneAndGoFifteenUnderSpeedLimit();
}

你可以快速地使用枚举进行开关,并且它让您知道(在某种程度上)可以使用哪些汽车。

使用枚举的一个缺点是,CarType是固定的; 如果另一个(外部)程序集依赖于ICar并添加了新的Tesla汽车,则不能将Tesla类型添加到CarType中。 枚举也不适用于类层次结构:如果您希望Chevy成为CarType.Chevy CarType.GM,则必须将枚举用作标志(在这种情况下难看),或者确保在检查枚举时先检查Chevy,再检查GM,或者在对枚举进行检查时使用大量的||操作符。


9

这是一个经典的双重分派问题,针对它有一种可接受的解决方案(访问者模式)。

//This is the car operations interface. It knows about all the different kinds of cars it supports
//and is statically typed to accept only certain ICar subclasses as parameters
public interface ICarVisitor {
   void StickAccelerator(Toyota car); //credit Mark Rushakoff
   void ChargeCreditCardEveryTimeCigaretteLighterIsUsed(Bmw car);
}

//Car interface, a car specific operation is invoked by calling PerformOperation  
public interface ICar {
   public string Make {get;set;}
   public void PerformOperation(ICarVisitor visitor);
}

public class Toyota : ICar {
   public string Make {get;set;}
   public void PerformOperation(ICarVisitor visitor) {
     visitor.StickAccelerator(this);
   }
}

public class Bmw : ICar{
   public string Make {get;set;}
   public void PerformOperation(ICarVisitor visitor) {
     visitor.ChargeCreditCardEveryTimeCigaretteLighterIsUsed(this);
   }
}

public static class Program {
  public static void Main() {
    ICar car = carDealer.GetCarByPlateNumber("4SHIZL");
    ICarVisitor visitor = new CarVisitor();
    car.PerformOperation(visitor);
  }
}

+1:展示了访问者模式的优美实现。我真希望我维护的代码也采用了这种模式。 - Mark Rushakoff
2
哈哈,我希望我使用的是我自己编写并需要维护的这种代码:D - Igor Zevaka
很好的例子!附上一个正确实例化ICarVisitor的示例会很有帮助。 - bloparod

0
你需要一个虚方法SpecificationMethod,每个类都要实现它。我建议阅读FAQ Lite关于继承的内容。他提到的设计方法也适用于 .Net。

1
我不认为你是错的,但我认为这有点自以为是。显然,OP有一个问题,即“某些方法只在[某些类]上有意义”。 - Stephen

0
更好的解决方案是让ICar声明一个GenericCarMethod(),然后让Bmw和Toyota进行重写。一般来说,如果可以避免,就不要依赖于向下转型,这不是一个好的设计实践。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接