Bits, bytes and two's complement; the sizes and ranges of C# numeric types; unchecked versus checked overflow; and bitwise operations and flags enums.
A bit is a single 0 or 1; a byte is 8 bits and is the smallest unit memory addresses. C# numeric types have fixed sizes on every platform: byte 1 byte, short 2, int 4, long 8, Int128 16, Half 2, float 4, double 8, decimal 16. Signed integers use two's complement, so int runs from β2,147,483,648 to 2,147,483,647. Integer arithmetic is unchecked by default: int.MaxValue + 1 silently wraps to int.MinValue, while float/double never throw and produce Infinity or NaN, and decimal always throws. Make overflow explicit where a wrap would be a bug (money, counters, sizes from external input): use checked, a wider type, or generic-math conversions such as int.CreateChecked(x) and int.CreateSaturating(x).
sizeof(int) is 4, and Unsafe.SizeOf<T>() gives the size of any unmanaged type.| Type | Bytes | Range / precision |
|---|---|---|
bool |
1 | true/false (one byte, not one bit) |
byte / sbyte |
1 | 0β255 / β128β127 |
short / ushort / char |
2 | β32,768β32,767 / 0β65,535 / one UTF-16 code unit |
int / uint |
4 | Β±2.1 billion / 0β4.29 billion |
long / ulong |
8 | Β±9.2 Γ 10ΒΉβΈ |
Int128 / UInt128 |
16 | Β±1.7 Γ 10Β³βΈ |
nint / nuint |
4 or 8 | pointer-sized (8 in a 64-bit process) |
Half / float / double |
2 / 4 / 8 | binary floating point, about 3 / 7 / 15β17 significant digits |
decimal |
16 | base-10 floating point, 28β29 significant digits |
BigInteger |
variable | arbitrary size, stored on the heap, slower |
Signed integers store negative numbers by inverting the bits and adding one. Addition then uses the same circuit for signed and unsigned values, and:
int.MinValue is β2Β³ΒΉ but int.MaxValue is 2Β³ΒΉ β 1.-int.MinValue cannot be represented: unchecked negation gives back int.MinValue, and Math.Abs(int.MinValue) throws.(uint)(-1) is 4,294,967,295.| Type family | On overflow | Why |
|---|---|---|
| Integers (default) | silently wrap | no check instruction, fastest |
Integers in checked |
throw OverflowException |
explicit opt-in |
float / double / Half |
become Β±Infinity; invalid operations (0/0, βββ) give NaN; no exception |
IEEE 754 rules, which the CPU implements in hardware |
decimal |
always throws | implemented in software, designed for money |
Casts ((byte)300) |
truncate to the low bits (44) unless checked |
Constant expressions that overflow are compile errors. The project property <CheckForOverflowUnderflow>true</CheckForOverflowUnderflow> makes all integer arithmetic checked unless marked unchecked. In generic code, INumber<T> offers T.CreateChecked, T.CreateSaturating and T.CreateTruncating.
&, |, ^, ~, <<, >> and >>> (unsigned right shift). Common uses: flags enums, masks, hashing, binary protocols. System.Numerics.BitOperations provides PopCount, LeadingZeroCount, RotateLeft and IsPow2, which map to single CPU instructions where available.
A reporting job summed transaction amounts in cents into an int. Once a large merchant's monthly total passed about $21.4 million (2Β³ΒΉ cents), the total silently became negative and a payout report showed a loss. The fix was to use long (or decimal for money in dollars), add checked arithmetic in the aggregation, and add a test with values near the limit.
Checks basic data representation and whether the candidate knows a classic silent-failure mode in C#.
Green flags: knows type sizes, explains two's complement and the asymmetric range, knows C# is unchecked by default and how to opt in, gives a realistic overflow example, comfortable with bitwise operators and flags enums.
Red flags: thinks overflow throws by default, uses double for money, can't explain why int.MinValue is special.
+5 = 0000 0101
invert -> 1111 1010
add 1 -> 1111 1011 = -5
0111 1111 = +127 (sbyte.MaxValue)
1000 0000 = -128 (sbyte.MinValue)
127 + 1 -> 1000 0000 -> wraps to -128
The top bit is the sign bit. Adding 5 + (β5) in binary gives 1 0000 0000; the carry falls off the top, leaving 0. That's why hardware needs only one adder.
| Situation | What goes wrong | Safer choice |
|---|---|---|
Summing money in cents as int |
Wraps negative above ~$21.4m | long or decimal, checked |
(a + b) / 2 midpoint in binary search |
a + b overflows for large indexes |
a + (b - a) / 2 |
| Hash code combination | Overflow is fine and intended | HashCode.Combine in unchecked code |
Converting long file sizes to int |
Truncates for files > 2 GiB | Keep long; use checked((int)x) if you must narrow |
Math.Abs(int.MinValue) |
Throws | Handle the edge case or use long |
| Time maths in milliseconds | int ms overflows after ~24.8 days |
long or TimeSpan |
CheckForOverflowUnderflow for Debug builds only is a common way to catch bugs in tests without paying the cost in Release. Decide deliberately: behaviour then differs between configurations.INumber<T>) has T.CreateChecked, CreateSaturating and CreateTruncating for explicit conversion behaviour.set bit n: x |= (1 << n)
clear bit n: x &= ~(1 << n)
toggle bit n: x ^= (1 << n)
test bit n: (x & (1 << n)) != 0
lowest set bit: x & -x
power of two: x > 0 && (x & (x - 1)) == 0 (or BitOperations.IsPow2)
Shifts in C# mask the count: for int, x << 33 is the same as x << 1, because only the low five bits of the count are used.
A [Flags] enum gives each value its own bit so several can be combined in one integer. Use powers of two (or 1 << n), include None = 0, and test with HasFlag or (value & Flag) == Flag. ToString() prints combinations such as Read, Write.
A bool uses a whole byte (and four bytes in some interop contexts). Storing a million flags as bool[] uses about 1 MB; BitArray or a ulong[] bitmap uses about 125 KB. For hot data, smaller means more fits in CPU cache.
int.MaxValue + 1 in an unchecked block and in a checked block, and explain both results.uint with a loop, then compare it with BitOperations.PopCount in BenchmarkDotNet.[Flags] enum Permissions { None = 0, Read = 1, Write = 2, Delete = 4 }, combine values, print them, and remove one with &= ~.<CheckForOverflowUnderflow> for Debug in a test project and write a test that proves your summing code throws instead of wrapping.namespace BitsAndBytes;
using System.Numerics;
[Flags]
public enum Permissions
{
None = 0,
Read = 1 << 0,
Write = 1 << 1,
Delete = 1 << 2,
}
public static class Program
{
public static void Main()
{
Console.WriteLine($"int is {sizeof(int)} bytes, range {int.MinValue}..{int.MaxValue}");
int max = int.MaxValue;
int wrapped = unchecked(max + 1);
Console.WriteLine(wrapped); // -2147483648
try
{
int boom = checked(max + 1);
}
catch (OverflowException ex)
{
Console.WriteLine($"checked: {ex.Message}");
}
// Safe midpoint for large indexes
int lo = 2_000_000_000, hi = 2_100_000_000;
int mid = lo + (hi - lo) / 2;
Console.WriteLine(mid);
// Money: never sum cents in an int
long totalCents = 0;
foreach (var cents in new long[] { 1_500_000_000, 1_500_000_000 })
{
totalCents = checked(totalCents + cents);
}
Console.WriteLine(totalCents);
var perms = Permissions.Read | Permissions.Write;
perms &= ~Permissions.Write;
Console.WriteLine(perms); // Read
Console.WriteLine(BitOperations.PopCount(0b1011_0001u)); // 4
Console.WriteLine(BitOperations.IsPow2(64)); // True
}
}
checked where wrap-around would be a bug, and pick types big enough for realistic maximums.count = 1_000_000_000 and recordSize = 8 makes count * recordSize wrap in int, so you allocate a tiny buffer and later read past it, or bypass a length check. Instead: Validate against sensible maximums first, compute sizes in long or checked context, and reject values that do not fit (int.CreateChecked(longValue) throws).(uint)(-1) is 4,294,967,295 and (int)uint.MaxValue is -1, so a negative length passed to an API taking uint becomes a huge size. Mixing int and uint in an expression also promotes to long, which hides the problem until the value is narrowed again. Instead: Validate the sign before converting, use checked casts, and keep public APIs on signed types (as .NET does with int lengths).(int)fileLength discards high bits for files over 2 GiB, giving a wrong or negative size. Instead: Keep long, or use checked((int)value); in generic code choose CreateChecked (throw), CreateSaturating (clamp) or CreateTruncating (wrap) deliberately.float or double. Why it is wrong: Binary floating point cannot represent 0.1 exactly, so sums drift by fractions of a cent. Instead: Use decimal, or integer cents in a long.==, or with double.Epsilon as the tolerance. Why it is wrong: 0.1 + 0.2 == 0.3 is false, and double.Epsilon is the smallest positive double (about 4.9e-324), far too small to be a tolerance. Half has only about 3 significant digits. Instead: Compare with a tolerance suited to the magnitude, check double.IsNaN explicitly, and use decimal for exact decimal values.(lo + hi) / 2. Why it is wrong: For large indexes the sum overflows and produces a negative index. Instead: Use lo + (hi - lo) / 2.Read | Write equals the third value and HasFlag gives wrong answers. Instead: Use 1 << n values and include None = 0.bool uses one bit, or using long everywhere "to be safe". Why it is wrong: bool uses a byte, and oversized types double memory and cache footprint for large arrays. Instead: Pick the narrowest type that fits the domain maximum; use BitArray or a ulong bitmap for millions of flags.Overflow bugs are rare but expensive, because they appear only at scale, long after the code shipped. Seniors choose types from the domain's realistic maximum and make overflow behaviour explicit:
| Need | Tool |
|---|---|
| Wrap is a bug | checked, CheckForOverflowUnderflow in Debug and tests |
| Clamp to the range (sensor values, pixel maths) | int.CreateSaturating(x), Math.Clamp |
| Wrap is intended (hashing) | unchecked, HashCode.Combine |
| Exact decimal maths (money) | decimal or integer cents in long |
Performance versus safety: a checked operation adds a compare-and-branch. In business code (orders, invoices, file sizes) that cost is invisible, so checked arithmetic is the right default there. In tight numeric loops (image processing, hashing, SIMD code) seniors keep unchecked arithmetic, prove the ranges with tests, and validate inputs once at the boundary instead of on every operation. decimal is roughly 10β20Γ slower than double because it is done in software, which is fine for money and wrong for physics or ML.
They treat overflow as a security issue too: attacker-controlled lengths that wrap can under-allocate buffers or bypass range checks. And they weigh memory: 10 million long values are 80 MB while int is 40 MB and a bitmap of flags is about 1.2 MB, which changes how much fits in CPU cache.
Endianness is the order in which the bytes of a multi-byte value are stored. Little-endian puts the least significant byte first, so 0x12345678 is stored as 78 56 34 12. Big-endian puts the most significant byte first: 12 34 56 78. x64 and Arm64 machines running .NET are little-endian, but network protocols ("network byte order") and many file formats are big-endian. In .NET I use BinaryPrimitives.ReadInt32BigEndian and friends so the byte order is explicit and portable. BitConverter follows the machine, and BinaryReader/BinaryWriter are always little-endian.
They're all positional number systems; only the base changes. Decimal is base 10 (digits 0β9), binary is base 2 (0 and 1) and hexadecimal is base 16 (0β9 then AβF). Computers store everything in binary because transistors have two states. Developers use hex because one hex digit is exactly four bits, so a byte is always two hex digits: 0xFF is 1111 1111 is 255. That makes memory addresses, hashes, GUIDs and byte dumps short and easy to read. In C# you write 0b1010 or 0xFF and format with ToString("X2") or Convert.ToHexString.
Last updated 23 Oct 2026.