Partilhar via


Inteiros de tamanho nativo

Observação

Este artigo é uma especificação de recurso. A especificação serve como o documento de design para o recurso. Ele inclui mudanças de especificação propostas, juntamente com as informações necessárias durante o design e desenvolvimento do recurso. Estes artigos são publicados até que as alterações de especificações propostas sejam finalizadas e incorporadas na especificação ECMA atual.

Pode haver algumas discrepâncias entre a especificação do recurso e a implementação concluída. Essas diferenças são capturadas nas notas pertinentes da Language Design Meeting (LDM).

Você pode saber mais sobre o processo de adoção de especificações de recursos no padrão de linguagem C# no artigo sobre as especificações .

Resumo

Suporte para tipos de inteiros assinados e não assinados de tamanho nativo em linguagens de programação.

A motivação é para cenários de interoperabilidade e para bibliotecas de baixo nível.

Desenho

Os identificadores nint e nuint são novas palavras-chave contextuais que representam tipos inteiros nativos assinados e não assinados. Os identificadores só são tratados como palavras-chave quando a pesquisa de nomes não encontra um resultado viável nesse local do programa.

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

Os tipos nint e nuint são representados pelos tipos subjacentes System.IntPtr e System.UIntPtr com o compilador apresentando conversões e operações adicionais para esses tipos como ints nativos.

Constantes

As expressões constantes podem ser do tipo nint ou nuint. Não há uma sintaxe direta para literais do tipo inteiro nativo (int). Em vez disso, podem ser usados moldes implícitos ou explícitos de outros valores constantes integrais: const nint i = (nint)42;.

As constantes nint estão no intervalo [ int.MinValue, int.MaxValue ].

As constantes nuint estão no intervalo [ uint.MinValue, uint.MaxValue ].

Não há campos MinValue ou MaxValue em nint ou nuint porque, além de nuint.MinValue, esses valores não podem ser emitidos como constantes.

O dobramento constante é suportado para todos os operadores unários { +, -, ~ } e operadores binários { +, -, *, /, %, ==, !=, <, <=, >, >=, &, |, ^, <<, >> }. As operações de dobramento constante são avaliadas com operandos Int32 e UInt32 em vez de ints nativos para um comportamento consistente, independentemente da plataforma do compilador. Se a operação resultar em um valor constante em 32 bits, o dobramento constante será executado em tempo de compilação. Caso contrário, a operação é executada em tempo de execução e não é considerada uma constante.

Conversões

Há uma conversão de identidade entre nint e IntPtr, e entre nuint e UIntPtr. Há uma conversão de identidade entre tipos compostos que diferem apenas por inteiros nativos e tipos subjacentes: matrizes, Nullable<>, tuplas e tipos construídos.

As tabelas abaixo abrangem as conversões entre tipos especiais. (A IL para cada conversão inclui as variantes para os contextos unchecked e checked, se diferentes.)

Notas gerais sobre o quadro abaixo:

  • conv.u é uma conversão com extensão de zero para inteiro nativo e conv.i é uma conversão com extensão de sinal para inteiro nativo.
  • checked contextos para alargamento e estreitamento são:
    • conv.ovf.* para signed to *
    • conv.ovf.*.un para unsigned to *
  • unchecked contextos para alargamento são:
    • conv.i* para signed to * (onde * é a largura de destino)
    • conv.u* para unsigned to * (onde * é a largura de destino)
  • unchecked contextos para estreitamento são:
    • conv.i* para any to signed * (onde * é a largura de destino)
    • conv.u* para any to unsigned * (onde * é a largura de destino)

Dando alguns exemplos:

  • sbyte to nint e sbyte to nuint usam conv.i enquanto byte to nint e byte to nuint usam conv.u porque são todos alargamento.
  • nint to byte e nuint to byte usam conv.u1 enquanto nint to sbyte e nuint to sbyte usam conv.i1. Para byte, sbyte, shorte ushort, o "tipo de pilha" é int32. Assim, conv.i1 é efetivamente "downcast para um byte assinado e, em seguida, sign-extend até int32", enquanto conv.u1 é efetivamente "downcast para um byte não assinado e, em seguida, zero-extend até int32".
  • checked void* to nint usa conv.ovf.i.un da mesma forma que checked void* to long usa conv.ovf.i8.un.
Operando Público-alvo Conversão IL
object nint Unboxing 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 Identidade
UIntPtr nint Nenhum
object nuint Unboxing unbox
void* nuint ApontadorParaVoid Não.
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 Nenhum
UIntPtr nuint Identidade
Enumeração nint Enumeração Explícita
Enumeração nuint Enumeração Explícita
Operando Público-alvo Conversão IL
nint object Boxe box
nint void* ApontadorParaVoid Nop / conv.ovf.u
nint nuint ExplicitNumeric conv.u (pode ser omitido) / 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 Identidade
nint UIntPtr Nenhum
nint Enumeração Enumeração Explícita
nuint object Boxe box
nuint void* PointerToVoid Não.
nuint nint ExplicitNumeric conv.i(pode ser omitido) / 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 Nenhum
nuint UIntPtr Identidade
nuint Enumeração Enumeração Explícita

A conversão de A para Nullable<B> é:

  • uma conversão implícita passível de nulidade se houver uma conversão de identidade ou uma conversão implícita de A para B;
  • uma conversão anulável explícita se houver uma conversão explícita de A para B;
  • caso contrário, inválido.

A conversão de Nullable<A> para B é:

  • uma conversão explícita anulável se houver uma conversão de identidade ou uma conversão numérica implícita ou explícita de A para B;
  • caso contrário, inválido.

A conversão de Nullable<A> para Nullable<B> é:

  • uma conversão de identidade, se houver uma conversão de identidade de A para B;
  • uma conversão anulável explícita se houver uma conversão numérica implícita ou explícita de A para B;
  • caso contrário, inválido.

Operadores

Os operadores predefinidos são os seguintes. Esses operadores são considerados durante a resolução de sobrecarga com base em regras normais para conversões implícitas se pelo menos um dos operandos for do tipo nint ou nuint.

(O IL para cada operador inclui as variantes para os contextos unchecked e checked, se diferentes.)

Unário Assinatura do 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
Binário Assinatura do operador Illinois
+ 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 alguns operadores binários, os operadores IL suportam tipos de operando adicionais (ver tabela de tipos de operando ECMA-335 III.1.5). Mas o conjunto de tipos de operando suportados pelo C# é limitado para simplicidade e consistência com operadores existentes na linguagem.

Versões elevadas dos operadores, onde os argumentos e os tipos de retorno são nint? e nuint?, são suportadas.

As operações de atribuição composta x op= y em que x ou y são ints nativos seguem as mesmas regras que com outros tipos primitivos com operadores predefinidos. Especificamente, a expressão está ligada como x = (T)(x op y) onde T é o tipo de x e onde x é avaliada apenas uma vez.

Os operadores de deslocamento devem mascarar o número de bits em deslocamento - para 5 bits se sizeof(nint) for 4 e para 6 bits se sizeof(nint) for 8. (ver §12.11) em C# spec).

O compilador C#9 relatará erros de vinculação a operadores inteiros nativos predefinidos ao compilar com uma versão de idioma anterior, mas permitirá o uso de conversões predefinidas de e para inteiros 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 do ponteiro

Não há operadores predefinidos em C# para adição ou subtração de ponteiro com deslocamentos inteiros nativos. Em vez disso, os valores nint e nuint são promovidos para long e ulong e a aritmética de ponteiro usa operadores predefinidos para esses 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)

Promoções numéricas binárias

O promoções numéricas binárias texto informativo (ver §12.4.7.3) em C# spec) é atualizado da seguinte forma:

  • Caso contrário, se um operando for do tipo ulong, o outro operando será convertido para o tipo ulong, ou ocorrerá um erro de tempo de ligação se o outro operando for do tipo sbyte, short, int, nintou long.
  • Caso contrário, se um operando for do tipo nuint, o outro operando será convertido em tipo nuint, ou ocorrerá um erro de tempo de ligação se o outro operando for do tipo sbyte, short, int, nintou long.
  • Caso contrário, se um operando for do tipo long, o outro operando será convertido em tipo long.
  • Caso contrário, se um operando for do tipo uint e o outro operando for do tipo sbyte, short, nint, ou int, ambos os operandos serão convertidos para o tipo long.
  • Caso contrário, se um operando for do tipo uint, o outro operando será convertido em tipo uint.
  • Caso contrário, se um operando for do tipo nint, o outro operando será convertido em tipo nint.
  • Caso contrário, ambos os operandos são convertidos para o tipo int.

Dinâmica

As conversões e operadores são sintetizados pelo compilador e não fazem parte dos tipos de IntPtr e UIntPtr subjacentes. Como resultado, essas conversões e operadores não estão disponíveis do fichário de tempo de execução 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'

Membros do tipo

O único construtor para nint ou nuint é o construtor sem parâmetros.

Os seguintes membros da System.IntPtr e System.UIntPtrestão explicitamente excluídos da nint ou 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();

Os membros restantes de System.IntPtr e System.UIntPtrestão implicitamente incluídos em no nint e nuint. Para o .NET Framework 4.7.2:

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

As interfaces implementadas por System.IntPtr e System.UIntPtrsão implicitamente incluídas em nint e nuint, com ocorrências dos tipos subjacentes substituídas pelos tipos inteiros nativos correspondentes. Por exemplo, se IntPtr implementa ISerializable, IEquatable<IntPtr>, IComparable<IntPtr>, então nint implementa ISerializable, IEquatable<nint>, IComparable<nint>.

Substituir, ocultar e implementar

nint e System.IntPtr, assim como nuint e System.UIntPtr, são considerados equivalentes para substituição, ocultação e implementação.

As sobrecargas não podem diferir apenas por nint e System.IntPtr, e nuint e System.UIntPtr. Substituições e implementações podem diferir por nint e System.IntPtr, ou nuint e System.UIntPtr, sozinhos. Os métodos ocultam outros métodos que diferem apenas por nint e System.IntPtr, ou nuint e System.UIntPtr.

Diversos

nint e nuint expressões usadas como índices de matriz são emitidas sem conversão.

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

nint e nuint não podem ser usados como um tipo base enum do C#.

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

Leituras e gravações são atômicas para nint e nuint.

Os campos podem ser marcados volatile para os tipos nint e nuint. No entanto, ECMA-334 15.5.4 não inclui enum com tipo base System.IntPtr ou System.UIntPtr.

default(nint) e new nint() são equivalentes a (nint)0; default(nuint) e new nuint() são equivalentes a (nuint)0.

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

sizeof(nint) e sizeof(nuint) são suportados, mas requerem compilação em um contexto inseguro (conforme necessário para sizeof(IntPtr) e sizeof(UIntPtr)). Os valores não são constantes de tempo de compilação. sizeof(nint) é implementado como sizeof(IntPtr) e não como IntPtr.Size; sizeof(nuint) é implementado como sizeof(UIntPtr) e não como UIntPtr.Size.

Diagnósticos do compilador para referências de tipo envolvendo nint ou nuint relatam nint ou nuint em vez de IntPtr ou UIntPtr.

Metadados

nint e nuint são representados em metadados como System.IntPtr e System.UIntPtr.

As referências de tipo que incluem nint ou nuint são emitidas com um System.Runtime.CompilerServices.NativeIntegerAttribute para indicar quais partes da referência de tipo são inteiros 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;
    }
}

A codificação de referências de tipo com NativeIntegerAttribute é abordada em NativeIntegerAttribute.md.

Alternativas

Uma alternativa à abordagem de "eliminação de tipos" acima é introduzir novos tipos: System.NativeInt e System.NativeUInt.

public readonly struct NativeInt
{
    public IntPtr Value;
}

Tipos distintos permitiriam sobrecarga distinta de IntPtr e permitiriam uma análise sintática distinta de ToString(). Mas haveria mais trabalho para o CLR lidar com esses tipos de forma eficiente, o que derrota o objetivo principal do recurso - a eficiência. E a interoperabilidade com o código int nativo existente que usa IntPtr seria mais difícil.

Outra alternativa é adicionar mais suporte int nativo para IntPtr no framework, mas sem qualquer suporte específico ao compilador. Quaisquer novas conversões e operações aritméticas seriam suportadas pelo compilador automaticamente. Mas a linguagem não forneceria palavras-chave, constantes ou operações checked.

Reuniões de design