Max S
Max S

Reputation: 156

Class with generic parameter of the same generic class type

I had a class which represents a prefix search tree:

public class PrefixTree<TData>
{
    private PrefixTree<TData>[] _children;

    private void SomeMethod()
    {
        _children = new PrefixTree<TData>[10];
    }
}

Then I created a derived class with additional features for its nodes:

public class NewPrefixTree<TData> : PrefixTree<TData>

The problem is that in SomeMethod() of derived class we still create instances of base class and it doesn't fit the meaning.

I refactored the base class to this:

public abstract class PrefixTree<TData, TNode>
    where TNode : PrefixTree<TData, TNode>, new()
{
    private TNode[] _children;

    private void SomeMethod()
    {
        _children = new TNode[10];
    } 
}

Despite the base class has complete functionality itself, I had to make it abstract because I can't write new DigitalPrefixTree<TData, DigitalPrefixTree<int, ...>>() .

But now I can use it this way and it works perfectly:

public class NewPrefixTree<TData> : PrefixTree<TData, NewPrefixTree<TData>> {} // for derived class
//or
public class PrefixTree<TData> : PrefixTree<TData, PrefixTree<TData>> {} // to use the base functionality

I've never done this before and I wonder if it's a good idea to declare a class with generic parameter of the same generic class type. Or I need to make some tricks with co/contra-variance of generic interfaces (but it probably doesn't work as I use the class type as method’s parameters type and as return type as well)?

Upvotes: 2

Views: 156

Answers (1)

Alexander Zhyshkevich
Alexander Zhyshkevich

Reputation: 396

Try this:

public interface INode<out TData>
{
    TData Data { get; }

    IEnumerable<INode<TData>> Children { get; }

    public IEnumerable<TRequiredData> GetNestedData<TRequiredData>();
}

public interface ITree<out TData>
{
    IEnumerable<INode<TData>> Children { get; }

    IEnumerable<TRequiredData> GetNestedData<TRequiredData>();
}

public class Node<TData> : INode<TData>
{
    public TData Data { get; }
    public IEnumerable<INode<TData>> Children { get; }

    public Node(TData data, IEnumerable<INode<TData>>? children = null)
    {
        Data = data;
        Children = children ?? Enumerable.Empty<INode<TData>>();
    }

    public IEnumerable<TRequiredData> GetNestedData<TRequiredData>()
    {
        List<TRequiredData> result = new();

        if (Data is TRequiredData requiredData)
            result.Add(requiredData);

        foreach (var child in Children)
            result.AddRange(child.GetNestedData<TRequiredData>());

        return result;
    }
}

public class Tree<TData> : ITree<TData>
{
    public IEnumerable<INode<TData>> Children { get; }

    public Tree(IEnumerable<INode<TData>> children)
    {
        Children = children;
    }

    public IEnumerable<TRequiredData> GetNestedData<TRequiredData>()
    {
        List<TRequiredData> result = new();

        foreach (var node in Children)
            result.AddRange(node.GetNestedData<TRequiredData>());

        return result;
    }
}

And here is example of usage:

    record SomeRecord();

    class SomeClass {}

    static void Main(string[] args)
    {
        var nodeWithNested = new Node<SomeClass>(
            data: new SomeClass(),
            children: new List<INode<SomeClass>>()
            {
                new Node<SomeClass>(new SomeClass()),
                new Node<SomeClass>(new SomeClass())
            });

        var nodes = new List<INode<object>>()
        {
            new Node<SomeClass>(new SomeClass()),
            nodeWithNested,
            new Node<SomeRecord>(new SomeRecord()),
        };

        var tree = new Tree<object>(nodes);

        var someClasses = tree.GetNestedData<SomeClass>(); // 4 items
        var someRecords = tree.GetNestedData<SomeRecord>(); // 1 item
    }

This approach based on out generic modifier.

The only restriction is that you can not use structs (int, bool and ect.) as they don't have common object to cast to.

Hope this will be useful.

Upvotes: 2

Related Questions