Udostępnij za pośrednictwem


Liczby całkowite o rozmiarze natywnym

Notatka

Ten artykuł jest specyfikacją funkcji. Specyfikacja służy jako dokument projektowy dla funkcji. Zawiera proponowane zmiany specyfikacji wraz z informacjami wymaganymi podczas projektowania i opracowywania funkcji. Te artykuły są publikowane do momentu sfinalizowania proponowanych zmian specyfikacji i włączenia ich do obecnej specyfikacji ECMA.

Mogą wystąpić pewne rozbieżności między specyfikacją funkcji a ukończoną implementacją. Te różnice są przechwytywane w odpowiednich spotkania projektowego języka (LDM).

Więcej informacji na temat procesu wdrażania specyfikacji funkcji można znaleźć w standardzie języka C# w artykule dotyczącym specyfikacji .

Problem z czempionem: https://github.com/dotnet/csharplang/issues/435

Streszczenie

Obsługa języka dla typów liczb całkowitych o rozmiarze natywnym i bez znaku.

Motywacją są scenariusze interoperacyjne i biblioteki niskopoziomowe.

Projektowanie

Identyfikatory nint i nuint to nowe kontekstowe słowa kluczowe reprezentujące natywne typy liczb całkowitych ze znakiem natywnym i bez znaku. Identyfikatory są traktowane tylko jako słowa kluczowe, gdy wyszukiwanie nazw nie znajdzie realnego wyniku w tej lokalizacji programu.

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

Typy nint i nuint są reprezentowane przez podstawowe typy System.IntPtr i System.UIntPtr, przy czym kompilator automatycznie zapewnia dodatkowe konwersje i operacje dla tych typów jako natywne liczby całkowite.

Stałe

Wyrażenia stałe mogą być typu nint lub nuint. Nie ma bezpośredniej składni dla natywnych literałów int. Zamiast tego można użyć niejawnych lub jawnych rzutów innych wartości stałych całkowitych: const nint i = (nint)42;.

Stała nint znajduje się w zakresie [int.MinValue, int.MaxValue].

Stała nuint znajduje się w zakresie [uint.MinValue, uint.MaxValue].

W MinValue lub MaxValue nie ma żadnych pól nint ani nuint, ponieważ inne niż nuint.MinValuewartości te nie mogą być emitowane jako stałe.

Stałe składanie jest obsługiwane dla wszystkich operatorów jednoargumentowych { +, -, ~ } i operatorów binarnych { +, -, *, /, %, ==, !=, <, <=, >, >=, &, |, ^, <<, >> }. Stałe operacje składania są oceniane przy użyciu Int32 i UInt32 operandów, a nie natywnych ints pod kątem spójnego zachowania niezależnie od platformy kompilatora. Jeśli operacja powoduje stałą wartość w 32-bitach, składanie stałe jest wykonywane w czasie kompilacji. W przeciwnym razie operacja jest wykonywana podczas działania programu i nie jest uznawana za stałą.

Konwersje

Istnieje konwersja tożsamości zarówno między nint a IntPtr, jak i między nuint a UIntPtr. Istnieje konwersja tożsamości między typami złożonymi, które różnią się jedynie natywnymi liczbami całkowitymi i typami bazowymi: tablice, Nullable<>, typy skonstruowane i krotki.

W poniższych tabelach omówiono konwersje między typami specjalnymi. (Il dla każdej konwersji obejmuje warianty dla kontekstów unchecked i checked, jeśli są różne).

Uwagi ogólne w poniższej tabeli:

  • conv.u jest konwersją rozszerzającą zero na natywną liczbę całkowitą, a conv.i jest konwersją rozszerzającą znak na natywną liczbę całkowitą.
  • checked konteksty dla rozszerzania i zawężania:
    • conv.ovf.* dla signed to *
    • conv.ovf.*.un dla unsigned to *
  • unchecked konteksty rozszerzania są następujące:
    • conv.i* dla signed to * (gdzie * jest szerokością docelową)
    • conv.u* dla unsigned to * (gdzie * jest szerokością docelową)
  • unchecked konteksty dla zawężania to są:
    • conv.i* dla any to signed * (gdzie * jest szerokością docelową)
    • conv.u* dla any to unsigned * (gdzie * jest szerokością docelową)

Kilka przykładów:

  • sbyte to nint i sbyte to nuint używają conv.i, podczas gdy byte to nint i byte to nuint używają conv.u, ponieważ wszystkie rozszerzają.
  • nint to byte i nuint to byte używają conv.u1, a nint to sbyte i nuint to sbyte używają conv.i1. W przypadku byte, sbyte, shorti ushort typ stosu jest int32. Dlatego conv.i1 jest skutecznie "rzutowany w dół na bajt ze znakiem, a następnie rozszerzony do int32 ze znakiem", podczas gdy conv.u1 jest skutecznie "rzutowany w dół na bajt bez znaku, a następnie rozszerzony do int32 bez znaku".
  • checked void* to nint używa conv.ovf.i.un w taki sam sposób, w jaki checked void* to long używa conv.ovf.i8.un.
Operand Cel Konwersja IL
object nint Rozpakowywanie unbox
void* nint WskaźnikToVoid nop / conv.ovf.i.un
sbyte nint Niejawnenumeryczne conv.i
byte nint Niejawnenumeryczne conv.u
short nint Niejawnenumeryczne conv.i
ushort nint Niejawnenumeryczne conv.u
int nint Niejawnenumeryczne conv.i
uint nint JawnaLiczba conv.u / conv.ovf.i.un
long nint JawnaLiczba conv.i / conv.ovf.i
ulong nint JawnaLiczba conv.i / conv.ovf.i.un
char nint Niejawnenumeryczne conv.u
float nint JawnaLiczba conv.i / conv.ovf.i
double nint JawnaLiczba conv.i / conv.ovf.i
decimal nint JawnaLiczba long decimal.op_Explicit(decimal) conv.i / ... conv.ovf.i
IntPtr nint Tożsamość
UIntPtr nint Żaden
object nuint Rozpakowywanie unbox
void* nuint WskaźnikToVoid Nop
sbyte nuint JawnaLiczba conv.i / conv.ovf.u
byte nuint Niejawnenumeryczne conv.u
short nuint JawnaLiczba conv.i / conv.ovf.u
ushort nuint Niejawnenumeryczne conv.u
int nuint JawnaLiczba conv.i / conv.ovf.u
uint nuint Niejawnenumeryczne conv.u
long nuint JawnaLiczba conv.u / conv.ovf.u
ulong nuint JawnaLiczba conv.u / conv.ovf.u.un
char nuint Niejawnenumeryczne conv.u
float nuint JawnaLiczba conv.u / conv.ovf.u
double nuint JawnaLiczba conv.u / conv.ovf.u
decimal nuint JawnaLiczba ulong decimal.op_Explicit(decimal) conv.u / ... conv.ovf.u.un
IntPtr nuint Żaden
UIntPtr nuint Tożsamość
Wyliczenie nint Enumeracja jawna
Wyliczenie nuint Enumeracja jawna
Operand Cel Konwersja IL
nint object Boks box
nint void* WskaźnikToVoid nop / conv.ovf.u
nint nuint JawnaLiczba conv.u (można pominąć) / conv.ovf.u
nint sbyte JawnaLiczba conv.i1 / conv.ovf.i1
nint byte JawnaLiczba conv.u1 / conv.ovf.u1
nint short JawnaLiczba conv.i2 / conv.ovf.i2
nint ushort JawnaLiczba conv.u2 / conv.ovf.u2
nint int JawnaLiczba conv.i4 / conv.ovf.i4
nint uint JawnaLiczba conv.u4 / conv.ovf.u4
nint long Niejawnenumeryczne conv.i8
nint ulong JawnaLiczba conv.i8 / conv.ovf.u8
nint char JawnaLiczba conv.u2 / conv.ovf.u2
nint float Niejawnenumeryczne conv.r4
nint double Niejawnenumeryczne conv.r8
nint decimal Niejawnenumeryczne conv.i8 decimal decimal.op_Implicit(long)
nint IntPtr Tożsamość
nint UIntPtr Żaden
nint Wyliczenie Enumeracja jawna
nuint object Boks box
nuint void* WskaźnikToVoid Nop
nuint nint JawnaLiczba conv.i(można pominąć) / conv.ovf.i.un
nuint sbyte JawnaLiczba conv.i1 / conv.ovf.i1.un
nuint byte JawnaLiczba conv.u1 / conv.ovf.u1.un
nuint short JawnaLiczba conv.i2 / conv.ovf.i2.un
nuint ushort JawnaLiczba conv.u2 / conv.ovf.u2.un
nuint int JawnaLiczba conv.i4 / conv.ovf.i4.un
nuint uint JawnaLiczba conv.u4 / conv.ovf.u4.un
nuint long JawnaLiczba conv.u8 / conv.ovf.i8.un
nuint ulong Niejawnenumeryczne conv.u8
nuint char JawnaLiczba conv.u2 / conv.ovf.u2.un
nuint float Niejawnenumeryczne conv.r.un conv.r4
nuint double Niejawnenumeryczne conv.r.un conv.r8
nuint decimal Niejawnenumeryczne conv.u8 decimal decimal.op_Implicit(ulong)
nuint IntPtr Żaden
nuint UIntPtr Tożsamość
nuint Wyliczenie Enumeracja jawna

Konwersja z A na Nullable<B> to:

  • niejawna konwersja na typ dopuszczający wartość null, jeśli istnieje konwersja tożsamości lub niejawna konwersja z A do B;
  • jawna konwersja do wartości null, jeśli istnieje jawna konwersja z A na B;
  • w przeciwnym razie jest to nieprawidłowe.

Konwersja z Nullable<A> na B to:

  • jawna konwersja do typu akceptującego wartość null, jeśli istnieje konwersja tożsamości lub niejawna lub jawna konwersja liczbowa z A na B;
  • w przeciwnym razie jest to nieprawidłowe.

Konwersja z Nullable<A> na Nullable<B> to:

  • konwersja tożsamości w przypadku konwersji tożsamości z A na B;
  • Jawna konwersja na wartość null, jeżeli istnieje niejawna lub jawna konwersja liczbowa z A na B;
  • w przeciwnym razie jest to nieprawidłowe.

Operatorów

Wstępnie zdefiniowane operatory są następujące. Te operatory są brane pod uwagę podczas rozpoznawania przeciążeń na podstawie normalnych reguł konwersji niejawnych , jeśli co najmniej jeden z operandów jest typu nint lub nuint.

(Il dla każdego operatora zawiera warianty dla unchecked i kontekstów checked, jeśli są inne).

Jednoargumentowy Podpis operatora 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
Dwójkowy Podpis operatora 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

W przypadku niektórych operatorów binarnych operatory IL wspierają dodatkowe typy operandów (zobacz ECMA-335 III.1.5 Tabela typów operandów). Jednak zestaw typów operandów obsługiwanych przez język C# jest ograniczony dla uproszczenia i spójności z istniejącymi operatorami w języku.

Podnoszone wersje operatorów, gdzie typy argumentów i zwracanych danych to nint? i nuint?, są obsługiwane.

Operacje przypisania złożonego x op= y, w których x lub y są natywnymi typami int, podlegają tym samym regułom co inne typy pierwotne ze wstępnie zdefiniowanymi operatorami. W szczególności wyrażenie jest powiązane jako x = (T)(x op y), gdzie T jest typem x i gdzie x jest obliczany tylko raz.

Operatory przesunięcia powinny maskować liczbę bitów do przesunięcia - do 5 bitów, jeśli sizeof(nint) wynosi 4, i do 6 bitów, jeśli sizeof(nint) wynosi 8. (patrz §12.11) w specyfikacji C#).

Kompilator języka C#9 będzie zgłaszać błędy powiązania ze wstępnie zdefiniowanymi operatorami liczb całkowitych natywnych podczas kompilowania przy użyciu starszej wersji języka, ale umożliwi użycie wstępnie zdefiniowanych konwersji do i z natywnych liczb całkowitych.

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

Arytmetyka wskaźnika

Nie ma wstępnie zdefiniowanych operatorów w języku C# do dodawania lub odejmowania wskaźników z natywnymi przesunięciami liczb całkowitych. Zamiast tego wartości nint i nuint są promowane do long i ulong, a arytmetyka wskaźnika używa wstępnie zdefiniowanych operatorów dla tych typów.

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)

Promocje numeryczne binarne

binarne promocje liczbowe tekst informacyjny (zobacz §12.4.7.3) w specyfikacji C#) jest aktualizowany w następujący sposób:

  • W przeciwnym razie, jeśli którykolwiek z operandów ma typ ulong, drugi operand jest konwertowany na typ ulong, albo występuje błąd czasu powiązania, jeśli inny operand jest typu sbyte, short, int, nintlub long.
  • W przeciwnym razie, jeśli którykolwiek z operandów ma typ nuint, drugi operand jest konwertowany na typ nuint, lub pojawia się błąd związany z czasem powiązania, jeśli drugi operand jest typu sbyte, short, int, nintlub long.
  • W przeciwnym razie jeśli którykolwiek operand ma typ long, drugi operand jest konwertowany na typ long.
  • W przeciwnym razie jeśli operand ma typ uint, a drugi operand ma typ sbyte, short, nint, lub int, oba operandy są konwertowane na typ long.
  • W przeciwnym razie jeśli którykolwiek operand ma typ uint, drugi operand jest konwertowany na typ uint.
  • W przeciwnym razie, jeśli którykolwiek operand ma typ nint, drugi operand jest konwertowany na typ nint.
  • W przeciwnym razie oba operandy są konwertowane na typ int.

Dynamiczny

Konwersje i operatory są syntetyzowane przez kompilator i nie są częścią podstawowych typów IntPtr i UIntPtr. W rezultacie te konwersje i operatory nie są dostępne z powiązania środowiska uruchomieniowego dla 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'

Składowe typu

Jedynym konstruktorem nint lub nuint jest konstruktor bez parametrów.

Następujący członkowie System.IntPtr i System.UIntPtrsą jawnie wykluczeni z nint lub 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();

Pozostali członkowie System.IntPtr i System.UIntPtrsą domyślnie uwzględniani w nint i nuint. W przypadku programu .NET Framework 4.7.2:

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

Interfejsy implementowane przez System.IntPtr i System.UIntPtrsą niejawnie uwzględniane w nint i nuint, zastępując wystąpienia typów bazowych odpowiednimi natywnymi typami liczbowymi. Jeśli na przykład IntPtr implementuje ISerializable, IEquatable<IntPtr>, IComparable<IntPtr>, nint implementuje ISerializable, IEquatable<nint>, IComparable<nint>.

Zastępowanie, ukrywanie i implementowanie

nint i System.IntPtroraz nuint i System.UIntPtrsą uważane za równoważne w kontekście zastępowania, ukrywania i implementacji.

Przeciążenia nie mogą różnić się jedynie przez nint i System.IntPtroraz nuint i System.UIntPtr. Przesłonięcia i implementacje mogą różnić się samodzielnie między nint i System.IntPtrlub nuint i System.UIntPtr. Metody ukrywają inne metody, które różnią się wyłącznie według nint i System.IntPtr, lub nuint i System.UIntPtr.

Różne

nint i wyrażenia nuint używane jako indeksy tablicowe są emitowane bez konwersji.

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

nint i nuint nie można używać jako typu podstawowego enum z języka C#.

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

Operacje odczytu i zapisu są atomowe dla nint i nuint.

Pola mogą być oznaczone volatile dla typów nint i nuint. ECMA-334 15.5.4 nie obejmuje jednak enum z bazowym typem System.IntPtr lub System.UIntPtr.

default(nint) i new nint() są równoważne (nint)0; default(nuint) i new nuint() są równoważne (nuint)0.

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

sizeof(nint) i sizeof(nuint) są obsługiwane, ale wymagają kompilowania w niebezpiecznym kontekście (zgodnie z wymaganiami sizeof(IntPtr) i sizeof(UIntPtr)). Wartości nie są stałymi czasu kompilacji. sizeof(nint) jest implementowany jako sizeof(IntPtr), a nie IntPtr.Size; sizeof(nuint) jest implementowany jako sizeof(UIntPtr), a nie UIntPtr.Size.

Diagnostyka kompilatora dotycząca odwołań do typów, które obejmują nint lub nuint, raportują nint lub nuint, a nie IntPtr lub UIntPtr.

Metadane

nint i nuint są reprezentowane w metadanych jako System.IntPtr i System.UIntPtr.

Odwołania do typów, które obejmują nint lub nuint, są emitowane z System.Runtime.CompilerServices.NativeIntegerAttribute, aby wskazać, które części odwołania typu są natywne jako liczby całkowite.

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

Kodowanie referencji typów za pomocą NativeIntegerAttribute zostało omówione w NativeIntegerAttribute.md.

Alternatywy

Alternatywą dla powyższego podejścia "wymazywania typów" jest wprowadzenie nowych typów: System.NativeInt i System.NativeUInt.

public readonly struct NativeInt
{
    public IntPtr Value;
}

Różne typy umożliwiałyby przeciążenie odrębne od IntPtr i umożliwiałyby również odrębną analizę ToString(). Ale CLR musiałby wykonać więcej pracy, aby efektywnie obsługiwać te typy, co niweczy podstawowy cel tej funkcji - wydajność. I współdziałanie z istniejącym natywnym kodem int, który używa IntPtr byłoby trudniejsze.

Inną alternatywą jest dodanie większej natywnej obsługi int dla IntPtr w frameworkie, ale bez żadnego konkretnego wsparcia dla kompilatora. Wszelkie nowe konwersje i operacje arytmetyczne będą obsługiwane automatycznie przez kompilator. Jednak język nie dostarczał słów kluczowych, stałych ani operacji checked.

Spotkania projektowe