Gleitkommakonvertierungen in kleine Ganzzahltypen sind sättigend

In .NET 11 weisen nicht geprüfte Gleitkommakonvertierungen in byte, ushort, short, char und jetzt an den Grenzen des Zieltyps ein sbyte auf. Werte, die zu klein oder zu groß sind, werden auf den Minimal- oder Maximalwert des Zieltyps festgelegt.

Diese Änderung setzt die .NET 9-Änderung an Gleitkomma-zu-Ganzzahl-Konvertierungen fort, die Konvertierungen von float und double zu int, uint, long und ulong standardisiert hat. .NET 11 erweitert die Sättigung auf 8- und 16-Bit-Ziele.

Die Änderung gilt für CoreCLR, einschließlich des Dolmetschers und der Native AOT. Mono ist in dieser Änderung nicht enthalten.

Weitere Informationen finden Sie unter dotnet/runtime#128604.

Eingeführt in Version

.NET 11 Vorschau 7

Bisheriges Verhalten

Bisher garantierte .NET das Ergebnis einer ungeprüften Gleitkomma-zu-Ganzzahl-Konvertierung nicht, wenn der Wert den Zieltyp überschritt oder NaN war. Die Ergebnisse können sich zwischen Laufzeitimplementierungen wie CoreCLR und Mono zwischen Architekturen wie x86, x64, Arm32, Arm64 und WebAssembly und zwischen Hardwareanweisungssätzen innerhalb einer Architektur wie x87, SSE2, AVX und AVX-512 unterscheiden.

Der .NET 9-Breaking Change hob speziell x86 und x64 hervor, bei denen Konvertierungen bei einem Überlauf häufig Sentinelwerte zurückgaben. Arm64 hat bereits Sättigungskonvertierungen nach Konventionen verwendet. Die Änderung standardisierte Konvertierungen in die breiteren Integer-Typen, anstatt die alten Sentinel-Ergebnisse als verbindliche Festlegung zu etablieren.

Bei kleinen Ganzzahltypen war in .NET 9 und .NET 10 eine gängige CoreCLR-Konvertierungssequenz eine Sättigungskonvertierung zu int, gefolgt von einer Verengung auf den Zieltyp durch Verwerfen der hohen Bits. Diese Sequenz erklärt das Verhalten vieler Anwendungen, war aber keine Garantie für eine direkte Umwandlung von Gleitkommazahlen in kleine Ganzzahlen.

Die folgende Tabelle zeigt Ergebnisse aus dieser zweistufigen Sequenz für eine Laufzeit float oder double einen Wert x. Diese Eingaben passen in int, sodass die Beispiele den Effekt des Verwerfens aller Bits außer den niedrigwertigen 8 bzw. 16 Bits des Ziels isolieren. Die beibehaltenen Bits werden für sbyte und byte als signiert und für ushort, char und short als vorzeichenlos interpretiert. Ergebnisse für char werden numerisch angezeigt.

Umwandeln in Wert von x Beibehalten von niedrigen Bits Beispiel für vorheriges Ergebnis
sbyte oder byte 298 0x2A 42
sbyte -298 0xD6 -42
byte -42 0xD6 214
short, ushort oder char 65578 0x002A 42
short -65578 0xFFD6 -42
ushort oder char -42 0xFFD6 65494

Der folgende Code könnte z. B. zurückgeben 42:

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

short result = ConvertValue(65578.0);

Der Zwischenwert int ist 65578 (0x0001002A). Wenn nur die unteren 16 Bit beibehalten werden, ergibt sich 42 (0x002A), statt einer Sättigung auf short.MaxValue.

Neues Verhalten

Ab .NET 11 sind nicht geprüfte Konvertierungen auf die Grenzen des Zieltyps begrenzt. Endliche Werte innerhalb des Zielbereichs werden weiterhin auf Null gerundet. NaN konvertiert in Null.

Umwandeln in Unter dem Minimum, einschließlich negativer Unendlichkeit Über dem Maximum, einschließlich positiver Unendlichkeit 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

Im vorherigen Beispiel wird jetzt 32767 (short.MaxValue) anstelle von 42 zurückgegeben. Ebenso gibt eine Konvertierung von 298 in byte nun 42 statt ushort zurück, und eine Konvertierung von -42 in 0 gibt nun 65494 statt 255 zurück.

Da sie über float konvertieren, verwenden die entsprechenden nicht überprüften Konvertierungen von Half ebenfalls das neue Verhalten. ConvertToInteger<TInteger>(Single) und ConvertToInteger<TInteger>(Double) werden jetzt bei diesen kleinen Zieltypen korrekt gesättigt.

Geprüfte Konvertierungen sind unverändert und lösen bei einem Überlauf weiterhin OverflowException aus. Diese Änderung ändert keine Ganzzahl-zu-Ganzzahl-Verengungskonvertierungen oder die breiteren Ganzzahl- und Vektorkonvertierungen, die durch die Änderung von .NET 9 abgedeckt werden.

Art der einschneidenden Änderung

Diese Änderung ist eine Verhaltensänderung.

Grund für die Änderung

Die .NET 9-Änderung führte ein Sättigungsverhalten für Konvertierungen in breitere Integer-Typen ein, aber Konvertierungen in 8-Bit- und 16-Bit-Zieltypen wiesen für Werte außerhalb des Wertebereichs und NaN weiterhin hardwareabhängiges und implementierungsabhängiges Verhalten auf. Diese Änderung verleiht diesen Konvertierungen ein deterministisches Sättigungsverhalten und sorgt dafür, dass die präinitialisierten Werte von JIT, CoreCLR-Interpreter und Native AOT übereinstimmen.

Wenn Ihr Code auf vorherigen Ergebnissen für Eingaben außerhalb des Bereichs basiert, aktualisieren Sie ihn so, dass die Sättigung an den Grenzen des Zieltyps nach Möglichkeit erwartet wird.

Wenn Sie das plattformeigene Verhalten benötigen, das vor diesen Änderungen häufig verwendet wurde, ist ConvertToIntegerNative<TInteger>(Single) oder ConvertToIntegerNative<TInteger>(Double) die einfachste Problemumgehung. Ersetzen Sie beispielsweise eine direkte (ushort)x-Typumwandlung durch double.ConvertToIntegerNative<ushort>(x), wenn float.ConvertToIntegerNative<ushort>(x) ein x ist, oder durch double, wenn es ein float ist.

Sie können auch die Zwischenkonvertierung explizit auswählen. In den folgenden Beispielen wird eine double Eingabe x und ein ushort Ziel verwendet:

Erforderliches Verhalten Umwandlung
Plattformeigene Konvertierung in den Zieltyp, wodurch das frühere Verhalten häufig wiederhergestellt wird double.ConvertToIntegerNative<ushort>(x)
Sättigung bis int, dann Verengung, entsprechend der häufigen CoreCLR-Sequenz von .NET 9 und .NET 10 unchecked((ushort)(int)x)
Plattformnative Konvertierung zu int, anschließende Verengung, entsprechend einer gängigen Sequenz vor .NET 9 unchecked((ushort)double.ConvertToIntegerNative<int>(x))

Verwenden Sie float für float.ConvertToIntegerNative-Eingaben und ersetzen Sie sbyte, byte, short oder char durch den entsprechenden Zieltyp.

Wie bei der Änderung in .NET 9 kann nicht garantiert werden, dass ConvertToIntegerNative vorherige Ergebnisse reproduziert, wenn Werte außerhalb des Bereichs liegen oder NaN sind. Es wählt das Verhalten aus, das für die aktuelle Plattform effizient ist, was sich über Laufzeiten, Architekturen oder Hardwarerevisionen hinweg ändern kann. Eine explizite (ushort)(int)x wählt stattdessen eine sättigende Konvertierung zu int mit anschließender Ganzzahlverengung; sie stellt nicht das Verhalten jeder historischen Implementierung wieder her.

Wenn der konvertierte Wert als Arrayindex, Pufferoffset oder Länge verwendet wird, überprüfen Sie, ob sich der resultierende Wert innerhalb der erforderlichen Grenzen befindet. Eine Konvertierung, die einen verwendbaren Wert auf einem Computer erzeugt, führt nicht dazu, dass die plattformeigene Konvertierung auf einem anderen Computer erfolgt.

Betroffene APIs