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
?
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];
}
}
...
}
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.
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();
}
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With