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.MinValue
wartoś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ą, aconv.i
jest konwersją rozszerzającą znak na natywną liczbę całkowitą. -
checked
konteksty dla rozszerzania i zawężania:-
conv.ovf.*
dlasigned to *
-
conv.ovf.*.un
dlaunsigned to *
-
-
unchecked
konteksty rozszerzania są następujące:-
conv.i*
dlasigned to *
(gdzie * jest szerokością docelową) -
conv.u*
dlaunsigned to *
(gdzie * jest szerokością docelową)
-
-
unchecked
konteksty dla zawężania to są:-
conv.i*
dlaany to signed *
(gdzie * jest szerokością docelową) -
conv.u*
dlaany to unsigned *
(gdzie * jest szerokością docelową)
-
Kilka przykładów:
-
sbyte to nint
isbyte to nuint
używająconv.i
, podczas gdybyte to nint
ibyte to nuint
używająconv.u
, ponieważ wszystkie rozszerzają. -
nint to byte
inuint to byte
używająconv.u1
, anint to sbyte
inuint to sbyte
używająconv.i1
. W przypadkubyte
,sbyte
,short
iushort
typ stosu jestint32
. Dlategoconv.i1
jest skutecznie "rzutowany w dół na bajt ze znakiem, a następnie rozszerzony do int32 ze znakiem", podczas gdyconv.u1
jest skutecznie "rzutowany w dół na bajt bez znaku, a następnie rozszerzony do int32 bez znaku". -
checked void* to nint
używaconv.ovf.i.un
w taki sam sposób, w jakichecked void* to long
używaconv.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
doB
; - jawna konwersja do wartości null, jeśli istnieje jawna konwersja z
A
naB
; - 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
naB
; - 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
naB
; - Jawna konwersja na wartość null, jeżeli istnieje niejawna lub jawna konwersja liczbowa z
A
naB
; - 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 typulong
, albo występuje błąd czasu powiązania, jeśli inny operand jest typusbyte
,short
,int
,nint
lublong
.- W przeciwnym razie, jeśli którykolwiek z operandów ma typ
nuint
, drugi operand jest konwertowany na typnuint
, lub pojawia się błąd związany z czasem powiązania, jeśli drugi operand jest typusbyte
,short
,int
,nint
lublong
.- W przeciwnym razie jeśli którykolwiek operand ma typ
long
, drugi operand jest konwertowany na typlong
.- W przeciwnym razie jeśli operand ma typ
uint
, a drugi operand ma typsbyte
,short
,nint
, lubint
, oba operandy są konwertowane na typlong
.- W przeciwnym razie jeśli którykolwiek operand ma typ
uint
, drugi operand jest konwertowany na typuint
.- W przeciwnym razie, jeśli którykolwiek operand ma typ
nint
, drugi operand jest konwertowany na typnint
.- 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.UIntPtr
są 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.UIntPtr
są 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.UIntPtr
są 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.IntPtr
oraz nuint
i System.UIntPtr
są 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.IntPtr
oraz nuint
i System.UIntPtr
.
Przesłonięcia i implementacje mogą różnić się samodzielnie między nint
i System.IntPtr
lub 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
- 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