Compartir a través de


Enteros con tamaño nativos

Nota:

Este artículo es una especificación de características. La especificación actúa como documento de diseño de la característica. Incluye cambios de especificación propuestos, junto con la información necesaria durante el diseño y el desarrollo de la característica. Estos artículos se publican hasta que se finalizan los cambios de especificación propuestos y se incorporan en la especificación ECMA actual.

Puede haber algunas discrepancias entre la especificación de características y la implementación completada. Esas diferencias se recogen en las notas de la reunión de diseño de lenguaje (LDM) correspondientes.

Puede obtener más información sobre el proceso de adopción de especificaciones de características en el estándar del lenguaje C#, en el artículo sobre especificaciones.

Problema planteado por experto: https://github.com/dotnet/csharplang/issues/435

Resumen

Compatibilidad del lenguaje con tipos enteros con signo y sin signo de tamaño nativo.

Esta incorporación está dirigida a casos de interoperabilidad y bibliotecas de bajo nivel.

Diseño

Los identificadores nint y nuint son palabras clave contextuales nuevas que representan tipos de enteros con signo y sin signo nativos. Los identificadores solo se consideran palabras clave cuando la búsqueda de nombres no encuentra un resultado viable en esa ubicación del programa.

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

Los tipos nint y nuint se representan mediante los tipos subyacentes System.IntPtr y System.UIntPtr, donde el compilador muestra conversiones y operaciones adicionales para esos tipos como enteros nativos.

Constantes

Las expresiones constantes pueden ser de tipo nint o nuint. No hay ninguna sintaxis directa para los literales int nativos. En su lugar, se pueden usar conversiones implícitas o explícitas de otros valores constantes enteros: const nint i = (nint)42;.

Las constantes nint están en el intervalo [ int.MinValue, int.MaxValue ].

Las constantes nuint están en el intervalo [ uint.MinValue, uint.MaxValue ].

No hay campos MinValue ni MaxValue en nint o nuint porque, aparte de nuint.MinValue, esos valores no se pueden emitir como constantes.

El plegado de constantes se admite en todos los operadores unarios { +, -, ~ } y operadores binarios { +, -, *, /, %, ==, !=, <, <=, >, >=, &, |, ^, <<, >> }. Las operaciones de plegado constante se evalúan con los operandos Int32 y UInt32 en lugar de enteros nativos, a fin de garantizar un comportamiento coherente independientemente de la plataforma del compilador. Si la operación da como resultado un valor constante en 32 bits, el plegado de constantes se realiza en tiempo de compilación. De lo contrario, la operación se ejecuta en tiempo de ejecución y no se considera una constante.

Conversiones

Hay una conversión de identidad entre nint y IntPtr, así como entre nuint y UIntPtr. Hay una conversión de identidad entre tipos compuestos que difieren solo por enteros nativos y tipos subyacentes: matrices, Nullable<>, tipos construidos y tuplas.

En las tablas siguientes se indican las conversiones entre tipos especiales. (El IL para cada conversión incluye las variantes de los contextos unchecked y checked si son diferentes).

Notas generales de la tabla siguiente:

  • conv.u es una conversión de extensión de ceros a entero nativo y conv.i es una conversión de extensión de signo a entero nativo.
  • los contextos checked para ampliación como para restricción son:
    • conv.ovf.* para signed to *
    • conv.ovf.*.un para unsigned to *
  • los contextos unchecked para ampliación son:
    • conv.i* en signed to * (donde * es el ancho de destino)
    • conv.u* en unsigned to * (donde * es el ancho de destino)
  • los contextos unchecked para restricción son:
    • conv.i* en any to signed * (donde * es el ancho de destino)
    • conv.u* en any to unsigned * (donde * es el ancho de destino)

Tomemos algunos ejemplos:

  • sbyte to nint y sbyte to nuint usan conv.i mientras que byte to nint y byte to nuint usan conv.u porque todos son de ampliación.
  • nint to byte y nuint to byte usan conv.u1, mientras que nint to sbyte y nuint to sbyte usan conv.i1. Para byte, sbyte, short y ushort el "tipo de pila" es int32. Por lo tanto, conv.i1 se "convierte a un byte firmado y luego se extiende hasta int32", mientras que conv.u1 se "convierte a un byte sin signo y luego se extiende con ceros hasta int32".
  • checked void* to nint usa conv.ovf.i.un de la misma manera que checked void* to long usa conv.ovf.i8.un.
Operando Objetivo Conversión IL
object nint Desempaquetado 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 Identidad
UIntPtr nint Ninguna
object nuint Desempaquetado 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 Ninguna
UIntPtr nuint Identidad
Enumeración nint ExplicitEnumeration
Enumeración nuint ExplicitEnumeration
Operando Objetivo Conversión IL
nint object Boxing box
nint void* PointerToVoid nop / conv.ovf.u
nint nuint ExplicitNumeric conv.u (puede omitirse) / 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 Identidad
nint UIntPtr Ninguna
nint Enumeración ExplicitEnumeration
nuint object Boxing box
nuint void* PointerToVoid nop
nuint nint ExplicitNumeric conv.i(puede omitirse) / 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 Ninguna
nuint UIntPtr Identidad
nuint Enumeración ExplicitEnumeration

La conversión de A a Nullable<B> es:

  • conversión implícita que admite valores NULL si hay una conversión de identidad o conversión implícita de A a B.
  • conversión explícita que admite valores NULL si existe una conversión explícita de A a B;
  • o bien, no es válido.

La conversión de Nullable<A> a B es:

  • conversión explícita que admite valores NULL si hay una conversión de identidad o conversión numérica implícita o explícita de A a B;
  • o bien, no es válido.

La conversión de Nullable<A> a Nullable<B> es:

  • una conversión de identidad si hay una conversión de identidad de A a B;
  • conversión explícita que admite valores NULL si hay una conversión numérica implícita o explícita de A a B;
  • o bien, no es válido.

Operadores

Los operadores predefinidos son los siguientes. Estos operadores se tienen en cuenta durante la resolución de sobrecargas en función de reglas ordinarias de las conversiones implícitas si al menos uno de los operandos es de tipo nint o nuint.

(El IL de cada operador incluye variantes en los contextos unchecked y checked si son diferentes).

Unario Firma del operador 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 del operador 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

Para algunos operadores binarios, los operadores IL admiten tipos de operando adicionales (consulte la tabla de tipos de operando III.1.5 de ECMA-335). Aun así, el conjunto de tipos de operando admitidos por C# está limitado por motivos de simplicidad y coherencia con los operadores existentes en el lenguaje.

Se admiten las versiones elevadas de los operadores, donde los argumentos y los tipos de valor devuelto son nint? y nuint?.

Las operaciones de asignación compuestas x op= y donde x o y son ints nativos obedecen las mismas reglas que con otros tipos primitivos con operadores predefinidos. En concreto, la expresión se enlaza como x = (T)(x op y) donde T es el tipo de x y donde x solo se evalúa una vez.

Los operadores de desplazamiento deben enmascarar el número de bits a desplazar: a 5 bits si sizeof(nint) es 4 y a 6 bits si sizeof(nint) es 8. (consulte §12.11) en la especificación de C#).

El compilador de C#9 avisará de los errores que tengan que ver con operadores enteros nativos predefinidos al compilarse con una versión de lenguaje anterior, pero permitirá el uso de conversiones predefinidas hacia y desde enteros nativos.

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
    }
}

Aritmética de puntero

No hay operadores predefinidos en C# para la suma o resta de punteros con desplazamientos enteros nativos. En su lugar, los valores nint y nuint pasan a long y ulong y la aritmética con punteros usarán operadores predefinidos para esos tipos.

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)

Promociones numéricas binarias

Las promociones numéricas binarias en el texto informativo (consulte §12.4.7.3 en las especificaciones de C#) se actualizan de la siguiente manera:

  • De lo contrario, si cualquiera de los operandos es del tipo ulong, el otro operando se convierte al tipo ulong, o bien, se producirá un error de vinculación si el otro operando es del tipo sbyte, short, int, nint o long.
  • De lo contrario, si cualquiera de los operandos es del tipo nuint, el otro operando se convierte al tipo nuint, o bien, se producirá un error de vinculación si el otro operando es del tipo sbyte, short, int, nint o long.
  • De lo contrario, si cualquiera de los operandos es del tipo long, el otro operando se convierte al tipo long.
  • En caso contrario, si uno de los operandos es del tipo uint y el otro es del tipo sbyte, short, nint, o int, ambos operandos se convertirán y pasarán al tipo long.
  • De lo contrario, si cualquiera de los operandos es del tipo uint, el otro operando se convierte al tipo uint.
  • De lo contrario, si cualquiera de los operandos es del tipo nint, , el otro operando se convierte al tipo nint.
  • En caso contrario, ambos operandos se convierten al tipo int.

Dinámica

El compilador sintetiza las conversiones y los operadores, y no forman parte de los tipos subyacentes de IntPtr y UIntPtr. Como resultado, esas conversiones y operadores no están disponibles desde el enlazador en tiempo de ejecución para 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'

Miembros de tipos

El único constructor de nint o nuint es el constructor sin parámetros.

Los siguientes miembros de System.IntPtr y System.UIntPtrestán excluidos explícitamente de 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();

Los miembros restantes de System.IntPtr y System.UIntPtrse incluyen implícitamente en nint y nuint. Para .NET Framework 4.7.2:

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

Las interfaces implementadas por System.IntPtr y System.UIntPtrse incluyen implícitamente en nint y nuint, con repeticiones de los tipos subyacentes reemplazados por los tipos enteros nativos correspondientes. Por ejemplo, si IntPtr implementa ISerializable, IEquatable<IntPtr>, IComparable<IntPtr>, nint implementará ISerializable, IEquatable<nint>, IComparable<nint>.

Invalidación, ocultación e implementación

nint y System.IntPtr, y nuint y System.UIntPtr, se consideran equivalentes para invalidar, ocultar e implementar.

No se puede diferenciar las sobrecargas solo por nint y System.IntPtr, nuint y System.UIntPtr. Las invalidaciones e implementaciones pueden diferir por nint y System.IntPtr, o bien nuint y System.UIntPtr, solamente. Los métodos ocultan solo otros métodos que difieren únicamente por nint y System.IntPtr, o por nuint y System.UIntPtr.

Varios

Las expresiones nint y nuint usadas como índices de matriz se emiten sin conversión.

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

nint y nuint no se pueden usar como un tipo base enum en C#.

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

Las lecturas y escrituras son atómicas para nint y nuint.

Los campos pueden ser marcados volatile para los tipos nint y nuint. ECMA-334 15.5.4 no incluye enum con tipo base System.IntPtr ni System.UIntPtr.

default(nint) y new nint() son equivalentes a (nint)0; default(nuint) y new nuint() son equivalentes a (nuint)0.

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

sizeof(nint) y sizeof(nuint) se admiten, pero requieren compilación en un contexto no seguro, como se requiere para sizeof(IntPtr) y sizeof(UIntPtr). Los valores no son constantes en tiempo de compilación. sizeof(nint) se implementa como sizeof(IntPtr) en vez de IntPtr.Size; sizeof(nuint) se implementa como sizeof(UIntPtr) en vez de UIntPtr.Size.

Diagnósticos del compilador para referencias de tipos que involucran nint o nuint notifican nint o nuint en lugar de IntPtr o UIntPtr.

Metadatos

nint y nuint se representan en metadatos como System.IntPtr y System.UIntPtr.

Las referencias de tipo que incluyen nint o nuint se emiten con un System.Runtime.CompilerServices.NativeIntegerAttribute para indicar qué partes de la referencia de tipo son enteros nativos.

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 codificación de referencias de tipo con NativeIntegerAttribute se trata en NativeIntegerAttribute.md.

Alternativas

Una alternativa al método de "borrado de tipos" anterior sería introducir nuevos tipos: System.NativeInt y System.NativeUInt.

public readonly struct NativeInt
{
    public IntPtr Value;
}

Los tipos distintos permitirían sobrecargas distintas de IntPtr y permitirían un análisis sintáctico diferente y ToString(). Sin embargo, implicaría más trabajo para que CLR controlara estos tipos de forma eficaz, lo que descartaría la finalidad principal de la funcionalidad, que es la eficiencia. Y la interoperabilidad con el código int nativo existente que usa IntPtr será más difícil.

Otra alternativa sería integrar más compatibilidad nativa con int en IntPtr en el marco de trabajo, pero sin ninguna compatibilidad específica con el compilador. El compilador admitiría automáticamente las nuevas conversiones y operaciones aritméticas. No obstante, el lenguaje no facilitaría palabras clave, constantes ni operaciones checked.

Reuniones de diseño