1

IGrouping supports the ElementAt method to index into the grouping's collection. So why doesn't the square bracket operator work?

I can do something like

 list.GroupBy(expr).Select(group => group.ElementAt(0)....) 

but not

 list.GroupBy(expr).Select(group => group[0]....) 

I'm guessing this is because the IGrouping interface doesn't overload the square bracket operator. Is there a good reason why IGrouping didn't overload the square bracket operator to do the same thing as ElementAt?

user990423
  • 1,397
  • 2
  • 12
  • 32
wrschneider
  • 17,913
  • 16
  • 96
  • 176

4 Answers4

2

That is because GroupBy returns an IEnumerable. IEnumerables don't have an indexing accessor

Derek Van Cuyk
  • 953
  • 7
  • 23
Luc
  • 1,491
  • 1
  • 12
  • 21
2

ElementAt<T> is a standard extension method on IEnumerable<T>, it's not a method on IGrouping, but since IGrouping derives from IEnumerable<T>, it works fine. There is no [] extension method because it's not supported by C# (it would be an indexed property, not a method)

Simon Mourier
  • 132,049
  • 21
  • 248
  • 298
1

That's a bit back to front, all enumerables are supported by (rather than supports, as it's an extension method provided from the outside) ElementAt() but only some are of a type that also support [], such as List<T> or anything that implements IList<T>.

Grouping certainly could implement [] easily enough, but then it would have to always do so, as the API would be a promise it would have to keep on keeping, or it would break code written to the old way if it did break it.

ElementAt() takes a test-and-use approach in that if something supports IList<T> it will use [] but otherwise it counts the appropriate number along. Since you can count-along with any sequence, it can therefore support any enumerable.

It so happens that Grouping does support IList<T> but as an explicit interface, so the following works:

//Bad code for demonstration purpose, do not use:
((IList<int>)Enumerable.Range(0, 50).GroupBy(i => i / 5).First())[3]

But because it's explicit it doesn't have to keep supporting it if there was ever an advantage found in another approach.

The test-and-use approach of ElementAt:

public static TSource ElementAt<TSource>(this IEnumerable<TSource> source, int index)
{
    if (source == null) throw Error.ArgumentNull("source");
    IList<TSource> list = source as IList<TSource>;
    if (list != null) return list[index];
    if (index < 0) throw Error.ArgumentOutOfRange("index");
    using (IEnumerator<TSource> e = source.GetEnumerator())
    {
        while (true)
        {
            if (!e.MoveNext()) throw Error.ArgumentOutOfRange("index");
            if (index == 0) return e.Current;
            index--;
        }
    }
}

Therefore gets the optimal O(1) behaviour out of it, rather than the O(n) behaviour otherwise, but without restricting Grouping to making a promise the designers might later regret making.

Jon Hanna
  • 110,372
  • 10
  • 146
  • 251
  • This makes sense now. To summarize: `ElementAt` is an extension method of `IEnumerable`, `IGrouping` extends `IEnumerable` and thus inherits `ElementAt`, but the indexed property (Item) is unique to `IList` and not available in `IGrouping`. The concrete class from `GroupBy` *does* implement `IList` but that is not guaranteed to be the case. – wrschneider Oct 27 '15 at 18:13
  • Pretty much. *Inherits* is poor wording, since that suggests it's inherited from a base, whereas extension methods are actually static methods that can be called with a syntax that makes calling them look like calling instance methods. Apart from that niggle, yes. – Jon Hanna Oct 28 '15 at 15:51
0

ElementAt is an extension method (btw highly inefficient) defined for IEnumerable<T> to provide a pseudo-indexed access for the sequences that do not natively support it. Since IGrouping<TKey, TElement> returned from a GroupBy method inherits IEnumerable<TElement>, you can use ElementAt method. But of course IEnumerable<T> does not have an indexer defined, so thta's why you cannot use [].

Update: Just to clarify what I meant by "highly inefficient" - the implementation is the best that could be provided, but the method itself in general for the sequences that do not natively support indexer. For example

var source = Enumerable.Range(0, 1000000);
for (int i = 0, count = source.Count(); i < count; i++)
{
    var element = source.ElementAt(i);
}
Ivan Stoev
  • 195,425
  • 15
  • 312
  • 343
  • There doesn't appear to be anything inherently inefficient about the implementation of ElementAt ([reference source](http://referencesource.microsoft.com/#System.Core/System/Linq/Enumerable.cs,7db56d44563d8761)) - in fact if the `IEnumerable` implements `IList` then the lists Indexer is used. – Rob Oct 27 '15 at 17:46
  • @Rob I meant that for sequences that do not support indexer, it's a O(N^2) time complexity to access each element by index. – Ivan Stoev Oct 27 '15 at 17:55