浮動小数点から小さい整数型への変換が飽和している

.NET 11 では、sbyte、byte、short、ushort、および char への unchecked な浮動小数点変換が、変換先の型の境界値で 飽和 する動作をするようになりました。 小さすぎる値または大きすぎる値は、それぞれ宛先の型の最小値または最大値に設定されます。

この変更により、.NET 9 の浮動小数点から整数への変換が継続され、floatおよびdoubleからint、uint、long、およびulongへの変換が標準化されます。 .NET 11 では、飽和が 8 ビットおよび 16 ビットの出力先まで拡張されます。

この変更は、インタープリターやネイティブ AOT を含む CoreCLR に適用されます。 Mono は、この変更には含まれません。

詳細については、「 dotnet/runtime#128604」を参照してください。

導入されたバージョン

.NET 11 Preview 7

以前の動作

以前は、.NET では、値が変換先の型でオーバーフローした場合、または NaN であった場合、チェックを行わない浮動小数点から整数への変換の結果を保証していませんでした。 結果は、CoreCLR や Mono などのランタイム実装と、x86、x64、Arm32、Arm64、WebAssembly などのアーキテクチャ間、および x87、SSE2、AVX、AVX-512 などのアーキテクチャ内のハードウェア命令セット間で異なる場合があります。

.NET 9 の破壊的変更では、特に x86 と x64 が強調表示され、一般的に変換によってオーバーフロー時に sentinel 値が返されます。 Arm64 では、慣例により飽和変換が既に使用されています。 変更により、古いセンチネルの結果をコントラクトとして確立するのではなく、より広い整数型への変換が標準化されました。

小さい整数型の場合、.NET 9 と .NET 10 の共通の CoreCLR 変換シーケンスは、intへの飽和変換であり、次に上位ビットを破棄して宛先の型に絞り込みます。 このシーケンスでは、多くのアプリケーションで発生した動作について説明しますが、浮動小数点から小整数キャストへの直接コントラクトは保証されていませんでした。

次の表は、ランタイム float または double 値 xのその 2 段階シーケンスの結果を示しています。 これらの入力は intに収まるため、例では宛先の下位 8 ビットまたは 16 ビットを除くすべてを破棄する効果を分離します。 保持されるビットは、 sbyte と shortの符号付きビットとして解釈され、 byte、 ushort、および charでは符号なしとして解釈されます。 charの結果を数値で示します。

変換する x の値 下位ビットの保持 前の結果の例
sbyte または byte 298 0x2A 42
sbyte -298 0xD6 -42
byte -42 0xD6 214
short、 ushort、または char 65578 0x002A 42
short -65578 0xFFD6 -42
ushort または char -42 0xFFD6 65494

たとえば、次のコードは 42を返します。

static short ConvertValue(double value)
{
    return unchecked((short)value);
}

short result = ConvertValue(65578.0);

中間 int は 65578 (0x0001002A) です。 低い 16 ビットのみが保持されているため、結果はshort.MaxValueの飽和ではなく、42 (0x002A) になります。

新しい動作

.NET 11 以降では、チェックを行わない変換は変換先の型の境界値で飽和します。 ターゲット範囲内の有限値は、引き続きゼロに丸められます。 NaN は 0 に変換されます。

~に変換 最小以下 (負の無限大を含む) 正の無限大を含む最大値を超える NaN
sbyte -128 (sbyte.MinValue) 127 (sbyte.MaxValue) 0
byte 0 (byte.MinValue) 255 (byte.MaxValue) 0
short -32768 (short.MinValue) 32767 (short.MaxValue) 0
ushort 0 (ushort.MinValue) 65535 (ushort.MaxValue) 0
char 0 (char.MinValue) 65535 (char.MaxValue) 0

前の例では、42ではなく32767 (short.MaxValue) が返されるようになりました。 同様に、298 から byte への変換では、42ではなく255が返されるようになり、-42 から ushort への変換では、65494ではなく0が返されるようになりました。

float を介して変換されるため、Half からの対応する未チェック変換も新しい動作になります。 ConvertToInteger<TInteger>(Single) と ConvertToInteger<TInteger>(Double) は、これらの小さい宛先型に対して正しく飽和するようになりました。

チェックを行わない変換はこれまでどおり変更されず、変換がオーバーフローした場合は引き続き OverflowException がスローされます。 この変更により、整数から整数への縮小変換や、.NET 9 の変更によってカバーされるより広い整数およびベクトル変換は変更されません。

破壊的変更の種類

この変更は 動作の変更です。

変更理由

.NET 9 の変更により、より広い整数型への変換の飽和動作が確立されましたが、8 ビットと 16 ビットの変換先への変換では、まだ、範囲外の値とNaNに対するハードウェア依存および実装依存の動作がありました。 この変更により、これらの変換は決定的で飽和的な動作になり、JIT、CoreCLR インタープリター、およびネイティブ AOT 事前初期化値が一致します。

コードが範囲外の入力に対して以前の結果に依存している場合は、可能な限り、変換先の型の境界での飽和を期待するように更新します。

これらの変更の前に一般的に使用されるプラットフォームネイティブの動作が必要な場合、最も簡単な回避策は ConvertToIntegerNative<TInteger>(Single) または ConvertToIntegerNative<TInteger>(Double)です。 たとえば、(ushort)x が double.ConvertToIntegerNative<ushort>(x) の場合は直接 x キャストを double に置き換え、それが float.ConvertToIntegerNative<ushort>(x) の場合は float に置き換えます。

中間変換を明示的に選択することもできます。 次の例では、double 入力 x と ushort 変換先を使用します。

必要な動作 Conversion
移行先の型へのプラットフォームネイティブ変換。これは、一般的に以前の動作を回復します double.ConvertToIntegerNative<ushort>(x)
一般的な .NET 9 および .NET 10 CoreCLR シーケンスに一致する、int への飽和、その後のナロー化 unchecked((ushort)(int)x)
int へのプラットフォーム ネイティブ変換を行った後に縮小変換を行うことで、.NET 9 より前の一般的なシーケンスと一致します unchecked((ushort)double.ConvertToIntegerNative<int>(x))

float入力にはfloat.ConvertToIntegerNativeを使用し、sbyte、byte、short、またはcharの適切な宛先の種類に置き換える。

.NET 9 の変更と同様に、ConvertToIntegerNativeは範囲外の値やNaNに対して以前の結果を再現することは保証されません。 現在のプラットフォームで効率的な動作が選択され、ランタイム、アーキテクチャ、またはハードウェアリビジョン間で変更される可能性があります。 明示的な (ushort)(int)x では、代わりに、 int への飽和変換の後に整数の縮小が選択されます。すべての履歴実装の動作が復元されるわけではありません。

変換された値が配列インデックス、バッファー オフセット、または長さとして使用される場合は、結果の値が必要な境界内にあることを検証します。 あるコンピューターで使用可能な値を生成する変換では、プラットフォームネイティブ変換が別のコンピューターで行うことを確立しません。

影響を受ける API