Native Integers in C#: Understanding nint and nuint
This article provides a comprehensive overview of native-sized
integers in C#, specifically the nint and
nuint types introduced in C# 9. You will learn what these
types represent, how the runtime adapts their size and behavior across
32-bit and 64-bit hardware architectures, how they differ from
traditional integer types and System.IntPtr, and the
primary scenarios where they should be used.
What Are Native Integers?
Native integers, represented by the contextual keywords
nint (signed) and nuint (unsigned), are
integer types whose storage size matches the natural word size of the
host machine's processor.
Under the hood, the C# compiler maps nint and
nuint directly to the underlying runtime types
System.IntPtr and System.UIntPtr. However,
unlike traditional pointers, the compiler treats native integers as
standard numeric types, complete with full support for arithmetic
operators, comparisons, bitwise operations, and overflow checking.
How Native Integers Work Across Architectures
The primary characteristic of nint and
nuint is that their size is determined at execution time
based on the process architecture:
- 32-Bit Environments (x86, ARM32): On 32-bit
architectures,
nintbehaves like a standard 32-bit signed integer (int/System.Int32) with a range of -2,147,483,648 to 2,147,483,647. Similarly,nuintacts as a 32-bit unsigned integer (uint/System.UInt32) ranging from 0 to 4,294,967,295. - 64-Bit Environments (x64, ARM64): On 64-bit
architectures,
nintexpands to an 8-byte, 64-bit signed integer equivalent tolong(System.Int64), spanning from -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807. Likewise,nuintfunctions as an 8-byte, 64-bit unsigned integer equivalent toulong(System.UInt64), ranging from 0 to 18,446,744,073,709,551,615.
Because the underlying representation matches the pointer size of the
executing system, you do not need to write architecture-specific code or
conditional compilation directives (#if) to adjust integer
widths between 32-bit and 64-bit builds.
Language Support vs. IntPtr
Prior to C# 9, developers working with pointer-sized integers relied
on System.IntPtr and System.UIntPtr. However,
these types lacked standard mathematical operators, requiring verbose
casting to and from long or unsafe pointer manipulation to
perform basic addition or comparison.
With nint and nuint, native types receive
first-class language support:
- Standard Arithmetic: You can add, subtract,
multiply, and divide native integers directly using
+,-,*, and/. - Bitwise Logic: Bitwise operations such as
&,|,^,<<, and>>work natively without casting. - Overflow Checking: Operations honor
checkedanduncheckedcontexts. If an arithmetic overflow occurs inside acheckedblock, anOverflowExceptionis thrown based on the executing architecture's limits. - Literal Values: You can assign standard integer
literals directly to
nintandnuintwithout explicit casting, provided the value fits within the target's platform constraints.
When to Use nint and nuint
Native integers are primarily designed for performance-critical scenarios and systems-level programming:
- Native Interoperability (P/Invoke): When
interfacing with C or C++ APIs that use platform-dependent types such as
size_t,intptr_t, oruintptr_t,nintandnuintprovide a direct, safe mapping. - Pointer Arithmetic and Span Indexing: When performing high-performance memory operations, native integers allow calculating memory offsets and buffer indices without the overhead of converting between 32-bit and 64-bit values.
- Low-Level Libraries: Frameworks, graphics engines, and runtime utilities benefit from native integers by maximizing CPU register utilization across different platforms without code duplication.