.NET 10で、TimeSpan型のFromMillisecondsメソッドに新しいオーバーロードが追加されます。
.NET 10で追加されるFromMillisecondsのオーバーロード
.NET 9時点で、TimeSpan型のFromMillisecondsメソッドは次の2つのオーバーロードがあります。
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引数でなく通常の引数を取るようになります。
public static TimeSpan FromMilliseconds(double value);
public static TimeSpan FromMilliseconds(long milliseconds);
public static TimeSpan FromMilliseconds(long milliseconds, long microseconds);
追加された理由
.NET 9で追加されたこのオーバーロード、一見良さそうなんですが実は問題がありました。
public static TimeSpan FromMilliseconds(long milliseconds, long microseconds = 0);
次に示すような、式ツリーを作るコードでコンパイルエラーになってしまいます。
Expression<Action> a = () => TimeSpan.FromMilliseconds(1000);
コンパイルエラーのメッセージは次のとおりです。
「An expression tree cannot contain a call or invocation that uses optional arguments」
式ツリーにおいて、optional引数は使えないようです。そのため、.NET 10で次の様なオーバーロードが追加され、
public static TimeSpan FromMilliseconds(long milliseconds);
次のオーバーロードのoptional引数が無くなりました。
public static TimeSpan FromMilliseconds(long milliseconds, long microseconds);
この追加により、.NET 10では次に示すコードがコンパイルできるようになりました。
Expression<Action> a = () => TimeSpan.FromMilliseconds(1000);
破壊的な変更ではない?
「この変更、破壊的な変更じゃないと思うけれど、もしかして破壊的な変更じゃないか?」と思って調べてみました。
まず「.NET 10の破壊的な変更一覧」に、記載がないかを確認しました。こちらには、記載されていませんでした。
.NET ライブラリの変更に関する規則には、次の様な記載がありました。
❌未許可: プロパティ、フィールド、またはパラメーターの既定値を変更する
パラメーターの既定値の変更または削除は、バイナリ区切りではありません。 パラメーターの既定値を削除すると、ソースの中断が発生し、パラメーターの既定値を変更すると、再コンパイル後に動作が中断する可能性があります。
このため、あいまいさをなくすために、既定値を新しいメソッド オーバーロードに "移動" する特定のケースでは、パラメーターの既定値を削除することができます。 たとえば、既存のメソッド MyMethod(int a = 1) を考えてみます。 2 つの省略可能なパラメーター a と b を使用して MyMethod のオーバーロードを導入する場合は、a の既定値を新しいオーバーロードに移動することで互換性を維持できます。 ここで、2 つのオーバーロードは MyMethod(int a) と MyMethod(int a = 1, int b = 2) です。 このパターンでは、MyMethod() をコンパイルできます。
このため、あいまいさをなくすために、既定値を新しいメソッド オーバーロードに "移動" する特定のケースでは、パラメーターの既定値を削除することができます。
今回のFromMillisecondsメソッドのオーバーロードの追加は、optional引数(上記ドキュメントでは規定値)を削除し、オーバーロードに移動するケースですね。
念の為、コード変更が互換性に影響を与えるしくみを確認します。
まずは、「ソースの互換性」。
ソースの互換性とは、API の既存のコンシューマーが、ソースを変更せずに新しいバージョンに再コンパイルできることを指します。 "ソース非互換な変更" は、コンシューマーが新しいバージョンの API が正しくビルドされるように、ソース コードを変更する必要がある場合に発生します。
次に示すような、TimeSpan型のFromMillisecondsメソッドを呼び出しているソースコードがあるとします。
var timespan = TimeSpan.FromMilliseconds(500);
.NET 9と.NET 10の間で、呼び出されるオーバーロードは変わりますが、正しく再コンパイルできるため、ソースの互換性は保てていそうです。
次は、「バイナリの互換性」。
バイナリの互換性とは、API のコンシューマーが、再コンパイルせずに新しいバージョンで API を使用できることです。 型へのメソッドの追加または新しいインターフェイス実装の追加などの変更は、バイナリの互換性には影響しません。 しかし、アセンブリによって公開されるインターフェイスと同じものにコンシューマーがアクセスできなくなるように、アセンブリのパブリック シグネチャが削除または変更されると、バイナリの互換性が影響を受けます。 このような種類の変更は、"バイナリ非互換な変更" と呼ばれます。
次に示すオーバーロードを使って.NET 9でコンパイルされたバイナリは、.NET 10で変更されたAPIを、再コンパイルせずとも使用できそうです。
public static TimeSpan FromMilliseconds(long milliseconds, long microseconds = 0);
確認していきます。
まず、次に示すように第2引数を明示して呼び出しているソースコードを、.NET 9でコンパイルしたバイナリがあるとします。.NET 10でも、変わらず引数を2つ取るオーバーロードを呼び出しており、問題なく使用することができます。
var timespan0 = TimeSpan.FromMilliseconds(500, 0);
var timespan1 = TimeSpan.FromMilliseconds(500, 100);
そして、次に示すoptional引数を活用しているコードを、.NET 9でコンパイルしたバイナリがあるとします。
var timespan3 = TimeSpan.FromMilliseconds(500);
optional引数は、メソッド利用側(呼び出し側)のバイナリに、optional引数の値が埋め込まれます。そのため、.NET 9でコンパイルしたバイナリは実質、次に示すような呼び出しとなっています。
var timespan3 = TimeSpan.FromMilliseconds(500, 0);
そのため、NET 10でも変わらず引数を2つ取るオーバーロードを呼び出しており、こちらも再コンパイルする必要なく、問題なく使用することができます。
次に、「動作の変更」です。
動作の変更とは、メンバーの動作の変更を指します。 この変更は、外部から参照可能 (たとえば、メソッドが別の例外をスローするなど) である場合があります。または、変更された実装 (たとえば、戻り値の計算方法の変更、メソッドの内部呼び出しの追加または削除、またはパフォーマンスの大幅な向上など) を指す場合があります。
動作の変更が、外部から参照可能、かつ型のパブリック コントラクトを変更するものである場合、バイナリの互換性に影響するため、簡単に評価できます。 実装の変更の評価はさらに困難です。変更の性質、および API の使用の頻度およびパターンによって、変更の影響は深刻なものから無害なものにまで及びます。
これは、変更PRを確認すると、実装の変更はされています。しかし、破壊的な変更ではなさそうです。
まとめ
本投稿では、.NETにおけるoptional引数関連の互換性に触れつつ、.NET 10で追加されるTimeSpan型のFromMillisecondsメソッドの新しいオーバーロードを紹介しました。