C# の LINQ で末尾を取る Last と LastOrDefault
Last と LastOrDefault はシーケンスの末尾要素を取得するメソッドだ。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); // 4LastOrDefault:要素がなければデフォルト値
要素が見つからない場合にデフォルト値を返す。
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<Transaction>
{
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}円");期間の始まりと終わりを取得するのに便利だ。ただし、時系列順に並んでいることが前提になる。