Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Does calling .Last() on an IList iterate the entire list? [duplicate]

Tags:

c#

Does the .Last() extension method take into account if it's called on an IList? I'm just wondering if there's a significant performance difference between these:

IList<int> numbers = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 };

int lastNumber1 = numbers.Last();
int lastNumber2 = numbers[numbers.Count-1];

Intuition tells me that the first alternative is O(n) but the second is O(1). Is .Last() "smart" enough to try casting it to an IList?

like image 823
Scott Whitlock Avatar asked Jan 28 '11 21:01

Scott Whitlock


4 Answers

Probably not, as it can do list[list.count-1]

Verified by reflector:

public static TSource Last<TSource>(this IEnumerable<TSource> source)
{
    if (source == null)
    {
        throw Error.ArgumentNull("source");
    }
    IList<TSource> list = source as IList<TSource>;
    if (list != null)
    {
        int count = list.Count;
        if (count > 0)
        {
            return list[count - 1];
        }
    }
    ...
}
like image 123
Ohad Schneider Avatar answered Oct 19 '22 23:10

Ohad Schneider


This is an undocumented optimization, but the predicate-less overload of Enumerable.Last does indeed skip straight to the end.

Note that the overload with a predicate doesn't just go from the end, working backwards as you might expect - it goes forwards from the start. I believe this is to avoid inconsistency when the predicate may throw an exception (or cause other side effects).

See my blog post about implementing First/Last/Single etc for more information - and an inconsistency which is present between the overloads of Single/SingleOrDefault.

like image 32
Jon Skeet Avatar answered Oct 20 '22 00:10

Jon Skeet


Reflector:

public static TSource Last<TSource>(this IEnumerable<TSource> source)
{
    ...
    if (list != null)
    {
        int count = list.Count;
        if (count > 0)
        {
            return list[count - 1];
        }
    }
    else
    {
        using (IEnumerator<TSource> enumerator = source.GetEnumerator())
        {
            ...
        }
    }
    throw Error.NoElements();
}
like image 37
wj32 Avatar answered Oct 19 '22 23:10

wj32


Answer: Yes.

Here's a neat way to find out:

class MyList<T> : IList<T> { 
    private readonly List<T> list = new List<T>();
    public T this[int index] {
        get {
            Console.WriteLine("Inside indexer!");
            return list[index];
        }
        set {
            list[index] = value;
        }
    }

    public void Add(T item) {
        this.list.Add(item);
    }

    public int Count {
        get {
            Console.WriteLine("Inside Count!");
            return this.list.Count;
        }
    }

    // all other IList<T> interface members throw NotImplementedException
}

Then:

MyList<int> list = new MyList<int>();
list.Add(1);
list.Add(2);
Console.WriteLine(list.Last());

Output:

Inside Count!
Inside indexer!
2

If you try this:

Console.WriteLine(list.Last(n => n % 2 == 0));

then you get an exception in GetEnumerator showing that it is trying to walk the list. If we implement GetEnumerator via

public IEnumerator<T> GetEnumerator() {
    Console.WriteLine("Inside GetEnumerator");
    return this.list.GetEnumerator();
}

and try again we see

Inside GetEnumerator!
2

on the console showing that the indexer was never used.

like image 25
jason Avatar answered Oct 19 '22 23:10

jason