概要
「Rx.NETやUniRxにあるXXXっていうメソッド(オペレーター)、なんでLINQにはないの?」って思った時におすすめなのがIx.NET。
以下、READMEより
The Interactive Extensions (Ix) is a .NET library which extends LINQ to Objects to provide many of the operators available in Rx but targeted for IEnumerable.
さてそんなIx.NETには以前から、「要素の中で一番大きい評価値をもつ要素を探すこと」ができるMaxByというメソッドが存在しました。このような機能を提供するメソッドは、LINQが登場してから長い間、標準ライブラリのLINQとしてはありませんでした。
ところが.NET 6で、標準ライブラリとしてLINQに「MaxBy」という同名で、Ix.NET版とは微妙に仕様が違うメソッドが追加されました。
それを受けてIx.NETではv6.0.1で、MaxByというメソッドを「MaxByWithTies」という名前に変更しました。同様に、MinByというメソッドを「MinByWithTies」という名前に変更しました。(途中紆余曲折あり)
該当のリリース(v6.0.1)はこちら。
該当の変更PRはこちら。
Ix.NET 5.1.0までのMaxBy
Ix.NET v5.1.0までのMaxByメソッドのシグニチャは以下の通りです。
public static IList<TSource> MaxBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
返り値型がIList<TSource>
なことに注目してください。
Ix.NET v5.1.0までのMaxByメソッドは、「最大と評価された要素」全てを含むリストを返しました。
次のレコードを使って例を示します。
record Player(String Name, int Level);
次のコードでは、MaxByを使ってリストplayersから最大Levelの要素を探します。次のコードを実行すると、maxLevelPlayersは複数の要素を含むことがポイントです。
var players = new List<Player>
{
new("taro", 5),
new("jiro", 5),
new("saburo", 4),
};
// IList<Player>型
var maxLevelPlayers = players.MaxBy(it => it.Level);
// 次のように表示
// Player { Name = taro, Level = 5 }
// Player { Name = jiro, Level = 5 }
Console.WriteLine(string.Join("\n", maxLevelPlayers));
一方で次のコードでは、maxLevelPlayersは単一の要素のみを含みます。
var players = new List<Player>
{
new("taro", 100),
new("jiro", 5),
new("saburo", 4),
};
// IList<Player>型
var maxLevelPlayers = players.MaxBy(it => it.Level);
// 次のように表示
// Player { Name = taro, Level = 5 }
Console.WriteLine(string.Join("\n", maxLevelPlayers));
なお、IEnumerable<TSource>
が空の場合、例外を投げます。
このように、Ix.NET v5.1.0までのMaxByメソッドは、「最大と評価された要素」全てを含むリストを返しました。Ix.NETのMaxByでは、「最大と評価された要素」が複数あった場合、返り値のリストに全て含まれるため、全て活用することができました。
.NET 6で加わったMaxByメソッド
.NET 6から、MaxByメソッドが標準ライブラリに加わりました。
メソッドのシグニチャは次の通りです。
public static TSource? MaxBy<TSource,TKey> (this IEnumerable<TSource> source, Func<TSource,TKey> keySelector);
公式ドキュメントはこちら。
返り値の型がIList<TSource>
ではなくTSource?
なことに注目して下さい。メソッドのシグニチャからわかる通り、要素を一つ返すだけです。仮に「最大と評価された要素」が複数あったとしても、返されるのは一つの要素のみです。なお、返される一つの要素は、「最大と評価された要素」の先頭要素です。(.NET 6の実装では)
次のコードでは、Levelが最大の要素を探しています。最も大きいLevelの要素が2つあることに注目してください。このコードを実行すると、maxLevelPlayerには最初の要素「Player { Name = taro, Level = 5 }」が代入されます。
var players = new List<Player>
{
new("taro", 5),
new("jiro", 5),
new("saburo", 4),
};
// Player型
var maxLevelPlayer = players.MaxBy(it => it.Level);
// 次のように表示
// Player { Name = taro, Level = 5 }
Console.WriteLine(maxLevelPlayer);
このように、.NET 6から加わったMaxByメソッドでは、仮に「最大と評価された要素」が複数あったとしても、返されるのは一つの要素のみのため、返されなかった要素を活用することはできません。
Ix.NET 5.1.0、.NET 6以降でIx.NET版のMaxByなどを除外
MaxByというメソッド(System.Linq名前空間のIEnumerable<TSource>
の拡張メソッド)が、次の二つで衝突してしまいました。
- 元々あったIx.NETのMaxByメソッド(
IList<TSource>
を返す) - .NET 6で標準ライブラリに加わったMaxByメソッド(
TSource?
を返す)
そのため、Ix.NET 5.1.0では
「.NET 6以降では、MaxByなどいくつかのメソッド除外する」
という変更が行われました。
問題提起のIssueはこちら
Ix.NET 5.1.0のリリースはこちら
該当のPRはこちら
Ix.NET 6.0.1、IListを返すMaxByがMaxByWithTiesとして復活
Ix.NET 6.0.1において、IList<T>
を返すMaxByメソッドは名前をMaxByWithTiesと変え、復活しました。
復活の提案のIssueコメントはこちら
該当のPRはこちら
Ix.NET 6.0.1のリリースはこちら
合わせて、Ix.NET 6.0.1では、MaxByやMinByがObsoleteになっています。これは、.NET 6未満でIx.NET版のMaxByを利用した際、次のような警告を出して、新しい「MaxByWithTies」への移行を促すためでしょう。
CS0618: Method 'System.Linq.EnumerableEx.MaxBy(this IEnumerable, Func)' is obsolete: Use MaxByWithTies to maintain same behavior with .NET 6 and later
個人の感想・意見
- ちょいちょい標準ライブラリにLINQのメソッドが追加されている。標準ライブラリのメソッドと、3rd partyライブラリの名前が衝突して、微妙に仕様が違うこと、今後もありそう。
- 個人的には、標準ライブラリの
TSource?
を返すMaxByよりも、Ix.NETのIList<TSource>
を返す方が好み - Ix.NETのMaxByWithTiesという復活&名称変更はナイス対応
- 標準ライブラリのLINQに、MaxByは入っていてほしかったな(わがまま)
- 標準ライブラリのLINQ、MaxByは
IList<TSource>
返してほしかったな(これはプレビュー段階でフィードバックしなかったのが悪い)