1

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()
};
1
  • 2
    Couldnt you make Document generic where T : Result Commented May 7, 2018 at 18:51

2 Answers 2

4

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

Sign up to request clarification or add additional context in comments.

10 Comments

Good idea, but then SpecialDocument is not derived from Document anymore, right? So one cannot do a cast like doc as Document for SpecialDocument doc...
@ManuelFaux That is currrect. The typical solution is to create a non-generic IDocument interface that Document<T> implements. That will allow you to put any kind of Document<T> derived class in to an IDocument doc variable. You could also use an abstract, non-generic, Document class as the base class for Document<T> for the same purpose.
@BradleyUffner And is Results part of the interface specification in that case? Could one access Results from the IDocument point of view?
Yes, you can have it as part of the interface or base-class, but unfortunately, It'll have to be typed as Result (since that's what T is constrained to). You won't be able to get the specific Result sub-class. It will still be the correct type as far as polymorphism is concerned, but it will be viewed though Result. That may or may not be a problem for you, depending on what you want to do with Result.
@BradleyUffner Could you maybe post your solution? class Document<T> : IDocument where T : Result does not compile with IDocument containing List<Result> Result.
|
1

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);
}

2 Comments

I've updated my answer to take your specific unit test requirements in to account.
you have a cat as your icon and I have a dog.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.