Преобразования с плавающей запятой в небольшие целочисленные типы являются насыщенными

В .NET 11 непроверяемые преобразования из чисел с плавающей запятой в sbyte, byte, short, ushort и char теперь имеют поведение с насыщением на границах диапазона целевого типа. Значения, которые слишком малы или слишком большие, имеют минимальное или максимальное значение типа назначения соответственно.

Это изменение развивает изменение, внесённое в .NET 9 в преобразования чисел с плавающей запятой в целые, которое стандартизировало преобразования из float и double в int, uint, long и ulong. .NET 11 расширяет насыщенность до 8-и 16-разрядных назначений.

Это изменение распространяется на CoreCLR, включая его интерпретатор, и Native AOT. Mono не включен в это изменение.

Дополнительные сведения см. в статье dotnet/runtime#128604.

Представленная версия

.NET 11 предварительная версия 7

Предыдущее поведение

Ранее .NET не гарантировал результат непроверяемого преобразования числа с плавающей запятой в целочисленный тип, если значение выходило за диапазон целевого типа или было NaN. Результаты могут отличаться между реализациями среды выполнения, такими как CoreCLR и Mono, между архитектурами, такими как x86, x64, Arm32, Arm64 и WebAssembly, а также между наборами аппаратных инструкций в архитектуре, например x87, SSE2, AVX и AVX-512.

В критическом изменении .NET 9 были особо выделены x86 и x64, где преобразования при переполнении обычно возвращали сторожевые значения. Arm64 уже использовал насыщенные преобразования по соглашению. Это изменение стандартизировало преобразования к более широким целочисленным типам, а не закрепило старые сигнальные значения как часть контракта.

Для малых целочисленных типов типичная последовательность преобразования CoreCLR в .NET 9 и .NET 10 представляла собой преобразование с насыщением в 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 в 255 теперь возвращает 42 вместо ushort, а преобразование из -42 в 0 теперь возвращает 65494 вместо byte.

Поскольку они преобразуются через 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). Например, замените прямое приведение (ushort)x на double.ConvertToIntegerNative<ushort>(x), если x — это double, или на float.ConvertToIntegerNative<ushort>(x), если это float.

Вы также можете явно выбрать промежуточное преобразование. В следующих примерах используются double в качестве входных данных x и ushort в качестве места назначения:

Требуемое поведение Conversion
Собственное для платформы преобразование в целевой тип, которое обычно восстанавливает прежнее поведение. double.ConvertToIntegerNative<ushort>(x)
Насыщение до int, затем сужение, в соответствии с общей последовательностью CoreCLR в .NET 9 и .NET 10 unchecked((ushort)(int)x)
Преобразование в int, встроенное в платформу, с последующим сужением, что соответствует распространённой последовательности, использовавшейся до .NET 9 unchecked((ushort)double.ConvertToIntegerNative<int>(x))

Используйте float.ConvertToIntegerNative для входных данных sbyte и замените соответствующий целевой тип для byte, char, short или float.

Как и при изменении .NET 9, ConvertToIntegerNativeне гарантируется воспроизведение предыдущих результатов для значений вне диапазона илиNaN. Выбирается поведение, эффективное для текущей платформы, которое может различаться в разных средах выполнения, на разных архитектурах или между ревизиями оборудования. Явное указание (ushort)(int)x вместо этого выбирает преобразование с насыщением в int с последующим целочисленным сужением; это не восстанавливает поведение всех исторических реализаций.

Если преобразованное значение используется в качестве индекса массива, смещения буфера или длины, убедитесь, что результирующее значение находится в обязательных границах. Преобразование, которое дает пригодное для использования значение на одной машине, не означает, что собственное для платформы преобразование даст такой же результат на другой.

Затронутые API