1

Hopefully this isn't a dupe, couldn't find anything related online

I'm getting a strange compile time error in the following extension method:

public static TCol AddRange<TCol, TItem>(this TCol e, IEnumerable<TItem> values) 
    where TCol: IEnumerable<TItem>
{
    foreach (var cur in e)
    {
        yield return cur;
    }
    foreach (var cur in values)
    {
        yield return cur;
    }
}

Error:

The body of 'TestBed.EnumerableExtensions.AddRange(TCol, System.Collections.Generic.IEnumerable)' cannot be an iterator block because 'TCol' is not an iterator interface type

Does this mean that generic constraints are not considered by the compiler when determining if a method qualifies for yield return use?

I use this extension method in a class which defines the collection using a generic parameter. Something like (in addition to a few type cast operators):

public class TestEnum<TCol, TItem>
    where TCol : class, ICollection<TItem>, new()
{
    TCol _values = default(TCol);

    public TestEnum(IEnumerable<TItem> values)
    {
        _values = (TCol)(new TCol()).AddRange(values);
    }
    public TestEnum(params TItem[] values) : this(values.AsEnumerable()) { }

    ...
}

And in turn, used like (remember I have type cast operators defined):

TestEnum<List<string>, string> col = new List<string>() { "Hello", "World" };
string someString = col;
Console.WriteLine(someString);

Originally, my extension method looked like:

public static IEnumerable<TItem> AddRange<TItem>(this IEnumerable<TItem> e, IEnumerable<TItem> values)
{
    ...
}

Which compiles but results in:

Unhandled Exception: System.InvalidCastException: Unable to cast object of type '<AddRange>d__61[System.String]' to type 'System.Collections.Generic.List1[System.String]'.

Is there an alternative way to do this?


As requested, here's a small sample:

class Program
{
    public static void Main()
    {
        TestEnum<List<string>, string> col = new List<string>() { "Hello", "World" };
        string someString = col;

        Console.WriteLine(someString);
    }
}

public class TestEnum<TCol, TItem>
    where TCol : class, ICollection<TItem>, new()
{
    TCol _values = default(TCol);

    public TestEnum(IEnumerable<TItem> values)
    {
        _values = (TCol)(new TCol()).AddRange(values);
    }
    public TestEnum(params TItem[] values) : this(values.AsEnumerable()) { }
    public static implicit operator TItem(TestEnum<TCol, TItem> item)
    {
        return item._values.FirstOrDefault();
    }
    public static implicit operator TestEnum<TCol, TItem>(TCol values)
    {
        return new TestEnum<TCol, TItem>(values);
    }
}
public static class EnumerableExtensions
{
    public static IEnumerable<TItem> AddRange<TItem>(this IEnumerable<TItem> e, IEnumerable<TItem> values)
    {
        foreach (var cur in e)
        {
            yield return cur;
        }
        foreach (var cur in values)
        {
            yield return cur;
        }
    }
}

To repro the compile-time exception:

class Program
{
    public static void Main()
    {
        TestEnum<List<string>, string> col = new List<string>() { "Hello", "World" };
        string someString = col;

        Console.WriteLine(someString);
    }
}

public class TestEnum<TCol, TItem>
    where TCol : class, ICollection<TItem>, new()
{
    TCol _values = default(TCol);

    public TestEnum(IEnumerable<TItem> values)
    {
        _values = (TCol)(new TCol()).AddRange(values);
    }
    public TestEnum(params TItem[] values) : this(values.AsEnumerable()) { }
    public static implicit operator TItem(TestEnum<TCol, TItem> item)
    {
        return item._values.FirstOrDefault();
    }
    public static implicit operator TestEnum<TCol, TItem>(TCol values)
    {
        return new TestEnum<TCol, TItem>(values);
    }
}
public static class EnumerableExtensions
{
    public static TCol AddRange<TCol, TItem>(this TCol e, IEnumerable<TItem> values)
        where TCol : IEnumerable<TItem>
    {
        foreach (var cur in e)
        {
            yield return cur;
        }
        foreach (var cur in values)
        {
            yield return cur;
        }
    }
}
M.Babcock
  • 18,753
  • 6
  • 54
  • 84
  • 3
    It's hard to follow what's going on with all these snippets. A short but *complete* example would be easier to answer. – Jon Skeet Mar 24 '12 at 22:56
  • Updated with example. (Updated again to make it shorter). – M.Babcock Mar 24 '12 at 22:58
  • 1
    You mentioned a *compile-time* error, but your example compiles. It fails with an *exception* at execution time - is that actually what you meant, or does your code just not show what you were originally seeing? – Jon Skeet Mar 24 '12 at 23:01
  • Could you explain what are you trying to do? There is probably much easier way to do it than what you are trying. – svick Mar 24 '12 at 23:01
  • Off topic, but you seem to have reimplemented LINQ's `Union` extension method. – Justin Morgan - On strike Mar 24 '12 at 23:03
  • @JonSkeet - If you replace the `AddRange` extension method with the first snippet it produces the compile exception. – M.Babcock Mar 24 '12 at 23:03
  • @svick - Generally, what I am trying to accomplish is to have a single type that can potentially represent one value or a collection of values simulateously... this is just one small part of that. – M.Babcock Mar 24 '12 at 23:04
  • Why would you expect your example to work? Why do you think you can successfully cast the result of `AddRange()` to `List`? – svick Mar 24 '12 at 23:05
  • @svick - Because both enumerables are `IEnumerable`... why shouldn't it work? – M.Babcock Mar 24 '12 at 23:06
  • @M.Babcock, that's a really weird things to want: one type that can be two different things at the same time. But why does it need to be generic in `TCol`? – svick Mar 24 '12 at 23:08
  • @M.Babcock, you have a method that has a return type of `Animal`, but it actually returns a `Cat`. Why would you expect casting that to `Dog` would work? – svick Mar 24 '12 at 23:09
  • @svick - It's part of a generalized configuration framework. The number of values represented by a single configuration element (think a list within the configuration) could vary and I'd like to have a single type to represent both situations. – M.Babcock Mar 24 '12 at 23:10
  • @JonSkeet - Updated to include the snippet that reproduces the compile exception. – M.Babcock Mar 24 '12 at 23:12

3 Answers3

6

Let's simplify:

static T M() where T : IEnumerable<int>
{
    yield return 1;
}

Why is this illegal?

For the same reason that this is illegal:

static List<int> M() 
{
    yield return 1;
}

The compiler only knows how to turn M into a method that returns an IEnumerable<something>. It doesn't know how to turn M into a method that returns anything else.

Your generic type parameter T could be List<int> or any of infinitely many other types that implement IEnumerable<T>. The C# compiler doesn't know how to rewrite the method into one that returns a type it knows nothing about.

Now, regarding your method: what is the function of TCol in the first place? Why not just say:

public static IEnumerable<TItem> AddRange<TItem>(
  this IEnumerable<TItem> s1, IEnumerable<TItem> s2)
{
  foreach(TItem item in s1) yield return item;
  foreach(TItem item in s2) yield return item;
}

Incidentally, this method already exists; it is called "Concat".

Eric Lippert
  • 647,829
  • 179
  • 1,238
  • 2,067
  • Agreed. I originally tried `IEnumerable.Concat` but it resulted in basically the same errors at compile time, so I rolled my own version based on [Jared Par's version](http://stackoverflow.com/a/1210311/635634) for adding to `IEnumerable` instances (I already had it in my project so I just cloned the function). This led to me trying to use generics to abstract the types in hopes that it would work, however no luck (resulting in this question). I hoped for a solution that would work against `IEnumerable` but will settle with one for `ICollection`. – M.Babcock Mar 25 '12 at 00:24
5

I'm not sure what are you trying to accomplish, your method certainly doesn't look like AddRange(), because it doesn't add anything to any collection.

But if you write an iterator block, it will return an IEnumerable<T> (or IEnumerator<T>). The actual run-time type it returns is compiler generated and there is no way to force it to return some specific collection, like List<T>.

From your example, AddRange() simply doesn't return List<T>, which is why you can't cast the result to that type. And there is no way to make iterator block return List<T>.

If you want to create a method that adds something to a collection, it probably means you need to call Add(), not return some other collection from the method:

public static void AddRange<T>(
    this ICollection<T> collection, IEnumerable<T> items)
{
    foreach (var item in items)
        collection.Add(item);
}
svick
  • 236,525
  • 50
  • 385
  • 514
  • So then what is wrong with my second attempt (the first snippet)? – M.Babcock Mar 24 '12 at 23:07
  • Like I said, iterator block can return only `IEnumerable`, you can't make it return some random collection type, which is what you're trying to do. – svick Mar 24 '12 at 23:15
  • That makes sense (and was hinted at in my question). What's confusing is that `IEnumerable enumStr = new List() { "Hello" }.Concat(new List() { "World" });` works though it should still return the same custom iterator type, so how why would the code in my second snippet cause an exception? – M.Babcock Mar 24 '12 at 23:19
  • *Why* is that confusing to you? `Concat()` doesn't return a `List`, but you're also not casting to it, so the code works fine. But if you cast the result of an iterator block to `List`, it will throw, because the result is not a `List`. – svick Mar 24 '12 at 23:22
  • Isn't my implementation of `AddRange()` the solution for you? – svick Mar 24 '12 at 23:28
  • Strike that... your code snippet works (so you provided the solution as well). Thanks for tolerating my lapse in judgement. I did change it a little to return the collection, but otherwise perfect. Thanks for your help. – M.Babcock Mar 24 '12 at 23:28
1

In response to your comment to Eric Lippert's answer:

I hoped for a solution that would work against IEnumerable but will settle with one for ICollection.

public static void AddRange<TCol, TItem>(this TCol collection, IEnumerable<TItem> range)
    where TCol : ICollection<TItem>
{
    var list = collection as List<TItem>;
    if (list != null)
    {
        list.AddRange(range);
        return;
    }

    foreach (var item in range)
        collection.Add(item);
}

I defined the method as void to mimic the semantics of List<T>.AddRange().

phoog
  • 42,068
  • 6
  • 79
  • 117