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

发布于 2024-09-04 17:53:14 字数 607 浏览 6 评论 0原文

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

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

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

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

I'm thinking about designing a method that would return an object that implements an interface but whose concrete type won't be know until run-time. For example suppose:

ICar
Ford implements ICar
Bmw implements ICar
Toyota implements ICar

public ICar GetCarByPerson(int personId)

We don't know what car we will get back until runtime.

a) I want to know what type of car the person has.

b) depending on the concrete car type we get back we will call different methods (because some methods only make sense on the class). So the client code will do something like.

ICar car = GetCarByPerson(personId);

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

Is this a good design? Is there a code smell? Is there a better way to do this?

I'm fine with the method that returns the interface, and if the client code was calling interface methods obviously this would be fine. But my concern is whether the client code casting to concrete types is good design.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

盗心人 2024-09-11 17:53:14

在 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时,另一辆车会做一些特别的事情。您将将该功能添加到 Driver 中,并且您将考虑需要处理的其他特殊情况,并且您将浪费 20 分钟来跟踪存在 的每个位置>if (car is Foo),因为它现在分散在整个代码库中 - 在 Driver 内、Garage 内、在 ParkingLot 内code>... (我是根据处理遗留代码的经验来发言的。)

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

You can fast switch on an enum,它可以让您(在某种程度上)了解具体的情况可以使用的汽车的限制。

使用枚举的一个缺点是 CarType 是一成不变的;如果另一个(外部)程序集依赖于 ICar 并且他们添加了新的 Tesla 汽车,他们将无法添加 Tesla 类型汽车类型。枚举也不太适合类层次结构:如果您希望 Chevy 成为 CarType.Chevy CarType .GM,您要么必须使用枚举作为标志(在本例中很丑陋),要么确保在 GM 之前检查 Chevy,或者有很多|| 在您对枚举的检查中。

Using the is keyword in C# (in the manner you have demonstrated above) is almost always a code smell. And it stinks.

The problem is that something which is supposed to only know about an ICar is now required to keep track of several different classes that implement ICar. While this works (as in it produces code that operates), it's poor design. You're going to start off with just a couple cars...

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

And later on, another car is going to do something special when you FloorIt. And you'll add that feature to Driver, and you'll think about the other special cases that need to be handled, and you'll waste twenty minutes tracking down every place that there is a if (car is Foo), since it's scattered all over the code base now -- inside Driver, inside Garage, inside ParkingLot... (I'm speaking from experience in working on legacy code here.)

When you find yourself making a statement like if (instance is SomeObject), stop and ask yourself why this special behavior needs to be handled here. Most of the time, it can be a new method in the interface/abstract class, and you can simply provide a default implementation for the classes that aren't "special".

That's not to say that you should absolutely never check types with is; however, you must be very careful in this practice because it has a tendency to get out of hand and become abused unless kept in check.


Now, suppose you have determined that you conclusively must type-check your ICar. The problem with using is is that static code analysis tools will warn you about casting twice, when you do

if (car is Bmw)
{
   ((Bmw)car).ShiftLanesWithoutATurnSignal();
}

The performance hit is probably negligible unless it's in an inner loop, but the preferred way of writing this is

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

This requires only one cast (internally) instead of two.

If you don't want to go that route, another clean approach is to simply use an enumeration:

enum CarType
{
    Bmw,
    Toyota,
    Kia
}

interface ICar
{
    void Go();

    CarType Make
    {
        get;
    }
}

followed by

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

You can quickly switch on an enum, and it lets you know (to some extent) the concrete limit of what cars might be used.

One downside to using an enum is that CarType is set in stone; if another (external) assembly depends on ICar and they added the new Tesla car, they won't be able to add a Tesla type to CarType. Enums also don't lend themselves well to class hierarchies: if you want a Chevy to be a CarType.Chevy and a CarType.GM, you either have to use the enum as flags (ugly in this case) or make sure you check for Chevy before GM, or have lots of ||s in your checks against the enums.

ζ澈沫 2024-09-11 17:53:14

这是一个经典的双重调度问题,它有一个可接受的解决模式(访问者模式)。

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

This is a classic double dispatch problem and it has an acceptable pattern for solving it (Visitor pattern).

//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);
  }
}
倾听心声的旋律 2024-09-11 17:53:14

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

You would want just a virtual method, SpecificationMethod, which is implemented in each class. I recommend reading FAQ Lite's content on inheritence. The design method's he mentions can be applied to .Net as well.

笑红尘 2024-09-11 17:53:14

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

A better solution would have ICar declare a GenericCarMethod() and have Bmw and Toyota override it. In general, it's not a good design practice to rely on downcasting if you can avoid it.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文