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

10
  • 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... Commented May 7, 2018 at 19:03
  • @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. Commented May 7, 2018 at 19:26
  • @BradleyUffner And is Results part of the interface specification in that case? Could one access Results from the IDocument point of view? Commented May 7, 2018 at 19:37
  • 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. Commented May 7, 2018 at 19:45
  • @BradleyUffner Could you maybe post your solution? class Document<T> : IDocument where T : Result does not compile with IDocument containing List<Result> Result. Commented May 7, 2018 at 19:49
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
  • I've updated my answer to take your specific unit test requirements in to account. Commented May 7, 2018 at 20:11
  • you have a cat as your icon and I have a dog.
    – Kyle B
    Commented May 7, 2018 at 21:03

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.