Back to Blog
C#

C# Integer Types Comparison: Choose the Right Type

c# integer types comparison: Compare C# integer types by range, storage size, and use case. Learn when to use int, long, uint, nint, and BigInteger.

C#Integer TypesNumeric TypesData TypesType SelectionOverflow
Comparison of C# integer types showing range and storage size differences.

When you need to store whole numbers in C#, you have more options than just int. The choice of integer type affects memory usage, range, and even performance in tight loops. This c# integer types comparison walks through each built-in integer type, explains their differences, and gives practical guidance on when to use each.

Integer Types at a Glance

C# provides nine built-in integer types, each with a fixed size and a defined range. The table below summarizes their storage size and range. All of these are value types stored on the stack when used as local variables, and they are subject to the same boxing rules when cast to object.

TypeSize (bits)SignedRange (inclusive)Default
sbyte8Yes-128 to 1270
byte8No0 to 2550
short16Yes-32,768 to 32,7670
ushort16No0 to 65,5350
int32Yes-2,147,483,648 to 2,147,483,6470
uint32No0 to 4,294,967,2950
long64Yes-9,223,372,036,854,775,808 to 9,223,372,036,854,775,8070
ulong64No0 to 18,446,744,073,709,551,6150
nint32 or 64YesPlatform-dependent0
nuint32 or 64NoPlatform-dependent0

nint and nuint are native-sized integers. Their size matches the pointer size of the runtime, so on a 32-bit process they are 32 bits, and on a 64-bit process they are 64 bits. They are primarily used for interop and low-level memory operations.

Signed vs Unsigned Types

A signed type uses one bit to represent the sign, reducing the maximum positive value but allowing negative numbers. Unsigned types store only non-negative values, effectively doubling the upper range for the same storage size. For example, byte and sbyte both use 8 bits, but byte goes from 0 to 255 while sbyte goes from -128 to 127.

Most developers default to signed types because they are more flexible and match common arithmetic expectations. Unsigned types are useful when the value cannot logically be negative, such as a length, a count, or a raw byte from a stream. However, mixing signed and unsigned types in the same expression can trigger implicit conversions that may cause subtle bugs. For instance, int and uint cannot be implicitly combined; the compiler requires an explicit cast.

Choosing the Right Integer Type

The most common choice is int. It is large enough for most counters, indices, and IDs, and it is the default type for integer literals in C#. When you need a wider range, long is the natural next step. long is also used for timestamps, file sizes, and database auto-increment columns that may exceed the 2.1 billion limit of int.

Use byte or sbyte when you are working with raw binary data, such as reading from a network stream or parsing a binary file format. short and ushort appear less frequently, but they are useful when you need to interoperate with C or C++ structures that use 16-bit integers, or when you want to reduce memory in large arrays of small numbers.

For performance-sensitive code, the size of the type can matter. Smaller types use less memory, which improves cache locality when iterating over large arrays. However, the CPU often operates on 32-bit or 64-bit words, so using a byte may require extra conversion instructions. The performance impact is usually small, but it can become measurable in tight loops that process millions of elements.

Overflow Behavior and checked Contexts

Arithmetic on integer types can overflow when the result exceeds the type's range. By default, C# performs unchecked arithmetic, meaning the result wraps around. For example, adding 1 to int.MaxValue produces int.MinValue without throwing an exception. This behavior is efficient but can hide bugs.

You can enable checked arithmetic to force an OverflowException when overflow occurs. This is done either globally in the project settings or locally with the checked keyword:

int max = int.MaxValue; try { int result = checked(max + 1); // throws OverflowException } catch (OverflowException) { Console.WriteLine("Overflow detected"); }

The unchecked keyword explicitly disables overflow checking, even if the project has checked enabled globally. In practice, you should use checked contexts when overflow would indicate a serious logic error, such as in financial calculations or security-critical code. For performance-sensitive code, leave overflow unchecked to avoid the overhead of exception checks.

Special-Purpose Types: nint, nuint, and BigInteger

nint and nuint are native-sized integers introduced in C# 9. They are designed for scenarios where the natural word size of the platform is required, such as when calling native functions that take a pointer-sized integer. Their range is platform-dependent, so code that relies on a specific range will not be portable across architectures.

For arbitrarily large integers, the BigInteger type in System.Numerics can represent values of any size. It is a reference type, so it has higher memory overhead and slower arithmetic than the fixed-size types. Use BigInteger only when you need to handle numbers beyond the range of ulong, such as in cryptography or complex mathematical calculations.

using System.Numerics; BigInteger huge = BigInteger.Parse("123456789012345678901234567890"); BigInteger squared = huge * huge;

Performance and Memory Considerations

When choosing an integer type, consider how it will be used in memory. An array of byte uses one byte per element, while an array of long uses eight. For large collections, this difference can significantly affect memory consumption and cache behavior. If you are storing millions of values that fit in a short, using short instead of int can reduce memory footprint by half.

However, smaller types are not always faster. The .NET runtime and the JIT compiler are optimized for 32-bit and 64-bit integers. Operations on byte or short may require additional sign-extension or zero-extension instructions. In practice, the difference is often negligible unless the code is highly compute-bound. Profiling is the only reliable way to know if a smaller type improves performance in your specific scenario.

Another consideration is alignment. The CLR aligns fields in a class or struct to their natural boundary. A byte field can be placed at any offset, but a long field requires 8-byte alignment on 64-bit platforms. This can affect the total size of a struct due to padding. For example, a struct with a byte followed by a long may occupy 16 bytes instead of 9 because of alignment.

Conversions and Common Pitfalls

Implicit conversions are allowed from a smaller integer type to a larger one, as long as the target type can represent every value of the source type. For example, int can be implicitly converted to long, but not to uint because uint cannot represent negative values. Explicit casts are required when a larger type is assigned to a smaller type, or when converting between signed and unsigned types.

int i = 1000; long l = i; // implicit uint u = (uint)i; // explicit, but safe because i is positive short s = (short)i; // explicit, may overflow if i > 32767

A common mistake is assuming that int and uint are interchangeable. They are not, and mixing them in arithmetic can lead to unexpected results. For instance, int.MaxValue + 1 as an unchecked operation wraps to int.MinValue, but if you cast the operands to uint, the result is 2147483648. Always be explicit about the types in expressions that mix signed and unsigned values.

Another pitfall is using int for values that might exceed its range. This is especially common when reading file lengths or database counts. If there is any chance the value could be larger than 2,147,483,647, use long from the start. Changing the type later can be tedious and error-prone.

When interoperating with APIs that expect a specific integer type, such as uint for a Win32 handle or int for a COM method, you must match the exact type to avoid marshaling errors. The nint type is often used for pointers and handles, but it is not implicitly convertible to int or long; you need an explicit cast.

Finally, consider the default value. All integer types default to 0, which is often the correct initial value. But if you are using nullable integer types, the default is null, and you must handle the HasValue property. This is a separate concern from the underlying integer type, but it affects how you write your code.

Understanding the differences between C# integer types is not just about memorizing ranges. It is about selecting the type that matches the data you are modeling, the memory constraints of your application, and the arithmetic behavior you expect. The right choice reduces bugs, improves readability, and can make your code more efficient.

c# integer types comparison: Practical Usage and Code Exampl | RYUSLOG DEV