Back to Blog
C#

C# LINQ Last: Syntax, Edge Cases, and Performance

c# linq last: Learn how C# LINQ Last and LastOrDefault work, including predicate overloads, empty sequence behavior, performance tradeoffs, and alternatives like index...

C#LINQIEnumerableLastOrDefaultPerformance
Diagram showing the last element selection from a sequence with LINQ Last, highlighting empty sequence and performance considerations.

c# linq last requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.

When you need the final element of a sequence in C#, the LINQ Last method is the direct answer. But Last has behavior that surprises developers who assume it works like an array indexer: it throws on empty sequences, it can be O(n) for arbitrary IEnumerable sources, and its predicate overload scans the entire sequence. This article covers how Last and LastOrDefault work, when to use each, and where they can cause performance problems.

What Last and LastOrDefault Do

The Last method returns the final element of a sequence. The simplest call requires no arguments and works on any IEnumerable<T>:

var numbers = new[] { 1, 2, 3, 4, 5 }; int last = numbers.Last(); // 5

Last also has an overload that accepts a predicate. That overload returns the last element that satisfies the condition:

var numbers = new[] { 1, 2, 3, 4, 5 }; int lastEven = numbers.Last(n => n % 2 == 0); // 4

Both overloads execute immediately. They do not return a lazy iterator. The sequence is enumerated as needed, but the result is a single value.

Using Last with a Predicate

When you pass a predicate, LINQ iterates the sequence from the beginning and keeps track of the most recent match. When the iteration completes, it returns that match. This means the entire sequence is always enumerated, even if the match appears early. For example:

var data = GetData(); // IEnumerable<Record> var lastActive = data.Last(r => r.IsActive);

If no element satisfies the predicate, Last throws InvalidOperationException. This is the same exception thrown when the sequence is empty. The predicate version does not stop early; it must examine every element to confirm there is no later match.

Empty Sequences and Default Values

Last throws InvalidOperationException on an empty sequence. For many scenarios, that is too aggressive. LastOrDefault returns the default value for the type instead:

var empty = new int[0]; int result = empty.LastOrDefault(); // 0

For reference types, the default is null. For nullable value types, it is null as well. Starting with .NET 6, LastOrDefault has an overload that lets you specify the default value explicitly:

var empty = new int[0]; int result = empty.LastOrDefault(-1); // -1

This overload is useful when you want a sentinel value that is meaningful to your domain rather than the type's default. Note that this overload is not available in earlier .NET versions, so check the target framework.

Performance Characteristics

The performance of Last depends on the runtime type of the source. When the source implements IList<T> (for example, arrays, List<T>, or IReadOnlyList<T>), the parameterless overload uses the indexer to fetch the last element directly. That is an O(1) operation. The predicate overload, however, still iterates the entire list because it must find the last match.

For arbitrary IEnumerable<T> sources that do not implement IList<T>, even the parameterless Last must iterate the entire sequence to reach the final element. This is O(n). If you call Last repeatedly on the same sequence, each call re-enumerates. Consider materializing the sequence into a list or array if you need the last element frequently and the source is not already indexable.

Another edge case: if the sequence is infinite, Last will never terminate. The same applies to LastOrDefault. There is no way to know the last element without reaching the end.

Alternatives to Last

For arrays and lists, C# 8 introduced the index-from-end operator ^1, which gives you the last element without calling LINQ:

var list = new List<int> { 1, 2, 3 }; int last = list[^1]; // 3

This is O(1) and does not allocate an enumerator. However, it only works on types that support indexing, such as arrays, List<T>, and Span<T>. It does not work on arbitrary IEnumerable<T>.

The following table summarizes the key differences:

ApproachEmpty sequencePerformance on IListPerformance on IEnumerable
Last()ThrowsO(1)O(n)
LastOrDefault()Returns defaultO(1)O(n)
list[^1]ThrowsO(1)Not applicable

If you need the last element of a sequence that is expensive to enumerate, consider whether you can change the data structure. A LinkedList<T> gives you direct access to the last node, but it has other tradeoffs. A circular buffer or a custom collection might be more appropriate depending on the access pattern.

Common Mistakes and Edge Cases

One common mistake is assuming Last is always O(1). On a IEnumerable<T> that is not a list, it is O(n). Another mistake is using Last on a sequence that might be empty without catching the exception. Prefer LastOrDefault when the absence of a value is a valid outcome.

Be careful with lazy sequences. If the source is a generator that performs side effects, calling Last will execute the generator until it is exhausted. This can have surprising consequences if the generator is not repeatable. For example:

IEnumerable<int> Generate() { Console.WriteLine("Generating"); yield return 1; yield return 2; } var last = Generate().Last(); // Prints "Generating" once

If you call Last twice, the generator runs twice. If the generator has external state, the results may differ.

Finally, remember that Last and LastOrDefault are not the same as Single or SingleOrDefault. Single throws if there is more than one match. Last only cares about the final match. Choose the method that matches your domain rule.

c# linq last: Practical Usage and Code Examples | RYUSLOG DEV