Search code examples
c#igrouping

IGrouping ElementAt vs. square bracket operator


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?


Solution

  • 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.