Home > Mobile >  How to handle a List of Base class that contains elements of Derived classes
How to handle a List of Base class that contains elements of Derived classes

Time:10-13

public async IEnumerable<ParentClass> ReadResultAsync
{
    var results = new List<ParentClass>();

    results.Add(ChildClassA);
    results.Add(ChildClassB);

    return results;
}

I do this process in service layer. When I return results => IEnumerable<ParentClass> to Controller and show the results after calling a request, I face a problem: It only returns the properties which the ParentClass has.

Ex. ParentClass has property A, B, C.
ChildClassA inherit ParentClass has property D, E.
ChildClassB inherit ParentClass has property F.

The results only can get properties A, B, C. (Which I expect ChildClassA (A, B, C, D, E) and ChildClassB (A, B, C, F) in the same list)

My temporary solution is to change ParentClass to object, but I don't think it's good.

public async IEnumerable<object> ReadResultAsync
{
    var results = new List<object>();

    results.Add(ChildClassA);
    results.Add(ChildClassB);

    return results;
}

Is there any solution can create a parent list that can contain all child classes (which I want)?

CodePudding user response:

Your list can already contain instances of any class derived from ParentClass, as you have proved. The problem, if you can call it that, is that the code that uses the list knows nothing about the objects other than that they are of type ParentClass, so it is only able to access members of that type. How could it possibly access other methods when it doesn't know they exist? Using object instead doesn't help because then you can access fewer members still.

You could use dynamic, which would allow you to specify any member at all in the consuming code, but then you lose all type safety, so that is something you should generally avoid if possible. It basically provides the same late binding functionality that VB has always had.

If the number of derived types is small then your best bet may be to use conditional casting, e.g.

foreach (var p in await ReadResultAsync())
{
    if (p is ChildClassA ca)
    {
        // Use ca here.
    }
    else if(p is ChildClassB cb)
    {
        // Use cb here.
    }
}

CodePudding user response:

The easiest solution would be to use dynamic or object way. Type safety doesnt matter as you only emit the data.

You could use a mapping technique when emitting the objects by creating a DTO (data transfer object) or You could use Linq before emitting the result. But the remapping requires resources for processing.

An example of a linq solution could be:

 public async IEnumerable<object> ReadResultAsync
{
    var results = new List<object>();

    results.Add(ChildClassA);
    results.Add(ChildClassB);

    return await (from n in results 
            select new {
               //Here you assign the properties that you want and
               //cast the more abstract type to its derived type, 
               //and then assign more properties.
             } ).ToListAsync();
}

You create an extension method that does the process by using reflection and perhaps some expression trees. That would make the mapping more generic and reusable.

Another solution is to use interfaces instead of inheritance.

The last interface would have all the properties. A,B,C,D,E,F. The last and most derived interface will get its properties from the more abstract interfaces. So for example interface1 will have A,B,C, while interface2 will have E and F.

The problem with this approach is that it contradicts Liskov substitution processes as some derived classes wont have the implementation for some properties.

CodePudding user response:

If you declare your returning object with type IEnumerable<ParentClass>, the compiler cannot make any assumptions about the elements of this sequence, more than those guaranteed by the signature of ParentClass.

In other words, the elements might each ultimately be one different derived type, but anyone consuming that object doesn't know (and should not know) what are the concrete types.

To understand better, you can search and learn about concepts like: Liskov substitution principle, Covariance/Contravariance and Interfaces.

If it makes sense, based on your app logic, to assume a specific derived type of an element and call derived-type specific methods on the object, you have a few options:

  1. Get the element from the enumerable, and then cast to the derived type, like this

    foreach (ParentClass pc in results) {
      ChildClassA cca = (ChildClassA)pc; // could throw InvalidCastException
      cca.D();
    // ...
    }
    

    Note that this would fail with an InvalidCastException if, for example, one of the elements is of type ChildClassB and cannot be cast into ChildClassA. You can mitigate this by doing a safe cast with the as operator, and handle null.

    foreach (ParentClass pc in results) {
      ChildClassA cca = pc as ChildClassA; // could end up being null
      cca.D(); // could throw NullReferenceException
    // ...
    }
    
  2. Use the is operator to pattern match.

    foreach (ParentClass pc in results) {
      if (pc is ChildClassA cca) {
        cca.D(); // would only reach here if it is indeed ChildClassA
      }
    // ...
    }
    
  3. try to cast the whole enumerable to a derived type with the LINQ Cast extension method:

    IEnumerable<ChildClassA> childAResults = results.Cast<ChildClassA>();
    // could throw InvalidCastException
    foreach (ChildClassA cca in childAResults) {
      cca.D();
    // ...
    }
    

    This has the same issue as option 1 above, it would throw InvalidCastException if any element cannot be cast into ChildClassA.

  4. Filter the enumerable to only get elements of type ChildClassA (or derived), using the Linq method OfType.

    IEnumerable<ChildClassA> childAResults = results.OfType<ChildClassA>();
    foreach (ChildClassA cca in childAResults) { // safe
      cca.D();
    // ...
    }
    
  5. Hack with dynamic or object, which I do not recommend for this case by any means.

  6. Review your design. Trying to assume a derived type on an object is commonly a code smell, that your parent class abstraction is weak or that you are leaking implementation details and braking encapsulation. Check the SOLID principles.

  • Related