Condividi tramite


Numeri interi di dimensioni native

Nota

Questo articolo è una specifica di funzionalità. La specifica funge da documento di progettazione per la funzionalità. Include le modifiche specifiche proposte, insieme alle informazioni necessarie durante la progettazione e lo sviluppo della funzionalità. Questi articoli vengono pubblicati fino a quando le modifiche specifiche proposte non vengono completate e incorporate nella specifica ECMA corrente.

Potrebbero verificarsi alcune discrepanze tra la specifica di funzionalità e l'implementazione completata. Tali differenze vengono acquisite nelle note pertinenti del language design meeting (LDM) .

Per ulteriori dettagli sul processo di adozione degli speclet di funzionalità nello standard del linguaggio C#, consultare l'articolo sulle specifiche di .

Problema del campione: https://github.com/dotnet/csharplang/issues/435

Sommario

Supporto linguistico per tipi di interi con segno e senza segno di dimensioni native.

La motivazione è per scenari di interoperabilità e per librerie a basso livello.

Disegno

Gli identificatori nint e nuint sono nuovi termini contestuali che rappresentano tipi interi con segno e senza segno nativi. Gli identificatori vengono trattati solo come parole chiave quando il controllo dei nomi non trova un risultato valido in quel punto del programma.

nint x = 3;
_ = nint.Equals(x, 3);

I tipi nint e nuint sono rappresentati dai tipi sottostanti System.IntPtr e System.UIntPtr con il compilatore che rende visibili conversioni e operazioni aggiuntive per tali tipi come numeri interi nativi.

Costanti

Le espressioni costanti possono essere di tipo nint o nuint. Non esiste una sintassi diretta per i letterali interi nativi. È possibile usare invece cast impliciti o espliciti di altri valori costanti integrali: const nint i = (nint)42;.

nint costanti sono incluse nell'intervallo [ int.MinValue, int.MaxValue ].

nuint costanti sono incluse nell'intervallo [ uint.MinValue, uint.MaxValue ].

Non sono presenti campi MinValue o MaxValue in nint o nuint perché, oltre a nuint.MinValue, tali valori non possono essere emessi come costanti.

La riduzione costante è supportata per tutti gli operatori unari { +, -, ~ } e gli operatori binari { +, -, *, /, %, ==, !=, <, <=, >, >=, &, |, ^, <<, >> }. Le operazioni di riduzione costante vengono valutate con Int32 e UInt32 operandi anziché int nativi per un comportamento coerente indipendentemente dalla piattaforma del compilatore. Se l'operazione restituisce un valore costante in 32 bit, la riduzione costante viene eseguita in fase di compilazione. In caso contrario, l'operazione viene eseguita in fase di esecuzione e non considerata una costante.

Conversioni

Esiste una conversione di identità tra nint e IntPtre tra nuint e UIntPtr. Esiste una conversione di identità tra tipi composti che differiscono solo per i tipi nativi e per i tipi sottostanti: matrici, Nullable<>, tipi costruiti e tuple.

Le tabelle seguenti illustrano le conversioni tra tipi speciali. L'IL per ogni conversione include le varianti per i contesti unchecked e checked se diversi.

Note generali sulla tabella seguente:

  • conv.u è una conversione a estensione zero in intero nativo e conv.i è una conversione a estensione di segno in intero nativo.
  • checked contesti sia per allargamento che per restrizione:
    • conv.ovf.* per signed to *
    • conv.ovf.*.un per unsigned to *
  • unchecked contesti per l'ampliamento di sono:
    • conv.i* per signed to * (dove * è la larghezza di destinazione)
    • conv.u* per unsigned to * (dove * è la larghezza di destinazione)
  • unchecked contesti di restringimento per sono:
    • conv.i* per any to signed * (dove * è la larghezza di destinazione)
    • conv.u* per any to unsigned * (dove * è la larghezza di destinazione)

Prendendo alcuni esempi:

  • sbyte to nint e sbyte to nuint usano conv.i, mentre byte to nint e byte to nuint usano conv.u perché sono tutti che ampliano.
  • nint to byte e nuint to byte usare conv.u1 mentre nint to sbyte e nuint to sbyte usare conv.i1. Per byte, sbyte, shorte ushort il "tipo di stack" è int32. Quindi conv.i1 è effettivamente "ridotto a un byte con segno e quindi esteso nel segno fino a int32" mentre conv.u1 è effettivamente "ridotto a un byte senza segno e quindi esteso con zeri fino a int32".
  • checked void* to nint usa conv.ovf.i.un allo stesso modo in cui checked void* to long usa conv.ovf.i8.un.
Operando Bersaglio Conversione IL
object nint Apertura della confezione unbox
void* nint PointerToVoid nop/conv.ovf.i.un
sbyte nint ImplicitNumeric conv.i
byte nint ImplicitNumeric conv.u
short nint ImplicitNumeric conv.i
ushort nint ImplicitNumeric conv.u
int nint ImplicitNumeric conv.i
uint nint ExplicitNumeric conv.u / conv.ovf.i.un
long nint ExplicitNumeric conv.i / conv.ovf.i
ulong nint ExplicitNumeric conv.i / conv.ovf.i.un
char nint ImplicitNumeric conv.u
float nint ExplicitNumeric conv.i / conv.ovf.i
double nint ExplicitNumeric conv.i / conv.ovf.i
decimal nint ExplicitNumeric long decimal.op_Explicit(decimal) conv.i / ... conv.ovf.i
IntPtr nint Identità
UIntPtr nint Nessuno
object nuint Apertura della confezione unbox
void* nuint PointerToVoid Nop
sbyte nuint ExplicitNumeric conv.i / conv.ovf.u
byte nuint ImplicitNumeric conv.u
short nuint ExplicitNumeric conv.i / conv.ovf.u
ushort nuint ImplicitNumeric conv.u
int nuint ExplicitNumeric conv.i / conv.ovf.u
uint nuint ImplicitNumeric conv.u
long nuint ExplicitNumeric conv.u / conv.ovf.u
ulong nuint ExplicitNumeric conv.u / conv.ovf.u.un
char nuint ImplicitNumeric conv.u
float nuint ExplicitNumeric conv.u / conv.ovf.u
double nuint ExplicitNumeric conv.u / conv.ovf.u
decimal nuint ExplicitNumeric ulong decimal.op_Explicit(decimal) conv.u / ... conv.ovf.u.un
IntPtr nuint Nessuno
UIntPtr nuint Identità
Enumerazione nint Enumerazione Esplicita
Enumerazione nuint Enumerazione Esplicita
Operando Bersaglio Conversione IL
nint object Boxe box
nint void* PointerToVoid nop/conv.ovf.u
nint nuint ExplicitNumeric conv.u (può essere omesso) /conv.ovf.u
nint sbyte ExplicitNumeric conv.i1 / conv.ovf.i1
nint byte ExplicitNumeric conv.u1 / conv.ovf.u1
nint short ExplicitNumeric conv.i2 / conv.ovf.i2
nint ushort ExplicitNumeric conv.u2 / conv.ovf.u2
nint int ExplicitNumeric conv.i4 / conv.ovf.i4
nint uint ExplicitNumeric conv.u4 / conv.ovf.u4
nint long ImplicitNumeric conv.i8
nint ulong ExplicitNumeric conv.i8 / conv.ovf.u8
nint char ExplicitNumeric conv.u2 / conv.ovf.u2
nint float ImplicitNumeric conv.r4
nint double ImplicitNumeric conv.r8
nint decimal ImplicitNumeric conv.i8 decimal decimal.op_Implicit(long)
nint IntPtr Identità
nint UIntPtr Nessuno
nint Enumerazione Enumerazione Esplicita
nuint object Boxe box
nuint void* PointerToVoid Nop
nuint nint ExplicitNumeric conv.i(può essere omesso) /conv.ovf.i.un
nuint sbyte ExplicitNumeric conv.i1 / conv.ovf.i1.un
nuint byte ExplicitNumeric conv.u1 / conv.ovf.u1.un
nuint short ExplicitNumeric conv.i2 / conv.ovf.i2.un
nuint ushort ExplicitNumeric conv.u2 / conv.ovf.u2.un
nuint int ExplicitNumeric conv.i4 / conv.ovf.i4.un
nuint uint ExplicitNumeric conv.u4 / conv.ovf.u4.un
nuint long ExplicitNumeric conv.u8 / conv.ovf.i8.un
nuint ulong ImplicitNumeric conv.u8
nuint char ExplicitNumeric conv.u2 / conv.ovf.u2.un
nuint float ImplicitNumeric conv.r.un conv.r4
nuint double ImplicitNumeric conv.r.un conv.r8
nuint decimal ImplicitNumeric conv.u8 decimal decimal.op_Implicit(ulong)
nuint IntPtr Nessuno
nuint UIntPtr Identità
nuint Enumerazione Enumerazione Esplicita

La conversione da A a Nullable<B> è:

  • conversione implicita nullable se è presente una conversione di identità o una conversione implicita da A a B;
  • una conversione esplicita "nullable" se è presente una conversione esplicita da A a B;
  • in caso contrario, non è valido.

La conversione da Nullable<A> a B è:

  • una conversione esplicita annullabile se esiste una conversione di identità o una conversione numerica, sia essa implicita o esplicita, da A a B;
  • in caso contrario, non è valido.

La conversione da Nullable<A> a Nullable<B> è:

  • una conversione di identità se è presente una conversione di identità da A a B;
  • una conversione esplicita annullabile se è presente una conversione numerica implicita o esplicita da A a B;
  • in caso contrario, non è valido.

Operatori

Gli operatori predefiniti sono i seguenti. Questi operatori vengono considerati durante la risoluzione dell'overload in base alle normali regole per le conversioni implicite se almeno uno degli operandi è di tipo nint o nuint.

(Il IL per ogni operatore include le varianti per unchecked e checked se diversi i contesti.)

Unario Firma dell'operatore IL
+ nint operator +(nint value) nop
+ nuint operator +(nuint value) nop
- nint operator -(nint value) neg
~ nint operator ~(nint value) not
~ nuint operator ~(nuint value) not
Binario Firma dell'operatore IL
+ nint operator +(nint left, nint right) add / add.ovf
+ nuint operator +(nuint left, nuint right) add / add.ovf.un
- nint operator -(nint left, nint right) sub / sub.ovf
- nuint operator -(nuint left, nuint right) sub / sub.ovf.un
* nint operator *(nint left, nint right) mul / mul.ovf
* nuint operator *(nuint left, nuint right) mul / mul.ovf.un
/ nint operator /(nint left, nint right) div
/ nuint operator /(nuint left, nuint right) div.un
% nint operator %(nint left, nint right) rem
% nuint operator %(nuint left, nuint right) rem.un
== bool operator ==(nint left, nint right) beq / ceq
== bool operator ==(nuint left, nuint right) beq / ceq
!= bool operator !=(nint left, nint right) bne
!= bool operator !=(nuint left, nuint right) bne
< bool operator <(nint left, nint right) blt / clt
< bool operator <(nuint left, nuint right) blt.un / clt.un
<= bool operator <=(nint left, nint right) ble
<= bool operator <=(nuint left, nuint right) ble.un
> bool operator >(nint left, nint right) bgt / cgt
> bool operator >(nuint left, nuint right) bgt.un / cgt.un
>= bool operator >=(nint left, nint right) bge
>= bool operator >=(nuint left, nuint right) bge.un
& nint operator &(nint left, nint right) and
& nuint operator &(nuint left, nuint right) and
| nint operator |(nint left, nint right) or
| nuint operator |(nuint left, nuint right) or
^ nint operator ^(nint left, nint right) xor
^ nuint operator ^(nuint left, nuint right) xor
<< nint operator <<(nint left, int right) shl
<< nuint operator <<(nuint left, int right) shl
>> nint operator >>(nint left, int right) shr
>> nuint operator >>(nuint left, int right) shr.un

Per alcuni operatori binari, gli operatori IL supportano tipi di operandi aggiuntivi (vedere ECMA-335 III.1.5 tabella dei tipi di operandi). Ma il set di tipi di operando supportati da C# è limitato per semplicità e per coerenza con gli operatori esistenti nel linguaggio.

Sono supportate le versioni lifted degli operatori, in cui gli argomenti e i tipi restituiti sono nint? e nuint?.

Le operazioni di assegnazione composte x op= y in cui x o y sono int nativi seguono le stesse regole di altri tipi primitivi con operatori predefiniti. In particolare, l'espressione viene associata come x = (T)(x op y) dove T è il tipo di x e dove x viene valutata una sola volta.

Gli operatori di spostamento devono mascherare il numero di bit da spostare, riducendolo a 5 bit se sizeof(nint) è 4 e a 6 bit se sizeof(nint) è 8. (vedere §12.11) nella specifica C#).

Il compilatore C#9 segnala errori di associazione a operatori integer nativi predefiniti durante la compilazione con una versione precedente del linguaggio, ma consentirà l'uso di conversioni predefinite da e verso numeri interi nativi.

csc -langversion:9 -t:library A.cs

public class A
{
    public static nint F;
}

csc -langversion:8 -r:A.dll B.cs

class B : A
{
    static void Main()
    {
        F = F + 1; // error: nint operator+ not available with -langversion:8
        F = (System.IntPtr)F + 1; // ok
    }
}

Aritmetica del puntatore

Non esistono operatori predefiniti in C# per l'addizione o la sottrazione del puntatore con offset integer nativi. Al contrario, i valori nint e nuint vengono promossi a long e ulong e l'aritmetica dei puntatori utilizza gli operatori predefiniti per tali tipi.

static T* AddLeftS(nint x, T* y) => x + y;   // T* operator +(long left, T* right)
static T* AddLeftU(nuint x, T* y) => x + y;  // T* operator +(ulong left, T* right)
static T* AddRightS(T* x, nint y) => x + y;  // T* operator +(T* left, long right)
static T* AddRightU(T* x, nuint y) => x + y; // T* operator +(T* left, ulong right)
static T* SubRightS(T* x, nint y) => x - y;  // T* operator -(T* left, long right)
static T* SubRightU(T* x, nuint y) => x - y; // T* operator -(T* left, ulong right)

Promozioni numeriche binarie

Le promozioni numeriche binarie nel testo informativo della specifica C# (vedere §12.4.7.3) sono state aggiornate come segue:

  • In caso contrario, se uno degli operandi è di tipo ulong, l'altro operando viene convertito nel tipo ulongo si verifica un errore di binding se l'altro operando è di tipo sbyte, short, int, ninto long.
  • In caso contrario, se uno degli operandi è di tipo nuint, l'altro operando viene convertito in tipo nuinto si verifica un errore di binding se l'altro operando è di tipo sbyte, short, int, ninto long.
  • In caso contrario, se uno degli operandi è di tipo long, l'altro operando viene convertito nel tipo long.
  • In caso contrario, se uno degli operandi è di tipo uint e l'altro operando è di tipo sbyte, short, nint, o int, entrambi gli operandi vengono convertiti nel tipo long.
  • In caso contrario, se uno degli operandi è di tipo uint, l'altro operando viene convertito nel tipo uint.
  • In caso contrario, se uno degli operandi è di tipo nint, l'altro operando viene convertito nel tipo nint.
  • In caso contrario, entrambi gli operandi vengono convertiti in tipo int.

Dinamico

Le conversioni e gli operatori vengono sintetizzati dal compilatore e non fanno parte dei tipi IntPtr e UIntPtr sottostanti. Di conseguenza, tali conversioni e operatori non sono disponibili dal binder di runtime per dynamic.

nint x = 2;
nint y = x + x; // ok
dynamic d = x;
nint z = d + x; // RuntimeBinderException: '+' cannot be applied 'System.IntPtr' and 'System.IntPtr'

Membri del tipo

L'unico costruttore per nint o nuint è il costruttore senza parametri.

I membri seguenti di System.IntPtr e System.UIntPtrvengono esclusi in modo esplicito da nint o nuint:

// constructors
// arithmetic operators
// implicit and explicit conversions
public static readonly IntPtr Zero; // use 0 instead
public static int Size { get; }     // use sizeof() instead
public static IntPtr Add(IntPtr pointer, int offset);
public static IntPtr Subtract(IntPtr pointer, int offset);
public int ToInt32();
public long ToInt64();
public void* ToPointer();

I membri rimanenti di System.IntPtr e System.UIntPtrvengono inclusi in modo implicito in nint e nuint. Per .NET Framework 4.7.2:

public override bool Equals(object obj);
public override int GetHashCode();
public override string ToString();
public string ToString(string format);

Le interfacce implementate da System.IntPtr e System.UIntPtrsono incluse implicitamente in in nint e nuint, con le occorrenze dei tipi sottostanti sostituite dai corrispondenti tipi interi nativi. Ad esempio, se IntPtr implementa ISerializable, IEquatable<IntPtr>, IComparable<IntPtr>, nint implementa ISerializable, IEquatable<nint>, IComparable<nint>.

Sovrascrivere, nascondere e implementare

nint e System.IntPtr, e nuint e System.UIntPtr, sono considerati equivalenti per l'override, l'occultamento e l'implementazione.

Gli overload non possono differire solo per nint e System.IntPtr, o nuint e System.UIntPtr. Le sostituzioni e le implementazioni possono differire per nint e System.IntPtr, o nuint e System.UIntPtr, singolarmente. I metodi nascondono altri metodi che differiscono solo per nint e System.IntPtr, o nuint e System.UIntPtr.

Misto

Le espressioni nint e nuint utilizzate come indici di array sono emesse senza conversione.

static object GetItem(object[] array, nint index)
{
    return array[index]; // ok
}

nint e nuint non possono essere usati come tipo di base enum da C#.

enum E : nint // error: byte, sbyte, short, ushort, int, uint, long, or ulong expected
{
}

Le operazioni di lettura e scrittura sono atomiche per nint e nuint.

I campi possono essere contrassegnati volatile per i tipi nint e nuint. ECMA-334 15.5.4 non include enum con base di tipo System.IntPtr o System.UIntPtr tuttavia.

default(nint) e new nint() sono equivalenti a (nint)0; default(nuint) e new nuint() sono equivalenti a (nuint)0.

typeof(nint) è typeof(IntPtr); typeof(nuint) è typeof(UIntPtr).

sizeof(nint) e sizeof(nuint) sono supportati, ma richiedono la compilazione in un contesto non sicuro (come richiesto per sizeof(IntPtr) e sizeof(UIntPtr)). I valori non sono costanti in fase di compilazione. sizeof(nint) viene implementato come sizeof(IntPtr) anziché come IntPtr.Size; sizeof(nuint) viene implementato come sizeof(UIntPtr) anziché come UIntPtr.Size.

Diagnostica del compilatore per i riferimenti ai tipi che coinvolgono nint o nuint riportano nint o nuint piuttosto che IntPtr o UIntPtr.

Metadati

nint e nuint sono rappresentati nei metadati come System.IntPtr e System.UIntPtr.

I riferimenti di tipo che includono nint o nuint vengono generati con un System.Runtime.CompilerServices.NativeIntegerAttribute per indicare quali parti del riferimento al tipo sono int nativi.

namespace System.Runtime.CompilerServices
{
    [AttributeUsage(
        AttributeTargets.Class |
        AttributeTargets.Event |
        AttributeTargets.Field |
        AttributeTargets.GenericParameter |
        AttributeTargets.Parameter |
        AttributeTargets.Property |
        AttributeTargets.ReturnValue,
        AllowMultiple = false,
        Inherited = false)]
    public sealed class NativeIntegerAttribute : Attribute
    {
        public NativeIntegerAttribute()
        {
            TransformFlags = new[] { true };
        }
        public NativeIntegerAttribute(bool[] flags)
        {
            TransformFlags = flags;
        }
        public readonly bool[] TransformFlags;
    }
}

La codifica dei riferimenti di tipo con NativeIntegerAttribute è descritta in NativeIntegerAttribute.md.

Alternative

Un'alternativa all'approccio di cancellazione dei tipi precedente consiste nell'introdurre nuovi tipi: System.NativeInt e System.NativeUInt.

public readonly struct NativeInt
{
    public IntPtr Value;
}

I tipi distinti consentirebbero l'overload distinto da IntPtr e permetterebbero un'analisi distinta e ToString(). Tuttavia, ci sarebbe più lavoro per il CLR nel gestire questi tipi in modo efficiente, vanificando lo scopo principale della funzionalità: l'efficienza. E l'interoperabilità con il codice int nativo esistente che usa IntPtr sarebbe più difficile.

Un'altra alternativa consiste nell'aggiungere un supporto int più nativo per IntPtr nel framework, ma senza supporto specifico del compilatore. Tutte le nuove conversioni e le operazioni aritmetiche sarebbero supportate automaticamente dal compilatore. Tuttavia, il linguaggio non fornisce parole chiave, costanti o operazioni di checked.

Riunioni di design