C#: Method to return object whose concrete type is determined at runtime?

前端 未结 4 2125
小蘑菇
小蘑菇 2021-02-10 07:10

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:

相关标签:
4条回答
  • 2021-02-10 07:37

    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.

    0 讨论(0)
  • 2021-02-10 07:47

    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);
      }
    }
    
    0 讨论(0)
  • 2021-02-10 07:53

    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.

    0 讨论(0)
  • 2021-02-10 08:02

    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.

    0 讨论(0)
提交回复
热议问题