Ix.NETのMaxBy、.NET 6から加わった標準メソッドと名称が衝突したので、MaxByWithTiesになってた

# 概要

「Rx.NETやUniRxにあるXXXっていうメソッド（オペレーター）、なんでLINQにはないの？」って思った時におすすめなのがIx.NET。

以下、[README](https://github.com/dotnet/reactive#interactive-extensions)より

> 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）はこちら。

https://github.com/dotnet/reactive/releases/tag/ixnet-v6.0.1

該当の変更PRはこちら。

https://github.com/dotnet/reactive/pull/1700

# Ix.NET 5.1.0までのMaxBy

Ix.NET v5.1.0までのMaxByメソッドのシグニチャは以下の通りです。

```csharp
public static IList<TSource> MaxBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
```

返り値型が`IList<TSource>`なことに注目してください。
Ix.NET v5.1.0までのMaxByメソッドは、「最大と評価された要素」全てを含むリストを返しました。

次のレコードを使って例を示します。

```csharp
record Player(String Name, int Level);
```

次のコードでは、MaxByを使ってリストplayersから最大Levelの要素を探します。次のコードを実行すると、maxLevelPlayersは複数の要素を含むことがポイントです。

```csharp
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は単一の要素のみを含みます。

```csharp
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メソッドが標準ライブラリに加わりました。

メソッドのシグニチャは次の通りです。

```csharp
public static TSource? MaxBy<TSource,TKey> (this IEnumerable<TSource> source, Func<TSource,TKey> keySelector);
```

公式ドキュメントはこちら。

https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.maxby?view=net-6.0

返り値の型が`IList<TSource>`ではなく`TSource?`なことに注目して下さい。メソッドのシグニチャからわかる通り、要素を一つ返すだけです。仮に「最大と評価された要素」が複数あったとしても、返されるのは一つの要素のみです。なお、返される一つの要素は、「最大と評価された要素」の先頭要素です。(.NET 6の実装では)


次のコードでは、Levelが最大の要素を探しています。最も大きいLevelの要素が2つあることに注目してください。このコードを実行すると、maxLevelPlayerには最初の要素「Player { Name = taro, Level = 5 }」が代入されます。

```csharp
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はこちら

https://github.com/dotnet/reactive/issues/1647

Ix.NET 5.1.0のリリースはこちら

https://github.com/dotnet/reactive/releases/tag/ixnet-v5.1.0

該当のPRはこちら

https://github.com/dotnet/reactive/pull/1660


# Ix.NET 6.0.1、IList<T>を返すMaxByがMaxByWithTiesとして復活

Ix.NET 6.0.1において、`IList<T>`を返すMaxByメソッドは名前をMaxByWithTiesと変え、復活しました。

復活の提案のIssueコメントはこちら

https://github.com/dotnet/reactive/issues/1647#issuecomment-994434264

該当のPRはこちら

https://github.com/dotnet/reactive/pull/1700

Ix.NET 6.0.1のリリースはこちら

https://github.com/dotnet/reactive/releases/tag/ixnet-v6.0.1

合わせて、Ix.NET 6.0.1では、MaxByやMinByがObsoleteになっています。これは、.NET 6未満でIx.NET版のMaxByを利用した際、次のような警告を出して、新しい「MaxByWithTies」への移行を促すためでしょう。

> CS0618: Method 'System.Linq.EnumerableEx.MaxBy<TSource,TKey>(this IEnumerable<TSource>, Func<TSource,TKey>)' 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>`返してほしかったな(これはプレビュー段階でフィードバックしなかったのが悪い)