Nested generic collections

M

Michi Henning

I can pass a generic collection as ICollection<T> just fine:

static void flatCollection(ICollection<int> c) {}

// ...

List<int> l = new List<int>();
flatCollection(l); // Works fine

Now I want to pass nested collections generically:

static void nestedCollection(ICollection<ICollection<int>> c) {}

List<List<int>> l = new List<List<int>>();
nestedCollection(l); // Compile-time error

The compiler complains with:

Error 2 Argument '1': cannot convert from
'System.Collections.Generic.List<System.Collections.Generic.List<int>>'
to
'System.Collections.Generic.ICollection<System.Collections.Generic.ICollection<int>>'

This seems exceedingly strange. If I change the signature of
nestedCollection to:

static void nestedCollection(ICollection<List<int>> c) {}

things work. But that defeats the purpose because now, I can no longer
generically pass collections of collections. What gives?

Cheers,

Michi.
 
J

Jon Skeet [C# MVP]

Michi Henning said:
But that defeats the purpose because now, I can no longer
generically pass collections of collections. What gives?

It's exactly the same as why you can't use List<string> as a
List<object>.

Suppose your ICollection<ICollection<int>> was actually a
List<List<int>> at runtime. Using the interface version, someone could
add a LinkedList<int> to it, at which point the type safety has been
broken.
 
M

Michi Henning

Suppose your ICollection<ICollection<int>> was actually a
List<List<int>> at runtime. Using the interface version, someone could
add a LinkedList<int> to it, at which point the type safety has been
broken.

I'm sorry, I don't follow that. All the formal parameter types says is
that we have ICollection<int> of ICollection<int>.
So, if someone add a few List<int> and another few LinkedList<int> to
the collections, what's broken? Both are ICollection<int>, so
everything would work fine, as far as I can see.

I just don't get it: I can pass a List<int> as an ICollection<int>.
The receiver of the list knows that it is an ICollection<int>, so the
receiver cannot do anything but invoke ICollection operations on the
passed parameter (ignoring introspection and down-casts).

Now, if I have an ICollection<ICollection<int>>, why shouldn't the
same apply. The receiver gets a collection of collections. Some of the
elements of the outer collection might be List<int>, other elements
might LinkedList<int>, but so what? The receiver still can only invoke
operations on ICollection<int> on the elements of the outer list.

I honestly can't see why this would break type safety in any way.

Cheers,

Michi.
 
J

Jon Skeet [C# MVP]

I'm sorry, I don't follow that. All the formal parameter types says is
that we have ICollection<int> of ICollection<int>.
So, if someone add a few List<int> and another few LinkedList<int> to
the collections, what's broken? Both are ICollection<int>, so
everything would work fine, as far as I can see.

Correct, but look at your original code (variable renamed for
clarity):

List<List<int>> lists = new List<List<int>>();
nestedCollection(lists);

That says that each element of lists is a List<int>. That means you
can't do:
lists.Add(new LinkedList<int>());
With me so far?

But suppose your nestedCollection method were written like this:

static void NestedCollection(ICollection<ICollection<int>> c)
{
c.Add (new LinkedList<int>());
}

You'd be trying to add the LinkedList<int> to a List<List<int>>. It's
fine as far as the NestedCollection method is concerned - there's
nothing problematic in that method - but it would still be breaking
type safety.


If you change your original declaration to:
List<ICollection<int>> lists = new List<ICollection<int>>();
then it should be fine. At that point you're saying that you don't
care what each elements of "lists" is so long as it implements
ICollection<int>.

Jon
 
P

Peter Duniho

Michi said:
I'm sorry, I don't follow that. All the formal parameter types says is
that we have ICollection<int> of ICollection<int>.

(Well, actually it says you have an ICollection<> of ICollection<int>,
or in other words an ICollection<ICollection<int>>. I think you knew
that, but just to be clear...)

Anyway, that's correct. That is _all_ it says.

Let's assume you're allowed to do what you want. That means that from
the point of view of the code within that method, it could put anything
into the ICollection<ICollection<int>> as long as it's an
ICollection<int>. Any number of generic classes meet this requirement.

Jon's example was the LinkedList<int> generic class. So, let's assume
that you pass a List<List<int>> to the method. Then let's assume that
the method tries to add a LinkedList<int> to the
ICollection<ICollection<int>> in the method. This would be legal under
your suggested rules. Unfortunately, this would also allow your
List<List<int>> (which is what was actually passed to the method and
which is the class being modified when you add the LinkedList<int>) to
contain a LinkedList<int>.

That violates the requirement that the List<List<int>> only contain
instances of List said:
So, if someone add a few List<int> and another few LinkedList<int> to
the collections, what's broken?

that's a serious said:
Both are ICollection<int>, so
everything would work fine, as far as I can see.

As far as the method knows, the items in the collection are only
ICollection<int> instances. The problem is that the collection is
I just don't get it: I can pass a List<int> as an ICollection<int>.

Yes, that's true, you can. List<int> implements ICollection<int> and so
it can be used anywhere that an ICollection<int> is required.

But you can't pass a List<int> as a List<object> or an
ICollection<object>. And it is for the same reason. The issue here
isn't about whether the contained element is compatible with the
declared contained element in the method. It's whether the outer
collection itself is identical.
The receiver of the list knows that it is an ICollection<int>, so the
receiver cannot do anything but invoke ICollection operations on the
passed parameter (ignoring introspection and down-casts).

But the analog here isn't passing a List<int> as an ICollection<int>.
Now, if I have an ICollection<ICollection<int>>, why shouldn't the
same apply. The receiver gets a collection of collections. Some of the
elements of the outer collection might be List<int>, other elements
might LinkedList<int>, but so what?

The problem isn't whether the method being called can safely use the
list. The problem is whether the list can be guaranteed to remain
correct relative to its actual type. That is, when the method is done,
can the caller expect the list to still only have List<int> elements?

Because the method doesn't know anything about the type except the type
of the parameter, the answer to that question is no. The method could
have added anything that implements ICollection<int> to the list,
The receiver still can only invoke
operations on ICollection<int> on the elements of the outer list.

That's true. But it doesn't have anything to do with why you can't pass
the List said:
I honestly can't see why this would break type safety in any way.

I hope the above explains it satisfactorily.

Pete
 
M

Michi Henning

I hope the above explains it satisfactorily.

Yes, thanks--yours and John's explanation are perfectly clear. In
fact, I realized about ten minutes after posting my follow-up that the
problem is the Add() method on ICollection<T>. So, yes, it makes
sense.

I have to say though that this is a rather steep price to pay for the
lack of const-correctness. If methods could be marked const and could
be passed by const reference (as in C++), I wouldn't have this
problem.

Anyway, given the absence of const-correctness, it makes perfect
sense, thanks.

Cheers,

Michi.
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Top