浮點數轉換為小型整數類型時會採用飽和處理

在 .NET 11 中,未經檢查的浮點數轉換為 sbyte、byte、short、ushort 和 char 時,現在會在目標類型的界限採用 飽和 行為。 過小或過大值分別設定為目的型別的最小值或最大值。

此變更延續了 .NET 9 對浮點轉整數轉換的做法,標準化了從 float 和 double 到 int、 uintlongulong的轉換。 .NET 11 將飽和度擴展至 8 位元與 16 位元目的地。

此變更適用於 CoreCLR,包括其直譯器,以及 Native AOT。 Mono 不包含在此次變更中。

更多資訊請參見 dotnet/runtime#128604。

所推出的版本

.NET 11 預覽版 7

以前的行為

先前,當數值溢出目標型別或為 NaN 時,.NET 不保證未經檢查的浮點數轉換為整數的結果。 結果可能因執行時實作(如 CoreCLR 和 Mono)、不同架構(如 x86、x64、Arm32、Arm64 和 WebAssembly),以及同一架構內的硬體指令集(如 x87、SSE2、AVX 和 AVX-512)而有所不同。

.NET 9 的破壞性變更特別指出 x86 和 x64 平台,在這些平台上,轉換發生溢位時通常會回傳哨值。 Arm64 已經按照慣例使用飽和轉換。 此變更將轉換統一為較寬的整數型別,而非將舊有的哨兵結果視為契約。

對於小型積分型別,.NET 9 和 .NET 10 常見的 CoreCLR 轉換序列是先飽和轉換成 int,接著透過捨棄高位元來縮小到目標類型。 此序列說明了許多應用程式所表現出的行為,但對於將浮點數直接轉換為較小的整數型別,這並不是一項保證的約定。

下表顯示該兩步序列對執行時間 float 或 double 值 x的結果。 這些輸入都可放入 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 位元時,結果為 42 (0x002A),而非飽和到 short.MaxValue。

新行為

從 .NET 11 開始,未勾選的轉換會在目的類型的邊界處飽和。 目標範圍內的有限值則持續向零四捨五入。 NaN 轉換成零。

轉換為 低於最小值,包括負無限 超過最大值,包括正無限 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

前述範例現在回傳 32767 (short.MaxValue) 而非 42。 同樣地,從 298 轉換為 byte 現在會回傳 255,而不是 42;從 -42 轉換為 ushort 現在也會回傳 0,而不是 65494。

由於它們是透過 float 進行轉換,對應的從 Half 的未檢查轉換也會採用新的行為。 ConvertToInteger<TInteger>(Single) 和 ConvertToInteger<TInteger>(Double) 現在可針對這些較小的目標型別正確執行飽和處理。

已檢查轉換維持不變,且當轉換發生溢位時,仍會擲回 OverflowException。 此變更不改變整數到整數的狹窄轉換,或 .NET 9 變更涵蓋的更寬整數與向量轉換。

破壞性變更的類型

此變更為行為變更。

變更原因

.NET 9 的變更確立了轉換為較寬整數型別時的飽和行為,但對於轉換為 8 位元和 16 位元目標型別的情況,超出範圍的值和 NaN 的行為仍取決於硬體與實作。 此變更使轉換行為具決定性且飽和,並使 JIT、CoreCLR 直譯器與 Native AOT 預初始值一致。

如果你的程式碼仰賴先前對超出範圍輸入的結果,請盡可能更新程式碼,使其預期結果會飽和至目標型別的上下限。

如果你需要在這些變更前常用的平台原生行為,最簡單的變通方法是 ConvertToIntegerNative<TInteger>(Single) 或 ConvertToIntegerNative<TInteger>(Double)。 例如,當 double.ConvertToIntegerNative<ushort>(x) 是 x 時,請將直接 double 轉型取代為 (ushort)x;如果是 float,則改為 float.ConvertToIntegerNative<ushort>(x)。

你也可以明確選擇中間轉換。 以下範例使用 double 輸入x 和 ushort 目的地:

必須的行為 Conversion
平台原生轉換至目的類型,通常能恢復先前的行為 double.ConvertToIntegerNative<ushort>(x)
飽和度為 int,接著變窄,與常見的 .NET 9 和 .NET 10 CoreCLR 序列相符 unchecked((ushort)(int)x)
先轉換為平台原生的 int,再縮小,這與 .NET 9 之前常見的順序相符 unchecked((ushort)double.ConvertToIntegerNative<int>(x))

使用 float.ConvertToIntegerNative 輸入 float ,並將適當的目的類型替換為 sbyte、 byte、 short或 char。

與 .NET 9 的變更相同,ConvertToIntegerNative對於超出範圍的NaN值或 ,並不保證能重現先前的結果。 它會選擇對當前平台有效率的行為,這些行為會隨著執行時、架構或硬體版本而改變。 明確指定 (ushort)(int)x 則會改為先飽和轉換到 int,再進行整數縮窄;它不會恢復所有歷史實作的行為。

若轉換後的值被用作陣列索引、緩衝區偏移量或長度,請驗證所得值是否在所需範圍內。 在一台機器上產生可用值的轉換,並不代表平台原生轉換在另一台機器上也能產生可用值。

受影響的 API