2

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.

Alain
  • 26,663
  • 20
  • 114
  • 184
  • 1
    If only I could use the excuse "I'm having a hard time knowing how to use the type" in my day job :( – Simon Whitehead Jan 07 '14 at 23:50
  • This can be improved provided you'll present what does `Exchange` function do. Either it should be split - for the type ignorant part be left in a base class, and the strictly typed part be subclassed, or perhaps something similar to `Clone` can be introduced. Hard to say without knowing what's inside `Exchange`. – BartoszKP Jan 07 '14 at 23:52
  • @BartoszKP `Exchange` is any function that returns a new object of the same runtime type as what was passed in. If you would like to mock something up, simply write a function that returns exactly the object passed in. `Public T Exchange(T in) { return in; }` – Alain Jan 07 '14 at 23:56
  • @Alain I mean that on this level of generality your solution seems the only one. IMHO the only improvement can be made along with refactoring of `Exchange`. For example: regarding to your mock, an `abstract` `Exchange` in the base class, overriden in subclasses would do the job. – BartoszKP Jan 07 '14 at 23:59
  • @BartoszKP - I'm working to avoid code duplication. `Exchange` could be thousands of lines of code, and would always have the exact same implementation, except for the reference types. Same goes for `RMA` - in this example it's short, but in reality it's much longer. The idea is to have the functions return strong types, but all share the same implementation. Your previous comment prompting me to give an example `Exchange` method gave me a new idea though! Check it out below. – Alain Jan 08 '14 at 00:10
  • @Alain 1) If `Exchange` is thousands lines of code then definitely it deserves refactoring. 2) You say it yourself - part of implementation is identical, and part is not, because of strong typing. That's exactly what I referred to in my first comment above. It can be split, for the shared part to be in the base class, and the rest in "specialization" (subclasses, generics, or whatever). – BartoszKP Jan 08 '14 at 00:13
  • 'Anyone creating their own Disk class or a reference to a Disk would have a hard time knowing how to correctly use the type' I disagree with this, once you are familiar with this idiom it isn't hard to use. – Yaur Jan 08 '14 at 00:15

4 Answers4

1

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();
Alain
  • 26,663
  • 20
  • 114
  • 184
  • I prefer this solution to your newer solution because it keeps the logic related to the object in the same class as the object. While I like extension methods, in this case they seem to add another place to look for the method. – Magus Jan 08 '14 at 00:20
  • @Magus - That's really the fault of Microsoft, not allowing static extension methods to be defined in non-static, or generic, or even nested classes. From an API (and Intellisense) point of view, it's impossible to distinguish this method from those defined directly in the classes themselves. – Alain Jan 08 '14 at 00:23
1

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();
Alain
  • 26,663
  • 20
  • 114
  • 184
1

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).

MarcinJuraszek
  • 124,003
  • 15
  • 196
  • 263
  • Interesting. I didn't know that casting a reference to a base type to `(dynamic)` would cause the correct overload to be created at runtime. Would that substitute for cases where you otherwise need to go `typeof(RMAService).GetMethod("Exchange").MakeGenericMethod(new Type[] { this.GetType() }).Invoke(null, new object[] { this });` ??? – Alain Jan 08 '14 at 00:38
  • Yes, pretty much yes. – MarcinJuraszek Jan 08 '14 at 00:46
0

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.

David R Tribble
  • 11,918
  • 5
  • 42
  • 52
  • 1
    I'm afraid that `Exchange(this)` has to be resolved compile-time, so it will look for `Exchange(Disk)`, not `Exchange(HDD)` or `Exchange(SSD)`. – MarcinJuraszek Jan 08 '14 at 00:28
  • I was afraid of that. Then you may have to use generics, like others have suggested. – David R Tribble Jan 08 '14 at 00:29
  • 1
    The other problem here is it doesn't force derived types to implemented an `RMA` method, so if someone tried to subclass `Disk`, it wouldn't necessarily have an RMA method. An `Disk` wouldn't have an `RMA` method either – psubsee2003 Jan 08 '14 at 00:30
  • I added a second approach. Not sure if C# allows methods to return overloaded subtypes like Java and C++ do, however. – David R Tribble Jan 08 '14 at 00:37
  • @DavidRTribble - Can't change the method signature when overriding an abstract method. Having a different return type changes the method signature in this case. – Alain Jan 08 '14 at 00:41
  • @Alain - Too bad, Java and C++ allow this. It's called [covariant return types](http://en.wikipedia.org/wiki/Covariant_return_type). – David R Tribble Jan 08 '14 at 00:42