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 yconv.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.*
parasigned to *
-
conv.ovf.*.un
paraunsigned to *
-
- los contextos
unchecked
para ampliación son:-
conv.i*
ensigned to *
(donde * es el ancho de destino) -
conv.u*
enunsigned to *
(donde * es el ancho de destino)
-
- los contextos
unchecked
para restricción son:-
conv.i*
enany to signed *
(donde * es el ancho de destino) -
conv.u*
enany to unsigned *
(donde * es el ancho de destino)
-
Tomemos algunos ejemplos:
sbyte to nint
ysbyte to nuint
usanconv.i
mientras quebyte to nint
ybyte to nuint
usanconv.u
porque todos son de ampliación.-
nint to byte
ynuint to byte
usanconv.u1
, mientras quenint to sbyte
ynuint to sbyte
usanconv.i1
. Parabyte
,sbyte
,short
yushort
el "tipo de pila" esint32
. Por lo tanto,conv.i1
se "convierte a un byte firmado y luego se extiende hasta int32", mientras queconv.u1
se "convierte a un byte sin signo y luego se extiende con ceros hasta int32". -
checked void* to nint
usaconv.ovf.i.un
de la misma manera quechecked void* to long
usaconv.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
aB
. - conversión explícita que admite valores NULL si existe una conversión explícita de
A
aB
; - 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
aB
; - 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
aB
; - conversión explícita que admite valores NULL si hay una conversión numérica implícita o explícita de
A
aB
; - 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 tipoulong
, o bien, se producirá un error de vinculación si el otro operando es del tiposbyte
,short
,int
,nint
olong
.- De lo contrario, si cualquiera de los operandos es del tipo
nuint
, el otro operando se convierte al tiponuint
, o bien, se producirá un error de vinculación si el otro operando es del tiposbyte
,short
,int
,nint
olong
.- De lo contrario, si cualquiera de los operandos es del tipo
long
, el otro operando se convierte al tipolong
.- En caso contrario, si uno de los operandos es del tipo
uint
y el otro es del tiposbyte
,short
,nint
, oint
, ambos operandos se convertirán y pasarán al tipolong
.- De lo contrario, si cualquiera de los operandos es del tipo
uint
, el otro operando se convierte al tipouint
.- De lo contrario, si cualquiera de los operandos es del tipo
nint
, , el otro operando se convierte al tiponint
.- 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.UIntPtr
está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.UIntPtr
se 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.UIntPtr
se 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
- https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-05-26.md
- https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-06-13.md
- https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-07-05.md#native-int-and-intptr-operators
- https://github.com/dotnet/csharplang/blob/master/meetings/2019/LDM-2019-10-23.md
- https://github.com/dotnet/csharplang/blob/master/meetings/2020/LDM-2020-03-25.md
C# feature specifications