C# Generics - How do I return a specific type?

2020-06-11 14:02发布

问题:

Maybe I'm going about this all wrong.

I have a bunch of classes that derive from the "Model" class, a base class with a bunch of common properties and methods. I want them all to implement a set of functionality:

public abstract void Create();
public abstract T Read<T>(Guid ID);  //<--Focus on this one
public abstract void Update();
public abstract void Delete();

Then I implement it in a child class like "Appointment" like so:

public override T Read<T>(Guid ID)
{
  var appt = db.Appointments.First(a => a.AppointmentID.Equals(ID));
  var appointment = new Appointment()
  {
    DateEnd = appt.dateEnd.GetValueOrDefault(),
    Location = appt.location,
    Summary = appt.summary
  };
return appointment;
}

This throws an exception "Can't implicitly convert type 'Appointment' to T". If I change the method's signature to "public override Appointment Read(Guid ID)", then the compiler says that I've not implemented the abstract method in the child class.

What am I missing? Can anyone give me some code samples?

回答1:

It looks like you could use a generic base class! Consider something like the following:

class Model<T>
{
    public abstract T Read(Guid ID);
}

class Appointment : Model<Appointment>
{
    public override Appointment Read(Guid ID) { }
}

Now your subclasses are all strongly typed. Of course, the tradeoff is that you no longer have a single base class. A Model<Appointment> isn't the same thing as a Model<Customer>. I have not generally found this to be a problem, though, because there's little common functionality-- the interfaces are similar, but they all work with different types.

If you'd like a common base, you can certainly cheat and implement an object-based interface that does the same general tasks. E.g., something in the spirit of (untested, but the idea's there):

interface IModelEntity
{
    object Read(Guid ID);
}

class Model<T> : IModelEntity
{
    public T Read(Guid ID)
    {
        return this.OnRead(ID); // Call the abstract read implementation
    }

    object IModelEntity.Read(Guid ID)
    {
        return this.OnRead(ID); // Call the abstract read implementation
    }

    protected abstract virtual T OnRead(Guid ID);
}

class Appointment : Model<Appointment>
{
    protected override Appointment OnRead(Guid ID) { /* Do Read Stuff */ }
}


回答2:

Would this work?

public abstract T Read<T>(Guid ID) where T : IAppointment;


回答3:

You need to box and cast. I wonder though why this method is generic?

return (T)(object)appointment;


回答4:

You must cast to object first then to T. The reason is rooted in that object is at the top of the inheritance chain. There is no direct correlation from Appointment to T; therefore you have to backtrack to object, then find your way back to T.

I provided this answer to give an explanation of why the return statement would not work unless it was doubly cast -- and in support of the answers given by Chaos & Greg



回答5:

First, I'd suggest you turn your base class into an interface. If that's an option for you, this will also reduce in slightly less-cluttered code, as you can get rid of the abstract and public keywords in the interface declaration and omit the override in the implementing classes.

Second, as your implementation of Appointment.Read suggests, you could change the method signature of Read to return a model object.

Both suggested changes would result in the following:

public interface IModel
{
    void Create();
    IModel Read(Guid ID);
    void Update();
    void Delete();
}

Third, it seems to me that Read should really be a factory method. In your current code, you need to first instantiate an Appointment object before you can call the Read method to retrieve another Appointment object. This seems wrong to me, from a class design perspective.

How about taking Read out of the base class/interface and providing it as a static method in all derived/implementing classes? For example:

public class Appointment : IModel
{
    public static Appointment Read(Guid ID)
    {
        return new Appointment()
               {
                   ...
               };
    }
}

You could also consider moving Read into a static (factory) class; however, it would then have to be smart enough to know what kind of object it should return. This would work e.g. if you had a table in your DB that would map a GUID to the corresponding object type.

Edit: The last suggestion above used to be this:

Third, if this is correct so far, the next question would be whether or not Read should be a static method instead. If so, it could be made static and be moved into a static Model class. The method would then act like a factory method that builds IModel objects from a DB:

Guid guid = ...;
IModel someModel = Model.Read(guid);


回答6:

There is something funky about this design.

Regardless of whether or not the Model class is templated, putting a template parameter on the Read method doesn't make a lot of sense as an instance method.

Usually you'd have something like what Greg D posted.



回答7:

If public abstract T Read<T>(Guid ID); of Model will only ever return derived types of Model, consider changing the signature to

public abstract class Model
{
    public abstract void Create();
    public abstract Model Read(Guid ID);  //<--here
    public abstract void Update();
    public abstract void Delete();
}


回答8:

in your Appointment class add this

public class Appointment<T> : Model where T : Appointment


回答9:

you can also call: var x = myModel.Read<Appointment>();