Search code examples
c#genericsinheritancetypesabstract-base-class

Is there a way to declare members of an abstract base class that are the type of the derived class?


Suppose I have an abstract base class that I want to declare members in that will match the type of the classes that derive from this base class.

public abstract class BaseClass
{
    protected BaseClass parent;
}

public class DerivedClass1 : BaseClass
{
    // parent could be of type DerivedClass2
}

public class DerivedClass2 : BaseClass
{
    // parent could be of type DerivedClass1
}

This won't work because the parent field in each derived class can be anything that derives from BaseClass. I want to ensure that the parent field in DerivedClass1 can only be of type DerivedClass1. So I'm thinking maybe I should use generics.

public abstract class BaseClass<T> where T : BaseClass<T>
{
    protected T parent;
}

This may seem confusing and circular, but it does compile. It's basically saying parent is of type T which has to derive from the generic BaseClass. So now a derived class can look like this:

public class DerivedClass : BaseClass<DerivedClass>
{
    // parent is of type DerivedClass
}

The problem is that I have to enforce the type-matching myself when I declare DerivedClass. There's nothing stopping someone from doing something like this:

public class DerivedClass1 : BaseClass<DerivedClass2>
{
    // parent is of type DerivedClass2
}

Does C# have a way of doing this so that the type of a member declared in the base is sure to match the derived type?

I think this is similar to what this C++ question was trying to ask: Abstract base class for derived classes with functions that have a return type of the derived class


Solution

  • If I understand your requirements correctly, you have a series of classes with an inheritance relationship, and you wish to arrange them in a tree structure where each instance has a parent of the same type and only of the same type. It's an interesting problem.

    After noodling with this for a bit, may I suggest you separate the requirements into two parallel but related object graphs, so that you have

    1. A set of classes with an inheritance relationship
    2. A set of classes that can contain any of the classes from the first set, and have type-strict parents and children.

    First, let's declare the first set of classes that inherit from each other. Ignore the Node bit for now.

    public class BaseClass 
    {
        public Node ContainerNode { get; set; }
    }
    
    public class DerivedClass1 : BaseClass
    {
    }
    
    public class DerivedClass2 : BaseClass
    {
    }
    

    These classes can't do much but it's just an example.

    Now let's set up another set of classes that can participate in a tree. Each element in the tree is called a Node.

    //Basic node
    public class Node
    {
    }
    
    //A node that can contain a T (which must be a BaseClass or derived from one)
    public class Node<T> : Node where T : BaseClass
    {
        public T Parent { get; set; }
        public T This { get; set; }
    
        public Node(T innerClass) 
        {
            this.This = innerClass;
            innerClass.ContainerNode = this;
        }
    }
    

    Now we have everything we need to enforce the type safety you seek. We can create classes in your inheritance hierarchy like this:

        var child1 = new Node<DerivedClass1>(new DerivedClass1());
        var parent1 = new Node<DerivedClass1>(new DerivedClass1());
        child1.Parent = parent1.This;
    

    Let's see what happens if we mistakenly mix up DerivedClass1 and DerivedClass2:

        var child2 = new Node<DerivedClass2>(new DerivedClass2());
        var parent2 = new Node<DerivedClass1>(new DerivedClass1()); //Oops
        child2.Parent = parent2.This;  //Does not compile
    

    So as you can see, the Parent property is now typesafe.

    Now all that stuff ^^^^ looks kind of messy, so let's clean it up by adding a few helper methods.

    public class Node
    {
        public T GetParent<T>() where T : BaseClass
        {
            return ((Node<T>)this).Parent;
        }
    
        static public Node<T> Create<T>(T innerClass) where T : BaseClass
        {
            return new Node<T>(innerClass);
        }
    
        static public T GetParent<T>(T child) where T: BaseClass
        {
            return child.ContainerNode.GetParent<T>();
        }
    
        static public implicit operator T (Node<T> input)
        {
            return input.This;
        }
    }
    

    Now, since the compiler can infer the <T> arguments, our declarations are much neater:

        var child1 = Node.Create(new DerivedClass1());
        var parent1 = Node.Create(new DerivedClass1());
        child1.Parent = parent1;
    

    And it's easy for any of the derived classes to find its own parent:

    public class DerivedClass1 : BaseClass
    {
        protected DerivedClass1 Parent
        {
            get
            {
                return Node.GetParent(this);  //This is type safe!
            }
        }
    }
    

    One objection to all this is that you don't want coders to deal with this Node layer. Well, they don't, because we set up an implicit cast:

        Node<DerivedClass1> a = Node.Create(new DerivedClass1());
        DerivedClass1 b = a;  //Works!!! And is type-safe.
    

    Full working code on DotNetFiddle.