C# の LINQ で最大・最小を取る Max と Min

MaxMin はシーケンスの最大値・最小値を取得するメソッドだ。数値だけでなく、比較可能なあらゆる型に使える。

基本的な使い方

数値の最大値と最小値を取得する。

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

int max = numbers.Max();
int min = numbers.Min();

Console.WriteLine($"Max: {max}");  // Max: 9
Console.WriteLine($"Min: {min}");  // Min: 1

シンプルで直感的だ。

オブジェクトのプロパティで取得

セレクタを指定すれば、特定のプロパティの最大・最小を取得できる。

var products = new List<Product>
{
    new Product { Name = "Apple", Price = 100 },
    new Product { Name = "Banana", Price = 80 },
    new Product { Name = "Cherry", Price = 200 }
};

int maxPrice = products.Max(p => p.Price);
int minPrice = products.Min(p => p.Price);

Console.WriteLine($"最高価格: {maxPrice}");  // 最高価格: 200
Console.WriteLine($"最安価格: {minPrice}");  // 最安価格: 80

価格の最大・最小が得られる。

最大・最小の要素を取得する

値ではなく、その値を持つ要素自体が欲しい場合がある。単純に Max を使うと値しか得られない。

var products = new List<Product>
{
    new Product { Name = "Apple", Price = 100 },
    new Product { Name = "Banana", Price = 80 },
    new Product { Name = "Cherry", Price = 200 }
};

// これは価格の最大値(200)だけ
int maxPrice = products.Max(p => p.Price);

// 最高価格の商品を取得するには OrderBy を使う
var mostExpensive = products.OrderByDescending(p => p.Price).First();
Console.WriteLine(mostExpensive.Name);  // Cherry

.NET 6 以降では MaxBy / MinBy が使える。

// .NET 6 以降
var mostExpensive = products.MaxBy(p => p.Price);
var cheapest = products.MinBy(p => p.Price);

Console.WriteLine(mostExpensive.Name);  // Cherry
Console.WriteLine(cheapest.Name);       // Banana

空のシーケンスに注意

空のシーケンスに対して Max / Min を呼ぶと例外が発生する。

var empty = new List<int>();

// int max = empty.Max();  // InvalidOperationException

対処法はいくつかある。

var list = new List<int>();

// 方法1:事前チェック
int? max1 = list.Any() ? list.Max() : (int?)null;

// 方法2:DefaultIfEmpty
int max2 = list.DefaultIfEmpty(0).Max();  // デフォルト値を指定

// 方法3:Nullable 型を使う(.NET 6以降)
int? max3 = list.Select(x => (int?)x).Max();

DefaultIfEmpty を使う方法が最もシンプルだ。

文字列の最大・最小

文字列でも使える。辞書順(アルファベット順)で比較される。

string[] names = { "Charlie", "Alice", "Bob" };

string max = names.Max();  // Charlie
string min = names.Min();  // Alice

大文字は小文字より前に来ることに注意。カスタムの比較が必要なら MaxByMinBy を使う。

日付の最大・最小

DateTime にも適用できる。最新の日付や最古の日付を取得するのに便利だ。

var events = new List<Event>
{
    new Event { Name = "Meeting", Date = new DateTime(2024, 1, 15) },
    new Event { Name = "Conference", Date = new DateTime(2024, 3, 20) },
    new Event { Name = "Workshop", Date = new DateTime(2024, 2, 10) }
};

DateTime latest = events.Max(e => e.Date);
DateTime earliest = events.Min(e => e.Date);

Console.WriteLine($"最新: {latest:yyyy/MM/dd}");    // 最新: 2024/03/20
Console.WriteLine($"最古: {earliest:yyyy/MM/dd}");  // 最古: 2024/01/15

Nullable 型での動作

int? のような Nullable 型では、null は無視される。

int?[] numbers = { 3, null, 7, null, 2 };

int? max = numbers.Max();  // 7
int? min = numbers.Min();  // 2

すべてが null の場合、結果も null になる。

int?[] allNull = { null, null, null };

int? max = allNull.Max();  // null(例外は発生しない)

GroupBy との組み合わせ

グループごとの最大・最小を求めるパターン。

var scores = new List<Score>
{
    new Score { Subject = "Math", Value = 80 },
    new Score { Subject = "English", Value = 90 },
    new Score { Subject = "Math", Value = 95 },
    new Score { Subject = "English", Value = 85 }
};

var result = scores
    .GroupBy(s => s.Subject)
    .Select(g => new 
    {
        Subject = g.Key,
        Max = g.Max(s => s.Value),
        Min = g.Min(s => s.Value)
    });

foreach (var item in result)
{
    Console.WriteLine($"{item.Subject}: 最高{item.Max}, 最低{item.Min}");
}
// Math: 最高95, 最低80
// English: 最高90, 最低85

科目ごとの最高点・最低点を集計している。SQL の GROUP BYMAX / MIN の組み合わせに相当する処理だ。