Anlo
Anlo

Reputation: 3266

Generic list of lists, converting List<List<T>> to IList<IList<T>>

We're using a class library that performs calculations on 3D measurement data, it exposes a method:

MeasurementResults Calculate(IList<IList<Measurement>> data)

I would like to allow calling this method with any indexable list of lists (of Measurement of course), for example both:

Measurement[][] array;
List<List<Measurement>> list;

Calling the method using the array works fine, which is a bit strange. Is there some compiler trick at work here? Trying to call with the List gives the familiar error:

cannot convert from 'List<List<Measurement>>' to 'IList<IList<Measurement>>'

So, I have written a facade class (containing some other things as well), with a method that splits the generic definition between the argument and method and converts to the IList type if necessary:

MeasurementResults Calculate<T>(IList<T> data) where T : IList<Measurement>
{
  IList<IList<Measurement>> converted = data as IList<IList<Measurement>>;
  if(converted == null)
    converted = data.Select(o => o as IList<Measurement>).ToList();
  return Calculate(converted);
}

Is this a good way to solve the problem, or do you have a better idea?

Also, while testing different solutions to the problem, I found out that if the class library method had been declared with IEnumerable instead of IList, it is ok to call the method using both the array and the List:

MeasurementResults Calculate(IEnumerable<IEnumerable<Measurement>> data)

I suspect that there is some compiler trick at work again, I wonder why they haven't made IList work with List while they were at it?

Upvotes: 6

Views: 4375

Answers (3)

Rune FS
Rune FS

Reputation: 21742

There are a few things with you implementation that I would change. First of all it might insert nulls into the result where there were objects in the original. Personally I would prefer it to throw.

MeasurementResults Calculate<T>(IList<T> data) where T : IList<Measurement>
{
    return Calculate(data as IList<IList<Measurement>>
                     ?? data.Cast<IList<Measurement>>().ToList());
}

Is a short example a like what I would personally do in your case, though I doubt I would actually implement the method. The interface was written like that for a reason (I hope) and I would try to use that knowledge in my implementation on top instead of fighting it if possible. Any changes made to the list in the method will not be reflected in the original list if you use your code (or anything like that). It's a new list being passed to the method and since the signature asks for an IList it's possible that changes can be made.

Aside from not potentially having nulls in the list instead of objects I've change to use the cast method since that's basically what you are trying to accomplish. (Cast does not allow for custom conversion operators).

Upvotes: 1

AakashM
AakashM

Reputation: 63340

Suppose we have a method

void Bar(IList<IList<int>> foo)

Within Bar, it should then be perfectly admissible to add a int[] to foo - after all, int[] implements IList<int>, doesn't it?

But if we had called Bar with a List<List<int>>, we would now be trying to add a int[] to something that only accepts a List<int>! which would be bad. So the compiler doesn't let you do this.

Also, while testing different solutions to the problem, I found out that if the class library method had been declared with IEnumerable instead of IList, it is ok to call the method using both the array and the List:

Indeed, because if the behavioural contract just says "I can output ints", nothing can go wrong. The key terms for further research are covariance and contravariance, and no one* can ever remember which is which.

In your particular case, if Calculate is only ever reading its input, changing it to consume IEnumerable is absolutely the right thing to do - it both allows you to pass in any qualifying objects, and it further communicates to anyone reading the signature that this method is intentionally designed to only consume, not mutate, its input.


* well, mostly no one

Upvotes: 3

Jon Skeet
Jon Skeet

Reputation: 1499800

It's okay to do this with IEnumerable<T>, because that is covariant in T. It wouldn't be safe to do that with IList<T>, as that is used for both "input" and "output".

In particular, consider:

List<List<Foo>> foo = new List<List<Foo>>();
List<IList<Foo>> bar = foo;
bar.Add(new Foo[5]); // Arrays implement IList<T>

// Eh?
List<Foo> firstList = foo[0];

See MSDN for more information about generic covariance and contravariance.

Upvotes: 6

Related Questions