Once again, in this series of posts I look at the parts of the .NET Framework that may seem trivial, but can help improve your code by making it easier to write and maintain.
Most of my time this week has been spent finishing the current iteration at work and updating a presentation for the Springfield DNUG, so today’s post will be a bit on the lighter side, but I wanted to continue my post series so I thought it would be a good time to quickly mention the ElementAt() and Last() LINQ extension methods.
We’ve already looked at the Single() and First() family of extension methods (here) which show how to get the first (or only) item in the list (or that matches a criteria). Now we’ll look at these two families of methods that will give you other specific items in an enumerable sequence.
Last() – Retrieves the last occurence
As you might expect, Last() is the antithesis of First(). Where First() finds the first item (or match) in a sequence, Last() finds the last item (or match) in a sequence.
Just like First(), there are actually four flavors of the Last() method, each with their own twist. Each of these are extensions methods that operate on any IEnumerable<TSource>:
- Last()
- Returns the very last item in the sequence, if no item exists throws InvalidOperationException.
- Last(Predicate<TSource>)
- Returns the very last item in the sequence that matches the predicate, if no item exists that matches throws InvalidOperationException.
- LastOrDefault()
- Returns the very last item in the sequence, if the sequence is empty returns default(TSource).
- LastOrDefault(Predicate<TSource>)
- Returns the very last item in the sequence that matches the predicate, if no item exists that matches returns default(TSource).
Because Last() is only interested in the last item (or match) in a sequence of enumerable, this creates some interesting performance considerations. While First() was able to immediately stop on the first match (or item), the form of Last() that matches on a predicate cannot do this, though the non-predicate versions can take advantage of the length of the collection if it’s an IList instance.
In particular, the Last() and LastOrDefault() versions that take no additional arguments can immediately check the last element in the enumerable using the Count property if the underlying collection is an IList<T>. This means, in essence that these two calls will have a performance of constant time (O(1)) for IList<T> implementations, but for all other implementations it will have to iterate through the whole collection to find the end are those will be linear time (O(n)).
Now, on the forms that take a predicate, the time will always be linear (O(n)) because it must scan the entire collection to find the last match.
So let’s take a quick look at how this method behaves. Once again assume the same class we used in our First/Single discussion:
1 : public sealed class Employee 2 : {
3 : public long Id {
get;
set;
}
4 : public string Name {
get;
set;
}
5 : public double Salary {
get;
set;
}
6:
}
And that we have the following lists defined (purely for illustrative purposes):
1 : // empty
2 : var noEmployeeList = new List<Employee>();
3 : 4 : // many items
5 : var employees = new List<Employee> 6 : {
7 : new Employee{Id = 1, Name = "Jim Smith", Salary = 12345.50},
8 : new Employee{Id = 7, Name = "Jane Doe", Salary = 31234.50},
9 : new Employee{Id = 9, Name = "John Doe", Salary = 13923.99},
10 : new Employee{Id = 13, Name = "Jim Smith", Salary = 30123.49},
11 : // ... etc ...
12 :
};
If we use the Last() method on these two lists, we can see how they’ll behave. In both cases, the methods return the last item (or match) in the enumerable.
1 : // gets the last, Jim Smith with Id == 13
2 : var last = employees.Last();
3 : 4 : // gets the last employee whose name ends with Doe (John Doe, Id == 9)
5 : var lastDoe = employees.Last(e = > e.Name.EndsWith("Doe"));
Once again, like First(), the Last() method will throw if there are no items (or no matches in the case of predicates) in the enumerable:
1 : // throws at runtime because empty enumerable.
2 : var empty = noEmployeeList.Last();
3 : 4
: // this line will throw at runtime because there is no item that matches
5 : // even though the enumerable itself is not empty
6 : var noMatch = employees.Last(e = > e.Id == 20);
And then the LastOrDefault() methods will return a default(TSource) if the list is empty or no matches are found:
1 : // returns default(Employee) -- null -- because list is empty
2 : var empty = noEmployeeList.LastOrDefault();
3 : 4 : // returns default(Employee) -- null -- because no item matches
// predicate.
5 : var noMatch = employees.Last(e = > e.Id == 20);
ElementAt() – Retrieves item at specified index
The ElementAt() family of methods give you a quick way to go to a specific item in an enumerable sequence at a specified index. The index is zero-offset and if the index specified is out of the bounds of the sequence, you will get either an ArgumentOutOfRangeException thrown or a default value for TSource depending on the version you used:
- ElementAt(index)
- Returns the item at the specified index. The index must be in the range of 0 – Count – 1. If the index is not valid, will throw ArgumentOutOfRangeException.
- ElementAtOrDefault(index)
- Returns the item at the specified index if the index is valid. If the index is outside of the bounds, will return default(TSource).
This is a handy family of methods for getting to a particular position in a sequence, and the ElementAtOrDefault() is especially handy if you are not sure whether the sequence contains enough items for the index you are querying.
Now, since this extension method can called for any IEnumerable<TSource> you may think that this means the efficiency is for the least common denominator and thus is a linear (O(n))efficiency, but rest assured Microsoft already thought of that and the extension method first checks to see if the collection implements IList<TSource>. If so it will use the constant time Count property to determine the range and then immediately jump in constant time (O(1)) to the correct item. So in the worst case, this is a linear operation, but will be constant time for all implementations of IList<TSource>.
So let’s look:
1 : 2 : // returns the second employee (index == 1) which is Jane Doe, Id == 7
3 : var janeDoe = employees.ElementAt(1);
4 : 5 : // since there's not 30 employees, this one will throw exception
6 : var willThrow = employees.ElementAt(30);
7 : 8 : // returns null because there aren't 30 employees, but we used the
// OrDefault flavor.
9 : var noMatch = employees.ElementAtOrDefault(30);
So you might say be wondering when you’d use this, since obviously if you had a List<T> you could just use the indexer directly. That’s a good point, but keep in mind that sometimes we don’t know what the underlying container is, in which case the ElementAt() gives you that same functionality, but applicable to all containers (and it will still be constant time for lists). In addition, the ElementAtOrDefault() is particularly useful because it lets you return a default instead of throwing if the item does not exist.
Summary
So just like the First() extension method, the Last() and ElementAt() family of methods can be used to get the last or any arbitrary position in an enumerable sequence. They both have their …OrDefault() flavors to remove the exception if the list is empty, has no matches, or the index is outside the bounds which can make it easy to do safe searching.
Print | posted on Thursday, April 28, 2011 7:35 PM | Filed Under [ My Blog C# Software .NET Little Wonders