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

我正在考虑设计一个方法,该方法将返回一个实现接口的对象,但其具体类型在运行时才会知道。 比如假设:

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

这是一个很好的设计吗? 有代码味吗? 有一个更好的方法吗?

我很好用返回接口的方法,如果客户端代码显然调用接口方法,那就没问题了。 但我担心的是客户端代码转换为具体类型是否是好的设计。

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

后来,当你在FloorIt时, 另一辆车会做一些特别的FloorIt 。 你将把这个function添加到Driver ,你会考虑需要处理的其他特殊情况,并且你会浪费20分钟跟踪每个有if (car is Foo) ,因为它是现在分散在整个代码库中 – 在Driver内部,在Garage里面,在ParkingLot里面… (我在这里谈论遗留代码的经验。)

当你发现自己发出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(); } 

您可以快速switch枚举,它可以让您(在某种程度上)了解可能使用的汽车的具体限制。

使用枚举的一个缺点是CarTypeCarType ; 如果另一个(外部)组件依赖于ICar并且他们添加了新的Tesla汽车,他们将无法将Tesla类型添加到CarType 。 枚举也不适合类层次结构:如果你想让Chevy成为CarType.Chevy CarType.GM ,你必须使用枚举作为标志(在这种情况下是丑陋的)或确保你检查在GM之前Chevy ,或者有很多|| 在你的支票核对。

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

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

您只需要一个虚拟方法, SpecificationMethod ,它在每个类中实现。 我建议阅读FAQ Lite关于遗产的内容。 他提到的设计方法也可以应用于.Net。

一个更好的解决方案是让ICar声明一个GenericCarMethod()并让Bmw和Toyota覆盖它。 一般来说,如果你可以避免它,那么依靠向下转换并不是一个好的设计实践。