Alain
Alain

Reputation: 27250

How to define a generic method that returns an instance of the deriving class type?

For instance, I have two classes performing the same function:

class HDD : Disk
{
    public HDD RMA()
    {
       HDD newHDD = RMAService.Exchange(this);
       return newHDD;
    }
}
class SSD : Disk
{
    public SSD RMA()
    {
       SSD newSSD = RMAService.Exchange(this);
       return newSSD;
    }
}

What I would like is to implement the RMA function in the base class, but maintain the strongly typed return value, so that the user can be certain by the function signature that they are getting back the type of disk they sent in!


What I have tried so far:

(see solutions below)

This type definition is ugly though. Anyone creating their own Disk class or a reference to a Disk would have a hard time knowing how to correctly use the type.

There's also no way to constrain the type argument to be exactly the class being defined. It just seems odd that there isn't a specialized way of declaring a property or method in a base class where the compile time type is whatever the derived type is.

Upvotes: 2

Views: 157

Answers (4)

David R Tribble
David R Tribble

Reputation: 12214

Not sure if this is what you want, but try separating the (hidden) implementation in the base class from the public interface methods in the derived subclasses.

abstract class Disk    //base class
{
    protected Disk CreateRMA()
    {
        Disk  newDisk;
        newDisk = new RMAService.Exchange(this);
        ...whatever else needs to be done...
        return newDisk;
    }
}

class HDD: Disk
{
    public HDD RMA()
    {
        return CreateRMA();
    }
}

class SSD: Disk
{
    public SSD RMA()
    {
        return CreateRMA();
    }
}

Each derived class's RMA() method simply calls the base class's CreateRMA() method and then casts the resulting Disk object to the derived class type. Each returned value has the proper subtype, and all of the actual work for each RMA() method is done in the base class method.

Now you can do these:

HDD  disk1 = someHDD.RMA();
SSD  disk2 = someSSD.RMA();

Addendum

This does not solve the OP's problem, since the call to RMAService.Exchange() should be specific to (overloaded for) each derived subtype. A different approach keeps that call in the derived types' method, but allows the base class to do all of the rest of whatever initialization work is needed:

// Second approach
abstract class Disk    //base class
{
    protected Disk CreateRMA(Disk newDisk)
    {
        ...other initialization code...
        return newDisk;
    }

    public abstract Disk RMA();    //not sure if this works
}

class HDD: Disk
{
    public override HDD RMA()
    {
        return CreateRMA(new RMAService.Exchange(this));
    }
}

class SSD: Disk
{
    public override SSD RMA()
    {
        return CreateRMA(new RMAService.Exchange(this));
    }
}

It's a little more work for the derived classes, but only for the overloaded new call; all of the rest of the initialization code is kept on the base class method.

Surrender

Okay, this won't work either, because C# does not support covariant return types.

Upvotes: 0

MarcinJuraszek
MarcinJuraszek

Reputation: 125660

You can make your baseline solution a little more easy-to-read and avoid type check within Disk constructor using protected constructor and dynamic on Exchange call:

abstract class Disk<T> where T : Disk<T> {
    protected Disk()
    {

    }

    public T RMA()
    {
        return RMAService.Exchange((dynamic)this);
    }
}

protected constructor makes classes like class HDD : Disk<SSD> fail at compile time and dynamic delays Exchange method overload matching decision till runtime, so you'll get the correct one (or error when non fits real this type).

Upvotes: 1

Alain
Alain

Reputation: 27250

I just came up with this solution using an extension method, which appears much better. Any reason why this wouldn't work how it appears to work at compile time?

public abstract class Disk
{
    public Disk() { }
}
public static class Disk_Extension_Methods
{  
    public static T RMA<T>(this T oldDisk) where T : Disk
    {
        T newDisk = RMAService.Exchange(oldDisk)
        return newDisk;
    }
}

Which allows you to go:

public class HDD : Disk { }
public class SSD : Disk { }

HDD newHDD = someHDD.RMA();

Upvotes: 1

Alain
Alain

Reputation: 27250

Posting this answer to set a baseline, but as I said, I'm not a fan of this odd "Have a class pass itself as a type parameter to its base class" construct.

abstract class Disk<T> where T : Disk<T> //Ugly recursive type parameter
{
    public Disk()
    {
        if( this.GetType() != typeof(T) ) //Ugly type check
            throw new Exception("The type argument must be precisely the " +
                                "derived class you're defining. Annoying eh?")
    }

    public T RMA()
    {
        Disk<T> newDisk = RMAService.Exchange(this)
        return (T)newDisk; //Ugly explicit cast
    }
}

Which allows you to go:

class HDD : Disk<HDD> { }  //Ugly self-referencing type parameter
class SSD : Disk<SSD> { }

HDD newHDD = someHDD.RMA();

Upvotes: 1

Related Questions