Le conversioni a virgola mobile in tipi integrali di piccole dimensioni sono sature

In .NET 11, le conversioni da virgola mobile non controllate in sbyte, byte, short, ushort e char ora hanno un comportamento di saturazione ai limiti del tipo di destinazione. I valori troppo piccoli o troppo grandi vengono impostati rispettivamente sul valore minimo o massimo del tipo di destinazione.

Questa modifica prosegue la modifica di .NET 9 relativa alle conversioni da virgola mobile a intero, che ha standardizzato le conversioni da float e double a int, long, uint e ulong. .NET 11 estende la saturazione alle destinazioni a 8 e 16 bit.

La modifica si applica a CoreCLR, incluso il relativo interprete e AOT nativo. Mono non è incluso in questa modifica.

Per altre informazioni, vedere dotnet/runtime#128604.

Versione introdotta

.NET 11 Preview 7

Comportamento precedente

In precedenza, .NET non garantiva il risultato di una conversione non verificata da virgola mobile a un tipo intero quando il valore eccedeva i limiti del tipo di destinazione o era NaN. I risultati possono variare tra le implementazioni di runtime, ad esempio CoreCLR e Mono, tra architetture, ad esempio x86, x64, Arm32, Arm64 e WebAssembly, e tra set di istruzioni hardware all'interno di un'architettura, ad esempio x87, SSE2, AVX e AVX-512.

La breaking change di .NET 9 metteva specificamente in evidenza x86 e x64, in cui le conversioni restituivano comunemente valori sentinella in caso di overflow. Arm64 usa già per convenzione conversioni con saturazione. La modifica ha standardizzato le conversioni verso i tipi interi più ampi anziché stabilire i precedenti valori sentinella come comportamento garantito.

Per i tipi integrali di piccole dimensioni, una comune sequenza di conversione di CoreCLR in .NET 9 e .NET 10 era una conversione con saturazione in int, seguita dal restringimento al tipo di destinazione mediante lo scarto dei bit più significativi. Questa sequenza spiega il comportamento riscontrato da molte applicazioni, ma non costituiva una garanzia per un cast diretto da virgola mobile a un tipo intero di piccole dimensioni.

La tabella seguente mostra i risultati di quella sequenza in due passaggi per un valore di runtime float o double pari a x. Questi input rientrano in int, quindi gli esempi isolano l'effetto dello scartare tutti i bit tranne gli 8 o 16 bit meno significativi della destinazione. I bit conservati vengono interpretati come firmati per sbyte e shorte senza segno per byte, ushorte char. I risultati per char vengono visualizzati numericamente.

Converti in Valore di x Bit bassi conservati Esempio di risultato precedente
sbyte oppure byte 298 0x2A 42
sbyte -298 0xD6 -42
byte -42 0xD6 214
short, ushort o char 65578 0x002A 42
short -65578 0xFFD6 -42
ushort oppure char -42 0xFFD6 65494

Ad esempio, il codice seguente potrebbe restituire 42:

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

short result = ConvertValue(65578.0);

L'elemento intermedio int è 65578 (0x0001002A). Poiché vengono mantenuti solo i 16 bit meno significativi, il risultato è 42 (0x002A), anziché una saturazione a short.MaxValue.

Nuovo comportamento

A partire da .NET 11, le conversioni non controllate vengono saturate ai limiti del tipo di destinazione. I valori finiti all'interno dell'intervallo di destinazione continuano a essere arrotondati verso zero. NaN viene convertito in zero.

Converti in Al di sotto del valore minimo, compreso l’infinito negativo Al di sopra del valore massimo, incluso l'infinito positivo 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

L'esempio precedente restituisce 32767 ora (short.MaxValue) anziché 42. Analogamente, una conversione da 298 a byte ora restituisce 255 invece di 42e una conversione da -42 a ushort ora restituisce 0 anziché 65494.

Poiché effettuano la conversione tramite float, anche le corrispondenti conversioni unchecked da Half utilizzano il nuovo comportamento. ConvertToInteger<TInteger>(Single) e ConvertToInteger<TInteger>(Double) ora vanno correttamente in saturazione per questi tipi di destinazione di piccole dimensioni.

Le conversioni controllate restano invariate e continuano a sollevare OverflowException quando si verifica un overflow della conversione. Questa modifica non altera le conversioni con riduzione da intero a intero né le conversioni più estese di interi e vettori previste dalla modifica di .NET 9.

Tipo di modifica che causa un'interruzione

Questa modifica è una modifica funzionale.

Motivo della modifica

La modifica di .NET 9 ha introdotto il comportamento di saturazione per le conversioni verso tipi interi più ampi, ma le conversioni verso tipi di destinazione a 8 e 16 bit continuavano ad avere un comportamento dipendente dall'hardware e dall'implementazione per i valori fuori intervallo e NaN. Questa modifica conferisce a quelle conversioni un comportamento deterministico e con saturazione e fa sì che i valori preinizializzati di JIT, interprete CoreCLR e Native AOT coincidano.

Se il codice si basa sui risultati precedenti per input fuori intervallo, aggiornalo in modo che tenga conto, ove possibile, della saturazione ai limiti del tipo di destinazione.

Se è necessario il comportamento nativo della piattaforma comunemente usato prima di queste modifiche, la soluzione alternativa più semplice è ConvertToIntegerNative<TInteger>(Single) o ConvertToIntegerNative<TInteger>(Double). Ad esempio, sostituire un cast diretto (ushort)x con double.ConvertToIntegerNative<ushort>(x) quando x è un double oggetto o float.ConvertToIntegerNative<ushort>(x) quando è un float oggetto.

È anche possibile selezionare la conversione intermedia in modo esplicito. Gli esempi seguenti utilizzano un double input x e una ushort destinazione:

Comportamento obbligatorio Conversion
Conversione nativa della piattaforma nel tipo di destinazione, che in genere recupera il comportamento precedente double.ConvertToIntegerNative<ushort>(x)
Saturazione fino a int, poi restringimento, in linea con la comune sequenza CoreCLR di .NET 9 e .NET 10 unchecked((ushort)(int)x)
Conversione nativa della piattaforma in int, quindi restringimento, in linea con una comune sequenza precedente a .NET 9 unchecked((ushort)double.ConvertToIntegerNative<int>(x))

Usare float.ConvertToIntegerNative per float gli input e sostituire il tipo di destinazione appropriato per sbyte, byte, shorto char.

Come per la modifica di .NET 9, ConvertToIntegerNativenon garantisce la riproduzione dei risultati precedenti per valori fuori intervallo o NaN. Seleziona il comportamento più efficiente per la piattaforma corrente, che può variare tra ambienti di runtime, architetture o revisioni hardware. Un (ushort)(int)x esplicito seleziona invece una conversione con saturazione in int, seguita da un restringimento a intero; non ripristina il comportamento di tutte le implementazioni storiche.

Se il valore convertito viene usato come indice di matrice, offset del buffer o lunghezza, verificare che il valore risultante si trova entro i limiti necessari. Una conversione che produce un valore utilizzabile in un computer non stabilisce che la conversione nativa della piattaforma verrà eseguita su un'altra.

Le API interessate