Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Polymorphism with Lists

I have an inheritance structure of objects, somewhat like the following:

public class A { }
public class B : A { }
public class C : B { }

Ideally, I would like to be able to pass a List of A, B or C to a single method like this:

private void Method(List<A> foos) { /* Method Implementation */ }

B toBee = new B();
B notToBee = new B();
List<B> hive = new List<B> { toBee, notToBee };

// Call Method() with a inherited type.  This throws a COMPILER ERROR 
// because although B is an A, a List<B> is NOT a List<A>.
Method(hive);

I would like to come up with a way to get this same functionality, with as little code duplication as possible.

The best I can come up with is to create wrapper methods which accept lists of the various types and then loop through the passed lists to call the same method; finally using polymorphism to my advantage:

private void Method(List<A> foos) { foreach (var foo in foos) Bar(foo); }
private void Method(List<B> foos) { foreach (var foo in foos) Bar(foo); }
private void Method(List<C> foos) { foreach (var foo in foos) Bar(foo); }

// An A, B or C object can be passed to this method thanks to polymorphism
private void Bar(A ayy) { /* Method Implementation */ }

Bus as you can see, I have literally copy and pasted that method three times, only changing the type contained in the generic of the list. I have come to believe that any time you start copy and pasting code, there is a better way to do it... but I can't seem to come up with one.

How I can accomplish such a feat without the undesirable copy and pasting?

like image 326
Zach Posten Avatar asked Nov 29 '22 14:11

Zach Posten


2 Answers

You need covariance.

If you can write the following:

private void Method(List<A> foos) { foreach (var foo in foos) Bar(foo); }
private void Method(List<B> foos) { foreach (var foo in foos) Bar(foo); }
private void Method(List<C> foos) { foreach (var foo in foos) Bar(foo); }

// An A, B or C object can be passed to this method thanks to polymorphism
private void Bar(A ayy) { /* Method Implementation */ }

Then it only means List<T> is the wrong parameter type in the first place.
Better use IEnumerable<out T> instead, for two reasons:

  • it's covariant, so an IEnumerable<C> is an IEnumerable<A>
  • it's really the only thing you need in the method, since you're only doing an iteration. You could pass in a HashSet<B>, a LinkedList<A> or a C[] and it would still work just fine.
private void Method(IEnumerable<A> foos)
{
    foreach (var foo in foos)
        Whatever(foo);
}

List<T> is a mechanism - it stores T references (or values for value types) in contiguous memory in an array. You don't require that particular property here.

By contrast, IEnumerable<out T> is a contract. It only says you can enumerate a sequence, which is all you need in this case.

There are other covariant interface types you could use, like IReadOnlyCollection<out T> (if you need to know the item count up front or if you have to enumerate multiple times) or IReadOnlyList<out T> (if you need item indexing).

Keep in mind most of the time it's better to take an interface type as a parameter where possible as it lets you switch the underlying implementation when needed.

But if you actually have to modify the list, then covariance won't be enough. In that case, go with BartoszKP's solution, but, hey, you can still use an IList<T> instead of a List<T> :-)

like image 44
Lucas Trzesniewski Avatar answered Dec 10 '22 12:12

Lucas Trzesniewski


Create a generic method:

private void Method<T>(List<T> foos) 

so you will be able to use it for various List<T>. You can also narrow down the list of accepted arguments for the method to handle only A subclasses, using generic constraints:

private void Method<T>(List<T> foos) 
    where T : A

Then you are sure that every element of foos can be used as an instance of A:

private void Method<T>(List<T> foos) 
    where T : A
{
    foreach (var foo in foos)
    {
        var fooA = foo as A;

        // fooA != null always (if foo wasn't null already)

        Bar(fooA);
    }
}

As Lucas Trzesniewski indicates in his answer, it is even better to use IEnumerable<T>, if you don't need to modify the collection. It is covariant so you won't be having the problem you've described.

like image 165
BartoszKP Avatar answered Dec 10 '22 13:12

BartoszKP