Click to See Complete Forum and Search --> : When's an IEnumerable<T> not an IEnumerable<T>?


Aurrin
November 17th, 2007, 03:19 PM
Okay, I was trying to build up a utility class for efficient joining of collections. To stick two collections together, you could just copy one and then call Append() as necessary, but I read an article about a cleaner way (http://www.c-sharpcorner.com/UploadFile/rmcochran/CartesianProductAndPermutation10082007165346PM/CartesianProductAndPermutation.aspx) to do this and came up with the following:


public static class CollectionUtilities
{
public static IEnumerable<T> JoinCollections<T>( IEnumerable<IEnumerable<T>> collections )
{
foreach ( IEnumerable<T> collection in collections )
{
foreach ( T item in collection )
{
yeild return T;
}
}
}
}


So, this takes a collection of collections, and then tacks them onto one another. I also made an overload that does the same thing but takes params instead, so that an array of collections, or collections seperated by commas can be passed. (A utility should be easy to use, after all.)

And, I dutifully set up some code to test it:



// Set up a list of lists of ints

List<int> FirstList = new List<int>();
FirstList.Add( 1 );
FirstList.Add( 2 );
FirstList.Add( 3 );
List<int> SecondList = new List<int>();
SecondList.Add( 4 );
SecondList.Add( 5 );
SecondList.Add( 6 );
List<List<int>> Lists = new List<List<int>>();
Lists.Add( FirstList );
Lists.Add( SecondList );

// Set up the expected result, to test

List<int> ExpectedResult = new List<int>();
ExpectedResult.Add( 1 );
ExpectedResult.Add( 2 );
ExpectedResult.Add( 3 );
ExpectedResult.Add( 4 );
ExpectedResult.Add( 5 );
ExpectedResult.Add( 6 );

// Run the function

List<int> JoinResult = CollectionUtilities.JoinCollections<int>( Lists );

// Test that the expected result was obtained

Debug.Assert( JoinResult.Equals( ExpectedResult ) );




Except... when try to compile, I get an error:


Error, Argument 1: Could not convert (System.Collections.Generic.List<System.Collections.Generic.List<int>>) to (System.Collections.Generic.IEnumerable<System.Collections.Generic.IEnumerable<int>>)


Wha--?!

List<T> implements IEnumerable<T>, so how can it fail a cast to IEnumerable<T>? Especially when the MSDN documentation says that in order to call methods of an explicitly implemented interface, you must first cast to that interface?

(Please ignore small spelling errors, the code is on a different machine, so I had to re-type it.)

Mutant_Fruit
November 17th, 2007, 04:13 PM
What you're probably trying to do is this:


// The 'join' method
public static IEnumerable<T> JoinCollections<T>(IEnumerable<IEnumerable<T>> collections)
{
foreach (IEnumerable<T> collection in collections)
foreach (T item in collection)
yield return item;
}

// Using the join method

foreach (int i in CollectionUtilities.JoinCollections<int>(Lists))
{
// In your example this will print out 1, 2, 3, 4, 5, 6
Console.WriteLine("Value: {0}", i);
}

Aurrin
November 17th, 2007, 11:27 PM
Yep. And when I try to compile that (copy-pasted that code into a form and re-arranged to make it fit) it does the same thing. Basically, the compiler is objecting to supplying List<List<int>> where IEnumerable<IEnumerable<int>> is expected, claiming that it cannot convert. Here is the exact exception pair:

Error 1 The best overloaded method match for 'WindowsApplication1.Form1.JoinCollections<int>(System.Collections.Generic.IEnumerable<System.Collections.Generic.IEnumerable<int>>)' has some invalid arguments C:\Documents and Settings\Azure Main\Local Settings\Application Data\Temporary Projects\WindowsApplication1\Form1.cs 22 31 WindowsApplication1

Error 2 Argument '1': cannot convert from 'System.Collections.Generic.List<System.Collections.Generic.List<int>>' to 'System.Collections.Generic.IEnumerable<System.Collections.Generic.IEnumerable<int>>' C:\Documents and Settings\Azure Main\Local Settings\Application Data\Temporary Projects\WindowsApplication1\Form1.cs 22 58 WindowsApplication1

Mutant_Fruit
November 18th, 2007, 08:13 AM
Ah, i getcha now.

That's a limitation of generics. I can't remember the exact word for it now. It is possible to express that construct in raw CIL, but not through C#.

One work around is:

List<IEnumerable<int>> Lists = new List<IEnumerable<int>>();
Lists.Add(FirstList);
Lists.Add(SecondList);


Thing is, the way generics work, you need a List of IEnumerable<int>. Not a list of something that inherits from IEnumerable<int>. So, the workaround is to realise it's perfectly valid to add a List<int> to a List<IEnumerable<int>> as a List<int> is an IEnumerable<int> (as you pointed out).

I hope that makes sense to you :)

EDIT: The original error i fixed was you wrote this:
List<int> JoinResult = CollectionUtilities.JoinCollections<int>( Lists );

List<int> != IEnumerable<int>, and JoinCollection<T> returns an IEnumerable<T>, so thats where the invalid cast was coming from.

Aurrin
November 18th, 2007, 04:48 PM
Ah, i getcha now.

That's a limitation of generics. I can't remember the exact word for it now. It is possible to express that construct in raw CIL, but not through C#.

One work around is:

List<IEnumerable<int>> Lists = new List<IEnumerable<int>>();
Lists.Add(FirstList);
Lists.Add(SecondList);


Thing is, the way generics work, you need a List of IEnumerable<int>. Not a list of something that inherits from IEnumerable<int>. So, the workaround is to realise it's perfectly valid to add a List<int> to a List<IEnumerable<int>> as a List<int> is an IEnumerable<int> (as you pointed out).

Ah, so the compiler isn't smart enough to check for nested interface implementations? That's rather disappointing, actually, as that would be one of the more useful applications of generics.


EDIT: The original error i fixed was you wrote this:
List<int> JoinResult = CollectionUtilities.JoinCollections<int>( Lists );

List<int> != IEnumerable<int>, and JoinCollection<T> returns an IEnumerable<T>, so thats where the invalid cast was coming from.

I realized that error, but it wasn't where the errors I was pasting came from. Those were all complaining about where I had supplied List<List<int>> as a parameter. Well, maybe they'll fix that in VS 2008? One can only hope...

Mutant_Fruit
November 18th, 2007, 05:03 PM
I remembered the term for what you're trying to do: contravariance.

http://weblog.ikvm.net/PermaLink.aspx?guid=21f9ed86-827e-4666-9553-5d4a8d735b7e

Also if you google c# contravariance, you'll see the issues with what you're trying to do. If you're willing to write the IL manually, you can actually implement what you're trying to do. C# (currently) has no way to express that type of construct though.

Aurrin
November 18th, 2007, 05:03 PM
I had a nasty feeling something deeper was going on, so I cross-posted to MSDN's C# Language, and they pointed me to a couple of resources on this topic, apparently called covariance and contravariance:

Eric Lippert's Blog - Covariance and Contravariance (http://blogs.msdn.com/ericlippert/archive/tags/Covariance+and+Contravariance/default.aspx)

MSDN Generics FAQ: Fundamentals (http://msdn2.microsoft.com/en-gb/library/aa479859.aspx#fundamentals_topic12)