Manuel Faux
Manuel Faux

Reputation: 2457

Class Inheritance Design with Generic Lists

I am currently struggling with a class design involving generic lists:

Code says more than thousand words:

class Document
{
    public List<Result> Results { get; } = new List<Result>();
}

class Result
{
}

class SpecialDocument : Document
{
    public new List<SpecialResult> Results { get; } = new List<SpecialResult>();
}

class SpecialResult : Result
{
}

What I don't like of the current design is that SpecialDocument.Results hides Document.Results. If one has the Document view on a SpecialDocument, there are no results at all, even there could be the Result view of all SpecialResult elements.

What I would like to accomplish is:

SpecialDocument doc = new SpecialDocument();
doc.Results.Add(new SpecialResult());
Assert.AreEqual(1, (doc as Document).Results.Count); // Here my design obviously fails right now

... I'd like to accomplish that without loosing type safety (as that's actually the reason for List<T> not being covariant).

Edit

I forgot to mention that SpecialDocument and Document actually need to have the same successor (or implement the same interface), such that they can coexist within one collection:

List<Document> documents = new List<Document>()
{
    new Document(),
    new SpecialDocument()
};

Upvotes: 1

Views: 66

Answers (2)

Bradley Uffner
Bradley Uffner

Reputation: 16991

If it is acceptable for IDocument.Results to be of type IEnumerable<Result> instead of IList<Result>, this will work.

The Result property must be typed this way due to co-variance / contra-variance rules.

Code is based on @Kyle B's excellent answer

void Main()
{

    IDocument doc = new SpecialDocument();

    //I added AddResult() to the interface to allow adding results, instead of calling Add() directly on the list.
    doc.AddResult(new SpecialResult());
    Assert.AreEqual(1, doc.Results.Count);

    // prooving that the items can be added to a list, and that list can handle all the result types.
    var docs = new List<IDocument>();
    docs.Add(new Document());
    docs.Add(new SpecialDocument());
    var results = docs.SelectMany(d => d.Results)
    // results now contains all results from all documents
}

abstract class Document<T> : IDocument where T : Result
{
    public List<T> Results { get; } = new List<T>();

    IEnumerable<Result> IDocument.Results => Results;

    void IDocument.AddResult(Result result)
    {
        this.Results.Add((T)result);
    }
}

abstract class Result
{
}

class Document : Document<Result>
{
}

class SpecialDocument : Document<SpecialResult>
{
}

class SpecialResult : Result
{
}

interface IDocument
{
    IEnumerable<Result> Results { get; }
    AddResult(Result result);
}

Upvotes: 1

Kyle B
Kyle B

Reputation: 2889

Have you tried generics?

abstract class Document<T> where T : Result
{
    public List<T> Results { get; } = new List<T>();
}

abstract class Result
{
}

class SpecialDocument : Document<SpecialResult>
{
}

class SpecialResult : Result
{
}

SpecialDocument will automatically instantiate List<SpecialResult>.

Upvotes: 4

Related Questions