Freigeben über


Integerwerte mit nativer Größe

Hinweis

Dieser Artikel ist eine Feature-Spezifikation. Die Spezifikation dient als Designdokument für das Feature. Es enthält vorgeschlagene Spezifikationsänderungen sowie Informationen, die während des Entwurfs und der Entwicklung des Features erforderlich sind. Diese Artikel werden veröffentlicht, bis die vorgeschlagenen Spezifikationsänderungen abgeschlossen und in die aktuelle ECMA-Spezifikation aufgenommen werden.

Es kann einige Abweichungen zwischen der Feature-Spezifikation und der abgeschlossenen Implementierung geben. Diese Unterschiede werden in den entsprechenden Hinweisen zum Language Design Meeting (LDM) erfasst.

Weitere Informationen zum Prozess für die Aufnahme von Funktions-Speclets in den C#-Sprachstandard finden Sie im Artikel zu den Spezifikationen.

Champion Issue: https://github.com/dotnet/csharplang/issues/435

Zusammenfassung

Sprachunterstützung für signierte und nicht signierte ganzzahlige Typen in nativer Größe.

Die Motivation ist für Interop-Szenarien und für Low-Level-Bibliotheken.

Design

Die Bezeichner nint und nuint sind neue kontextbezogene Schlüsselwörter, die systemeigene signierte und nicht signierte ganzzahlige Typen darstellen. Die Bezeichner werden nur als Schlüsselwörter behandelt, wenn die Namenssuche an diesem Programmspeicherort kein brauchbares Ergebnis findet.

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

Die Typen nint und nuint werden durch die zugrunde liegenden Typen System.IntPtr und System.UIntPtr dargestellt, wobei der Compiler zusätzliche Konvertierungen und Operationen für diese Typen als native Ints bereitstellt.

Konstanten

Konstantenausdrücke können vom Typ nint oder nuint sein. Es gibt keine direkte Syntax für native Integer-Literale. Implizite oder explizite Umwandlungen anderer integraler Konstantenwerte können stattdessen verwendet werden: const nint i = (nint)42;.

nint Konstanten befinden sich im Bereich [ int.MinValue, int.MaxValue ].

nuint Konstanten befinden sich im Bereich [ uint.MinValue, uint.MaxValue ].

Es gibt keine Felder MinValue oder MaxValue auf nint oder nuint, da diese Werte, mit Ausnahme von nuint.MinValue, nicht als Konstanten ausgegeben werden können.

Die Konstantenfaltung wird für alle unären Operatoren { +, -, ~ } und binären Operatoren { +, -, *, /, %, ==, !=, <, <=, >, >=, &, |, ^, <<, >> } unterstützt. Konstantenfaltungsoperationen werden mit Int32 und UInt32 Operanden statt mit nativen Ints ausgewertet, um ein konsistentes Verhalten unabhängig von der Compilerplattform sicherzustellen. Wenn der Vorgang zu einem Konstantenwert in 32 Bit führt, wird die Konstantenfaltung zur Kompilierungszeit ausgeführt. Andernfalls wird die Operation zur Laufzeit ausgeführt und nicht als Konstante betrachtet.

Konvertierungen

Es gibt eine Identitätskonvertierung zwischen nint und IntPtr und zwischen nuint und UIntPtr. Es gibt eine Identitätsumwandlung zwischen zusammengesetzten Typen, die sich nur durch native Ints und zugrunde liegende Typen unterscheiden: Arrays, Nullable<>, konstruierte Typen und Tupel.

Die folgenden Tabellen enthalten die Konversionen zwischen speziellen Typen. (Die IL für jede Konvertierung enthält die Varianten für die Kontexte unchecked und checked, falls diese unterschiedlich sind.)

Allgemeine Hinweise zur untenstehenden Tabelle:

  • conv.u ist eine Konvertierung mit Nullerweiterung zu einer nativen Ganzzahl, und conv.i ist eine Konvertierung mit Vorzeichenerweiterung zu einer nativen Ganzzahl.
  • checked Kontexte für Erweiterungen und Einschränkungen sind:
    • conv.ovf.* für signed to *
    • conv.ovf.*.un für unsigned to *
  • unchecked Rahmenbedingungen für Erweiterungen sind:
    • conv.i* für signed to * (wobei * die Zielbreite ist)
    • conv.u* für unsigned to * (wobei * die Zielbreite ist)
  • unchecked Kontexte für die Einschränkungen von sind:
    • conv.i* für any to signed * (wobei * die Zielbreite ist)
    • conv.u* für any to unsigned * (wobei * die Zielbreite ist)

Ein paar Beispiele:

  • sbyte to nint und sbyte to nuint verwenden conv.i, während byte to nint und byte to nuint conv.u verwenden, da sie alle erweitern.
  • nint to byte und nuint to byte verwenden conv.u1, während nint to sbyte und nuint to sbyte conv.i1 verwenden. Für byte, sbyte, short und ushort ist der „Stapeltyp” int32. Daher wird conv.i1 effektiv „herabgestuft zu einem vorzeichenbehafteten Byte und dann zu int32 mit Vorzeichen erweitert“, während conv.u1 effektiv „herabgestuft zu einem vorzeichenlosen Byte und dann zu int32 mit Nullen erweitert“ ist.
  • checked void* to nint verwendet conv.ovf.i.un auf die gleiche Weise wie checked void* to longconv.ovf.i8.un.
Operand Ziel Konvertierung BY
object nint Auspacken 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 Identität
UIntPtr nint Keine
object nuint Auspacken 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 Keine
UIntPtr nuint Identität
Enumeration nint ExplicitEnumeration
Enumeration nuint ExplicitEnumeration
Operand Ziel Konvertierung BY
nint object Boxing box
nint void* PointerToVoid nop / conv.ovf.u
nint nuint ExplicitNumeric conv.u (kann weggelassen werden) / 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 Identität
nint UIntPtr Keine
nint Enumeration ExplicitEnumeration
nuint object Boxing box
nuint void* PointerToVoid nop
nuint nint ExplicitNumeric conv.i (kann weggelassen werden) / 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 Keine
nuint UIntPtr Identität
nuint Enumeration ExplicitEnumeration

Die Umwandlung von A in Nullable<B> ist:

  • eine implizite nullable Konvertierung, wenn eine Identitätskonvertierung oder implizite Konvertierung von A in B vorhanden ist;
  • eine explizite nullable Konvertierung, wenn eine explizite Konvertierung von A in B möglich ist;
  • andernfalls ungültig.

Die Umwandlung von Nullable<A> in B ist:

  • eine explizite nullable Konvertierung, wenn eine Identitätskonvertierung oder eine implizite oder explizite numerische Konvertierung von A nach B vorhanden ist;
  • andernfalls ungültig.

Die Umwandlung von Nullable<A> in Nullable<B> ist:

  • eine Identitätskonvertierung, wenn eine Identitätskonvertierung von A in B erfolgt;
  • eine explizite nullfähige Konvertierung, wenn es eine implizite oder explizite numerische Konvertierung von A nach B gibt;
  • andernfalls ungültig.

Operatoren

Die vordefinierten Operatoren sind wie folgt. Diese Operatoren werden basierend auf den normalen Regeln für implizite Konvertierungen bei der Überladungsauflösung berücksichtigt, wenn mindestens einer der Operanden vom Typ nint oder nuint ist.

(Die IL für jeden Operator enthält die Varianten für die Kontexte unchecked und checked, falls diese unterschiedlich sind.)

Unär Signatur des Bedieners BY
+ 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är Signatur des Bedieners BY
+ 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

Für einige binäre Operatoren unterstützen die IL-Operatoren zusätzliche Operandentypen (siehe ECMA-335 III.1.5 Operandentypentabelle). Das Set der von C# unterstützten Operandentypen ist jedoch der Einfachheit halber und aus Gründen der Konsistenz mit den in der Sprache vorhandenen Operatoren begrenzt.

Angehobene Versionen der Operatoren, bei denen die Argumente und Rückgabetypen nint? und nuint?sind, werden unterstützt.

Zusammengesetzte Zuordnungsvorgänge x op= y, bei denen x oder y native Ints sind, folgen den gleichen Regeln wie bei anderen Grundtypen mit vordefinierten Operatoren. Genauer gesagt wird der Ausdruck als x = (T)(x op y) gebunden, wobei T der Typ von x ist und x nur einmal ausgewertet wird.

Die Shift-Operatoren sollten die Anzahl der zu verschiebenden Bits maskieren – auf 5 Bit, wenn sizeof(nint) 4 ist, und auf 6 Bit, wenn sizeof(nint) 8 ist. (siehe §12.11) in der C#-Spezifikation).

Der C#9-Compiler meldet Fehler beim Binden an vordefinierte native Ganzzahloperatoren beim Kompilieren mit einer früheren Sprachversion, ermöglicht jedoch die Verwendung vordefinierter Konvertierungen von und zu nativen Ganzzahlen.

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

Zeigerarithmetik

In C# gibt es keine vordefinierten Operatoren für das Addieren oder Subtrahieren von Zeigern mit nativen ganzen Zahlen als Offsets. Stattdessen werden die Werte nint und nuint auf long und ulong höhergestuft, und für die Zeigerarithmetik werden vordefinierte Operatoren für diese Typen verwendet.

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)

Binäre numerische Höherstufungen

Die binären numerischen Höherstufungen informativer Text (siehe §12.4.7.3) in der C#-Spezifikation wird wie folgt aktualisiert:

  • Wenn ein Operand vom Typ ulong ist, wird der andere Operand in den Typ ulong konvertiert. Andernfalls tritt ein Bindungszeitfehler auf, wenn der andere Operand von Typ sbyte, short, int, nint oder long ist.
  • Andernfalls, wenn ein Operand vom Typ nuintist, wird der andere Operand in den Typ nuintkonvertiert, oder es tritt ein Bindungszeitfehler auf, wenn der andere Operand vom Typ sbyte, short, int, nintoder longist.
  • Andernfalls, wenn ein Operand vom Typ longist, wird der andere Operand in den Typ longkonvertiert.
  • Andernfalls, wenn einer der beiden Operanden vom Typ uint und der andere Operand vom Typ sbyte, short, nint, oder int ist, werden beide Operanden in den Typ long konvertiert.
  • Andernfalls, wenn ein Operand vom Typ uintist, wird der andere Operand in den Typ uintkonvertiert.
  • Andernfalls, wenn ein Operand vom Typ nintist, wird der andere Operand in den Typ nintkonvertiert.
  • Andernfalls werden beide Operanden in den Typ int konvertiert.

Dynamisch

Die Konversionen und Operatoren werden vom Compiler synthetisiert und sind nicht Element der zugrundeliegenden IntPtr und UIntPtr Typen. Daher sind diese Konvertierungen und Operatoren nicht verfügbar aus dem Laufzeitbinder für 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'

Typmember

Der einzige Konstruktor für nint oder nuint ist der parameterlose Konstruktor.

Die folgenden Members von System.IntPtr und System.UIntPtr werden explizit von nint oder nuint ausgeschlossen:

// 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();

Die übrigen Members von System.IntPtr und System.UIntPtr werden implizit in nint und nuint eingeschlossen. Für .NET Framework 4.7.2:

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

Schnittstellen, die von System.IntPtr und System.UIntPtr implementiert werden, sind implizit in nint und nuint enthalten, wobei Vorkommen der zugrunde liegenden Typen durch die entsprechenden systemeigenen ganzzahligen Typen ersetzt werden. Wenn zum Beispiel IntPtr ISerializable, IEquatable<IntPtr>, IComparable<IntPtr> implementiert, dann implementiert nint ISerializable, IEquatable<nint>, IComparable<nint>.

Überschreiben, Ausblenden und Implementieren

nint und System.IntPtr und nuint und System.UIntPtr gelten als gleichwertig für das Überschreiben, Ausblenden und Implementieren.

Überladungen können sich nicht allein durch nint und System.IntPtr und nuint und System.UIntPtr unterscheiden. Unterschiede in Überschreibungen und Implementierungen können allein bei nint und System.IntPtr oder nuint und System.UIntPtr auftreten. Methoden blenden andere Methoden aus, die sich entweder nur durch nint und System.IntPtr oder nur durch nuint und System.UIntPtr unterscheiden.

Verschiedenes

nint- und nuint-Ausdrücke, die als Arrayindizes verwendet werden, werden ohne Konvertierung ausgegeben.

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

nint und nuint können nicht als enum-Basistyp in C# verwendet werden.

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

Lese- und Schreibvorgänge sind für nint und nuint atomar.

Felder können mit volatile für die Typen nint und nuintmarkiert werden. ECMA-334 15.5.4 enthält jedoch keine enum mit Basistyp System.IntPtr oder System.UIntPtr.

default(nint) und new nint() sind äquivalent zu (nint)0; default(nuint) und new nuint() sind äquivalent zu (nuint)0.

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

sizeof(nint) und sizeof(nuint) werden unterstützt, müssen aber in einem unsicheren Kontext kompiliert werden (wie für sizeof(IntPtr) und sizeof(UIntPtr) erforderlich). Die Werte sind keine Konstanten zur Kompilierungszeit. sizeof(nint) ist als sizeof(IntPtr) und nicht als IntPtr.Size implementiert; sizeof(nuint) ist als sizeof(UIntPtr) und nicht als UIntPtr.Size implementiert.

Compilerdiagnosen für Typverweise, die nint oder nuint involvieren, berichten nint oder nuint statt IntPtr oder UIntPtr.

Metadaten

nint und nuint werden in den Metadaten als System.IntPtr und System.UIntPtr dargestellt.

Typverweise, die nint oder nuint enthalten, werden mit einem System.Runtime.CompilerServices.NativeIntegerAttribute ausgegeben, um anzugeben, welche Teile des Typverweises systemeigene Ints sind.

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

Die Codierung von Typbezügen mit NativeIntegerAttribute wird in NativeIntegerAttribute.md behandelt.

Alternativen

Eine Alternative zum oben genannten Ansatz der „Typ-Löschung” besteht darin, neue Typen einzuführen: System.NativeInt und System.NativeUInt.

public readonly struct NativeInt
{
    public IntPtr Value;
}

Unterschiedliche Typen würden eine von IntPtr abweichende Überladung zulassen und ein unterschiedliches Parsen sowie ToString() ermöglichen. Aber es wäre mehr Arbeit für die CLR, diese Typen effizient zu behandeln, was den Hauptzweck des Features, die Effizienz, zunichtemachen würde. Und die Interoperabilität mit vorhandenem nativen Int-Code, der IntPtr verwendet, wäre schwieriger.

Eine weitere Alternative besteht darin, mehr native int-Unterstützung für IntPtr in das Framework aufzunehmen, jedoch ohne spezielle Compiler-Unterstützung. Alle neuen Konversionen und arithmetischen Operationen würden vom Compiler automatisch unterstützt werden. Die Sprache würde jedoch keine Schlüsselwörter, Konstanten oder checked-Operationen bereitstellen.

Designbesprechungen