2022年3月26日土曜日

MaxBy & MinBy in .NET 6

どうも、もりもりです。

今回は.NET 6で追加されたLINQのMaxBy / MinByについてメモ。
.NET 6 より前ではどうしてた?代用できるメソッドはあるの?といったところもあります。

まずはMaxBy / MinBy (.NET 6)

こんなListがあったとして

new List<Person>()
{
    new Person(Name: "Ichiro", Age: 47),
    new Person("Jiro", 47),
    new Person("Saburo", 46),
    new Person("Siro", 41),
    new Person("Goro", 35),
    new Person("Rokuro", 35),
};

MaxBy、MinByを呼んでやると

var maxAgePerson = People.MaxBy(_ => _.Age);
Console.WriteLine($"\tMax : {maxAgePerson}");

var minAgePerson = People.MinBy(_ => _.Age);
Console.WriteLine($"\tMin : {minAgePerson}");

最大、最小の年齢に合致した最初の要素を返してくれます。 なんで今までなかったのか不思議ですが、ラクに取得できるようになったので嬉しいですね。

Max : Person { Name = Ichiro, Age = 47 }
Min : Person { Name = Goro, Age = 35 }

MaxとFirstの合わせ技 (.NET 5以前)

LINQでパッと思い付くのはこれでしょうか。Max()で値を取得して、その値を保持している最初の要素をFirst()で取得。

var maxAge = People.Max(_ => _.Age);
var maxAgePerson = People.FirstOrDefault(_ => _.Age == maxAge);
Console.WriteLine($"\tMax : {maxAgePerson}");

var minAge = People.Min(_ => _.Age);
var minAgePerson = People.FirstOrDefault(_ => _.Age == minAge);
Console.WriteLine($"\tMin : {minAgePerson}");

Aggregateで代用 (.NET 5以前)

集計処理をしてくれるAggregateを使用して実現できます。
xに大きい方の値を保持していき、各要素yとの比較を繰り返していきます。 今回はMaxBy/MinByに合わせて最初の要素を得るために「>=」としていますが、 最後の要素が欲しいということであれば「>」にすればOK。

var maxAgePerson = People.Aggregate((x, y) => x.Age >= y.Age ? x : y);
Console.WriteLine($"\tMax : {maxAgePerson}");

var minAgePerson = People.Aggregate((x, y) => x.Age <= y.Age ? x : y);
Console.WriteLine($"\tMin : {minAgePerson}");

拡張メソッド (.NET 5以前)

何度もAggregateを呼ぶことになるなら拡張メソッドを用意しておけばOK。

public static class LinqExtension
{
    public static T MinBy<T, U>(this IEnumerable<T> source, Func<T, U> func) where U : IComparable<U>
        => source.Aggregate((x, y) => func(x).CompareTo(func(y)) <= 0 ? x : y);

    public static T MaxBy<T, U>(this IEnumerable<T> source, Func<T, U> func) where U : IComparable<U>
        => source.Aggregate((x, y) => func(x).CompareTo(func(y)) >= 0 ? x : y);
}

呼び方は.NET 6のMaxBy()と全く同じように書けます。

var maxAgePerson = People.MaxBy(_ => _.Age);
Console.WriteLine($"\tMax : {maxAgePerson}");

var minAgePerson = People.MinBy(_ => _.Age);
Console.WriteLine($"\tMin : {minAgePerson}");

全体ソース

全体ソースを貼っておきます。 .NET 6 はmainメソッドを書く必要なくなりましたね。 .NET 5 以前のソースは言語バージョンによっては動かない書き方もしてますが、 そのままコピペしてvscodeのCode Runnerで実行できます。

.NET 6

Console.WriteLine($"Main() Start");

MaxBySample.Test1();

Console.WriteLine($"Main() End");

public record Person(string Name, int Age);

internal static class MaxBySample
{
    private static List<Person> People => new List<Person>()
    {
        new Person(Name: "Ichiro", Age: 47),
        new Person("Jiro", 47),
        new Person("Saburo", 46),
        new Person("Siro", 41),
        new Person("Goro", 35),
        new Person("Rokuro", 35),
    };

    public static void Test1()
    {
        Console.WriteLine($"*** MaxBy ***");

        var maxAgePerson = People.MaxBy(_ => _.Age);
        Console.WriteLine($"\tMax : {maxAgePerson}");
        
        var minAgePerson = People.MinBy(_ => _.Age);
        Console.WriteLine($"\tMin : {minAgePerson}");
    }
}

.NET 5 以前

using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine($"Main() Start");

        MaxBySample.Test2();
        MaxBySample.Test3();
        MaxBySample.Test4();

        Console.WriteLine($"Main() End");
    }
}

public class Person
{
    public string Name {get; set;}
    public int Age {get; set;}

    public override string ToString()
        => $"Person {{ Name = {this.Name}, Age = {this.Age} }} ";
}

internal static class MaxBySample
{
    private static List<Person> People => new List<Person>()
    {
        new Person() {Name = "Ichiro", Age = 47},
        new Person() {Name = "Jiro", Age = 47},
        new Person() {Name = "Saburo", Age = 46},
        new Person() {Name = "Siro", Age = 41},
        new Person() {Name = "Goro", Age = 35},
        new Person() {Name = "Rokuro", Age = 35},
    };

    public static void Test2()
    {
        Console.WriteLine($"*** Max & First ***");

        var maxAge = People.Max(_ => _.Age);
        var maxAgePerson = People.FirstOrDefault(_ => _.Age == maxAge);
        Console.WriteLine($"\tMax : {maxAgePerson}");
        
        var minAge = People.Min(_ => _.Age);
        var minAgePerson = People.FirstOrDefault(_ => _.Age == minAge);
        Console.WriteLine($"\tMin : {minAgePerson}");
    }

    public static void Test3()
    {
        Console.WriteLine($"*** Aggregate ***");
        
        var maxAgePerson = People.Aggregate((x, y) => x.Age >= y.Age ? x : y);
        Console.WriteLine($"\tMax : {maxAgePerson}");

        var minAgePerson = People.Aggregate((x, y) => x.Age <= y.Age ? x : y);
        Console.WriteLine($"\tMin : {minAgePerson}");
    }

    public static void Test4()
    {
        Console.WriteLine($"*** MaxByExtention ***");
        
        var maxAgePerson = People.MaxBy(_ => _.Age);
        Console.WriteLine($"\tMax : {maxAgePerson}");

        var minAgePerson = People.MinBy(_ => _.Age);
        Console.WriteLine($"\tMin : {minAgePerson}");
    }
}

public static class LinqExtension
{
    public static T MinBy<T, U>(this IEnumerable<T> source, Func<T, U> func) where U : IComparable<U>
        => source.Aggregate((x, y) => func(x).CompareTo(func(y)) <= 0 ? x : y);

    public static T MaxBy<T, U>(this IEnumerable<T> source, Func<T, U> func) where U : IComparable<U>
        => source.Aggregate((x, y) => func(x).CompareTo(func(y)) >= 0 ? x : y);
}
以上、もりもりでした。

0 件のコメント:

コメントを投稿