Home > Back-end >  List<Parent> holding Child element without losing properties C#
List<Parent> holding Child element without losing properties C#

Time:12-16

I have these classes

class Start
{
    public List<Base> list { get; set; }
    public Start() 
    {
        list = new List<Base>();
    }
}

public abstract class Base
{
    public int a { get; set; }
}

class B : Base
{
    public int b;
    public B(int a, int b) { this.a = a; this.b = b; }
}

class C : Base
{
    public int c;
    public C(int a, int c) { this.a = a; this.c = c; }
}

I want list property of class Start to hold instances of class B or instances of class C (not both together, but it may hold the same type of any of B or C)
If possible, I don't want to use Generics
In C#, This is possible:

List<Object> lst = new List<Object>();
lst.Add(1);
list.Add("Text");
Console.WriteLine("{0} {1}", lst[0], lst[1]);

I don't understand why I can't make a similar behavior here:

Start s = new Start();
B b = new B(1, 2);
s.list.Add(b);
Console.WriteLine(s.list[0].a); //works
Console.WriteLine(s.list[0].b); //doesn't work

CodePudding user response:

The difference between the two snippets is that in the first one you are not accessing any type-specific information (fields/properties/methods), i.e. something like the following will not compile too:

List<Object> lst = new List<Object>();
lst.Add(1);
list.Add("Text");
// will not compile despite string having Length property:
Console.WriteLine("{0} {1}", lst[0], lst[1].Length); 

a is common property declared in Base class, so it is available for every child of Base, if you want to access child specific properties you need to type test/cast :

Start s = new Start();
B b = new B(1, 2);
s.list.Add(b);
Console.WriteLine(s.list[0].a); //works
if(s.list[0] is B b)
{
    Console.WriteLine(b.b);
}

or make Start generic:

class Start<T> where T: Base
{
    public List<T> list { get; set; }
    public Start() 
    {
        list = new List<T>();
    }
}

var s = new Start<B>();
s.list.Add(new B(1, 2));
Console.WriteLine(s.list[0].b);

P.S.

Note that overriding ToString in Base, B and A will make Console.WriteLine("{0}", s.list[0]); "work":

class B : Base
{
    // ...
    public override string ToString() => return $"B(A: {a} B: {b})";
}

class C : Base
{
    // ...
    public override string ToString() => return $"C(A: {a} B: {c})";
}

Start s = new Start();
B b = new B(1, 2);
s.list.Add(b);
s.list.Add(new C(4, 2));
Console.WriteLine("{0} {1}", s.list[0], s.list[1]); // prints "B(A: 1 B: 2) C(A: 4 B: 2)"

So possibly you can introduce some method in Base which will allow you to use List<Base> (hard to tell without knowing actual use case).

CodePudding user response:

The List<Object> example is possible because both int and string inherit from Object, which provides a ToString() method that is called implicitly on the line that writes the output. That is, no members of either the int or string types are used in that example that are specific to their own types.

You might accomplish what you need without generics by adding an interface that both B and C can implement, since both the b and c properties are compatible (they are both ints). However, this is clearly a contrived example, and I expect the real code is more complicated. In that case, generics are likely your best option.

CodePudding user response:

because all Base objects dont have 'b' fields

you need to test to see if list[0] is an instance of 'B' and then cast it to a B

if (list[0] is B )
{
       Console.WriteLine(((B)(list[0]).b);  
}

CodePudding user response:

Based on the comments underneath the question, perhaps a combination of both a non-generic interface and a generic Start class could work in this scenario.

The non-generic base interface for the generic Start class would declare a get-only List property as IReadOnlyList<Base>. IReadOnlyList is co-variant, allowing to return different List<T> instances where T is a concrete derived type from Base.

public interface IStart
{
    IReadOnlyList<Base> List { get; }
}

The generic Start<TBase> class implements IStart, puts the IStart.List property in an explicit interface declaration and declares its own List property that is typed as List<TBase>.

public class Start<TBase> : IStart where TBase : Base
{
    public List<TBase> List { get; set; }

    IReadOnlyList<Base> IStart.List => this.List;

    public Start() 
    {
        List = new List<TBase>();
    }
}

Note that both the explicit interface implementation of IStart.List and Start<TBase>'s own List property return the same List<TBase> instance.

This setup makes the following things possible (or impossible, see the code comments):

var startC = new Start<C>();

startC.List.Add(new C()); // this works, of course it works

startC.List.Add(new B()); // The compiler will NOT allow this


IStart SomeMethodProducingAStart()
{
   if (someCondition)
       return new Start<B>();
   else
       return new Start<C>();
}

void SomeMethodConsumingAStart(IStart start)
{
    if (start is Start<B> startWithBs)
    {
       // do stuff with startWithBs...
       Console.WriteLine(startWithBs.List[0].a);
       Console.WriteLine(startWithBs.List[0].b);
    }
    else if (start is Start<C> startWithCs)
    {
       // do stuff with startWithCs...
       Console.WriteLine(startWithCs.List[0].a);
       Console.WriteLine(startWithCs.List[0].c);
    }

    // if you don't care about the members specific to either B or C,
    // just do this
    Console.WriteLine(start.List[0].a);

    // since start can be any Start<T>
    // the following is denied by the compiler
    // simply by virtue of IStart.List being an IReadOnlyList
    start.List.Add(new C());    // no can do!
}


Whether this approach fits your application scenario well is for you to determine, but it's an approach that tries to avoid granular pattern matching on individual list items and aims at simplifying working with Start instances once they have been pattern-matched/cast to the correct Start<TBase> type.

  • Related