C# の LINQ で末尾を取る Last と LastOrDefault

LastLastOrDefault はシーケンスの末尾要素を取得するメソッドだ。First / FirstOrDefault の逆で、最後の要素を返す。

Last:末尾要素を取得

シーケンスの最後の要素を返す。

int[] numbers = { 5, 3, 8, 1, 9 };

int last = numbers.Last();
Console.WriteLine(last);  // 9

条件を指定すれば、その条件を満たす最後の要素を取得できる。

int[] numbers = { 5, 3, 8, 1, 9, 4 };

int lastEven = numbers.Last(n => n % 2 == 0);
Console.WriteLine(lastEven);  // 4

LastOrDefault:要素がなければデフォルト値

要素が見つからない場合にデフォルト値を返す。

int[] numbers = { 1, 3, 5, 7, 9 };

int lastEven = numbers.LastOrDefault(n => n % 2 == 0);
Console.WriteLine(lastEven);  // 0(int のデフォルト値)

空のシーケンスでの違い

First 系と同様の違いがある。

Last

要素がなければ InvalidOperationException をスロー

LastOrDefault

要素がなければデフォルト値を返す

var empty = new List<int>();

// int last = empty.Last();  // 例外!
int lastOrDefault = empty.LastOrDefault();  // 0

デフォルト値の指定(.NET 6 以降)

.NET 6 以降では、戻り値のデフォルト値を明示的に指定できる。

int[] numbers = { 1, 3, 5 };

int result = numbers.LastOrDefault(n => n % 2 == 0, -1);
Console.WriteLine(result);  // -1

実用的な例

最新のログ取得

var logs = new List<LogEntry>
{
    new LogEntry { Time = DateTime.Parse("2024-01-01"), Message = "Start" },
    new LogEntry { Time = DateTime.Parse("2024-01-02"), Message = "Process" },
    new LogEntry { Time = DateTime.Parse("2024-01-03"), Message = "End" }
};

var latest = logs.Last();
Console.WriteLine(latest.Message);  // End

最後のエラー取得

var logs = new List<LogEntry>
{
    new LogEntry { Level = "Info", Message = "Started" },
    new LogEntry { Level = "Error", Message = "Failed to connect" },
    new LogEntry { Level = "Info", Message = "Retrying" },
    new LogEntry { Level = "Error", Message = "Timeout" }
};

var lastError = logs.LastOrDefault(l => l.Level == "Error");
if (lastError != null)
{
    Console.WriteLine(lastError.Message);  // Timeout
}

パフォーマンスの考慮

Last のパフォーマンスはコレクションの種類によって異なる。

配列 / List

インデックスアクセス可能なので O(1)。直接末尾にアクセスできる。

IEnumerable 一般

全要素を走査する必要があり O(n)。シーケンス全体を列挙して最後を取得する。

// List は高速(インデックスアクセス)
var list = new List<int> { 1, 2, 3, 4, 5 };
int last1 = list.Last();  // O(1)

// Where の結果は遅い(全要素走査)
var filtered = list.Where(n => n > 0);
int last2 = filtered.Last();  // O(n)

Where などを通すと IEnumerable<T> になり、インデックスアクセスができなくなる。パフォーマンスが重要な場面では注意が必要だ。

TakeLast との違い

末尾から複数の要素を取得するなら TakeLast を使う。

int[] numbers = { 1, 2, 3, 4, 5 };

// Last: 最後の1要素(スカラー値)
int last = numbers.Last();  // 5

// TakeLast: 最後のN要素(シーケンス)
var lastThree = numbers.TakeLast(3);  // [3, 4, 5]

Last は単一の値、TakeLast はシーケンスを返す。

Reverse().First() との関係

理論的には Last()Reverse().First() と同じ結果を返す。

int[] numbers = { 1, 2, 3, 4, 5 };

int last1 = numbers.Last();
int last2 = numbers.Reverse().First();

Console.WriteLine(last1 == last2);  // True

ただし、配列やリストに対しては Last() の方が効率的だ。Reverse() は新しいシーケンスを生成するオーバーヘッドがある。

First / Last の組み合わせ

先頭と末尾を同時に取得するパターン。

var transactions = new List&lt;Transaction&gt;
{
    new Transaction { Date = DateTime.Parse("2024-01-01"), Amount = 100 },
    new Transaction { Date = DateTime.Parse("2024-01-15"), Amount = 200 },
    new Transaction { Date = DateTime.Parse("2024-01-31"), Amount = 150 }
};

var firstTx = transactions.First();
var lastTx = transactions.Last();

Console.WriteLine($"最初: {firstTx.Date:MM/dd} {firstTx.Amount}円");
Console.WriteLine($"最後: {lastTx.Date:MM/dd} {lastTx.Amount}円");

期間の始まりと終わりを取得するのに便利だ。ただし、時系列順に並んでいることが前提になる。