.NET 10のTimeSpan.FromMillisecondsメソッドのオーバーロードの追加について

.NET 10で、TimeSpan型のFromMillisecondsメソッドに新しいオーバーロードが追加されます。


# .NET 10で追加されるFromMillisecondsのオーバーロード

.NET 9時点で、TimeSpan型のFromMillisecondsメソッドは次の2つのオーバーロードがあります。

```csharp
public static TimeSpan FromMilliseconds(double value);
public static TimeSpan FromMilliseconds(long milliseconds, long microseconds = 0);
```

2つ目のオーバーロードは、.NET 9で加わった比較的新しいオーバーロードです。第2引数としてoptional引数なmicrosecondsという引数をとります。

さて、.NET 10でさらに新たなオーバーロードが追加されます。追加されるオーバーロードは、long型の引数millisecondsのみを取ります。また、既存のlong型を2つ引数にとるオーバーロードは、optional引数でなく通常の引数を取るようになります。

```csharp
public static TimeSpan FromMilliseconds(double value);
public static TimeSpan FromMilliseconds(long milliseconds);
public static TimeSpan FromMilliseconds(long milliseconds, long microseconds);
```

# 追加された理由

.NET 9で追加されたこのオーバーロード、一見良さそうなんですが実は問題がありました。

```csharp
public static TimeSpan FromMilliseconds(long milliseconds, long microseconds = 0);
```

次に示すような、式ツリーを作るコードでコンパイルエラーになってしまいます。

```csharp
Expression<Action> a = () => TimeSpan.FromMilliseconds(1000);
```

コンパイルエラーのメッセージは次のとおりです。

「An expression tree cannot contain a call or invocation that uses optional arguments」

式ツリーにおいて、optional引数は使えないようです。そのため、.NET 10で次の様なオーバーロードが追加され、

```csharp
public static TimeSpan FromMilliseconds(long milliseconds);
```

次のオーバーロードのoptional引数が無くなりました。

```csharp
public static TimeSpan FromMilliseconds(long milliseconds, long microseconds);
```

この追加により、.NET 10では次に示すコードがコンパイルできるようになりました。

```csharp
Expression<Action> a = () => TimeSpan.FromMilliseconds(1000);
```


# 破壊的な変更ではない？

「この変更、破壊的な変更じゃないと思うけれど、もしかして破壊的な変更じゃないか？」と思って調べてみました。

---

まず「[.NET 10の破壊的な変更一覧](https://learn.microsoft.com/ja-jp/dotnet/core/compatibility/10.0)」に、記載がないかを確認しました。こちらには、記載されていませんでした。


---


[.NET ライブラリの変更に関する規則](https://learn.microsoft.com/ja-jp/dotnet/core/compatibility/library-change-rules)には、次の様な記載がありました。

> ❌未許可: プロパティ、フィールド、またはパラメーターの既定値を変更する

> パラメーターの既定値の変更または削除は、バイナリ区切りではありません。 パラメーターの既定値を削除すると、ソースの中断が発生し、パラメーターの既定値を変更すると、再コンパイル後に動作が中断する可能性があります。

> このため、あいまいさをなくすために、既定値を新しいメソッド オーバーロードに "移動" する特定のケースでは、パラメーターの既定値を削除することができます。 たとえば、既存のメソッド MyMethod(int a = 1) を考えてみます。 2 つの省略可能なパラメーター a と b を使用して MyMethod のオーバーロードを導入する場合は、a の既定値を新しいオーバーロードに移動することで互換性を維持できます。 ここで、2 つのオーバーロードは MyMethod(int a) と MyMethod(int a = 1, int b = 2) です。 このパターンでは、MyMethod() をコンパイルできます。

> このため、あいまいさをなくすために、既定値を新しいメソッド オーバーロードに "移動" する特定のケースでは、パラメーターの既定値を削除することができます。

今回のFromMillisecondsメソッドのオーバーロードの追加は、optional引数（上記ドキュメントでは規定値）を削除し、オーバーロードに移動するケースですね。

---

念の為、[コード変更が互換性に影響を与えるしくみ](https://learn.microsoft.com/ja-jp/dotnet/core/compatibility/categories)を確認します。

まずは、「ソースの互換性」。

> ソースの互換性とは、API の既存のコンシューマーが、ソースを変更せずに新しいバージョンに再コンパイルできることを指します。 "ソース非互換な変更" は、コンシューマーが新しいバージョンの API が正しくビルドされるように、ソース コードを変更する必要がある場合に発生します。

次に示すような、TimeSpan型のFromMillisecondsメソッドを呼び出しているソースコードがあるとします。

```csharp
var timespan = TimeSpan.FromMilliseconds(500);
```

.NET 9と.NET 10の間で、呼び出されるオーバーロードは変わりますが、正しく再コンパイルできるため、ソースの互換性は保てていそうです。

---

次は、「バイナリの互換性」。

> バイナリの互換性とは、API のコンシューマーが、再コンパイルせずに新しいバージョンで API を使用できることです。 型へのメソッドの追加または新しいインターフェイス実装の追加などの変更は、バイナリの互換性には影響しません。 しかし、アセンブリによって公開されるインターフェイスと同じものにコンシューマーがアクセスできなくなるように、アセンブリのパブリック シグネチャが削除または変更されると、バイナリの互換性が影響を受けます。 このような種類の変更は、"バイナリ非互換な変更" と呼ばれます。

次に示すオーバーロードを使って.NET 9でコンパイルされたバイナリは、.NET 10で変更されたAPIを、再コンパイルせずとも使用できそうです。

```csharp
public static TimeSpan FromMilliseconds(long milliseconds, long microseconds = 0);
```

確認していきます。

まず、次に示すように第2引数を明示して呼び出しているソースコードを、.NET 9でコンパイルしたバイナリがあるとします。.NET 10でも、変わらず引数を2つ取るオーバーロードを呼び出しており、問題なく使用することができます。

```csharp
var timespan0 = TimeSpan.FromMilliseconds(500, 0);
var timespan1 = TimeSpan.FromMilliseconds(500, 100);
```

そして、次に示すoptional引数を活用しているコードを、.NET 9でコンパイルしたバイナリがあるとします。

```csharp
var timespan3 = TimeSpan.FromMilliseconds(500);
```

optional引数は、メソッド利用側（呼び出し側）のバイナリに、optional引数の値が埋め込まれます。そのため、.NET 9でコンパイルしたバイナリは実質、次に示すような呼び出しとなっています。


```csharp
var timespan3 = TimeSpan.FromMilliseconds(500, 0);
```

そのため、NET 10でも変わらず引数を2つ取るオーバーロードを呼び出しており、こちらも再コンパイルする必要なく、問題なく使用することができます。

---

次に、「動作の変更」です。

> 動作の変更とは、メンバーの動作の変更を指します。 この変更は、外部から参照可能 (たとえば、メソッドが別の例外をスローするなど) である場合があります。または、変更された実装 (たとえば、戻り値の計算方法の変更、メソッドの内部呼び出しの追加または削除、またはパフォーマンスの大幅な向上など) を指す場合があります。

> 動作の変更が、外部から参照可能、かつ型のパブリック コントラクトを変更するものである場合、バイナリの互換性に影響するため、簡単に評価できます。 実装の変更の評価はさらに困難です。変更の性質、および API の使用の頻度およびパターンによって、変更の影響は深刻なものから無害なものにまで及びます。

これは、[変更PR](https://github.com/dotnet/runtime/pull/110234)を確認すると、実装の変更はされています。しかし、破壊的な変更ではなさそうです。

# まとめ

本投稿では、.NETにおけるoptional引数関連の互換性に触れつつ、.NET 10で追加されるTimeSpan型のFromMillisecondsメソッドの新しいオーバーロードを紹介しました。

# 関連リンク

* [変更PR](https://github.com/dotnet/runtime/pull/110234)
* [関連issue](https://github.com/dotnet/runtime/issues/109833)
* 公式ドキュメント：[コード変更が互換性に影響を与えるしくみ](https://learn.microsoft.com/ja-jp/dotnet/core/compatibility/categories#behavioral-change)
* 公式ドキュメント：[Resolve errors and warnings generated from expressions prohibited in expression trees](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/compiler-messages/expression-tree-restrictions)