Whole Numbers
C# has several whole-number types with fixed ranges. int is the default, long is for big values, and overflow wraps silently.
int count = 42; long big = 42L; checked(a + b) unchecked(a + b)A whole number has no fractional part: 0, 7, -3. C# does not have one whole-number type. It has several, and they differ in how much memory they use. A bit is one single piece of memory, holding either a one or a zero. More bits hold a wider range of numbers.
The choices
| Type | Bits | Range |
|---|---|---|
sbyte / byte | 8 | -128 to 127 / 0 to 255 |
short / ushort | 16 | -32,768 to 32,767 / 0 to 65,535 |
int / uint | 32 | -2,147,483,648 to 2,147,483,647 / 0 to 4,294,967,295 |
long / ulong | 64 | about -9.2 to 9.2 quintillion / 0 to about 18.4 quintillion |
The types beginning with u are unsigned. They hold no negative values, and spend the freed range on larger positive ones. Use int by default. Use long when a value can pass about two billion, such as a byte count or a database identifier. Each keyword is a shorthand for a .NET type name, and the two are the same type: int and System.Int32 are interchangeable, as are long and System.Int64.
Overflow
Every type has a highest value. Adding one to it cannot produce a bigger number, because there is nowhere to put it. By default C# wraps around: int.MaxValue + 1 becomes int.MinValue, with no error at all. The checked keyword changes that. Arithmetic inside checked throws an OverflowException instead of wrapping. unchecked asks for the wrap on purpose. Both have a statement form and an expression form, and the two forms mean the same thing: checked { total = a + b; } is the longhand, total = checked(a + b); the short one.
What trips people up
The silent wrap applies to ordinary arithmetic on variables. Arithmetic the compiler can work out while building is checked instead, so int.MaxValue + 1 written with constants is a build error rather than a wrap.
Example
// Each whole-number type stores a fixed range, set by how many bits it uses.
byte red = 200; // 8 bits, 0 to 255
short offset = -3; // 16 bits
int population = 8_100_000; // 32 bits, the usual choice
long fileBytes = 9_000_000_000L; // 64 bits, this value is too big for int
Console.WriteLine($"{red} {offset} {population} {fileBytes}");
Console.WriteLine($"int holds {int.MinValue} to {int.MaxValue}");
// Running past the end of the range wraps around, with no error.
int biggest = int.MaxValue;
int wrapped = biggest + 1;
Console.WriteLine($"int.MaxValue + 1 = {wrapped}");
// checked turns that silent wrap into an exception. try runs the code and,
// if it fails, runs the catch block instead.
try
{
int tooBig = checked(biggest + 1);
Console.WriteLine(tooBig);
}
catch (OverflowException)
{
Console.WriteLine("checked threw OverflowException");
}
// Output:
// 200 -3 8100000 9000000000
// int holds -2147483648 to 2147483647
// int.MaxValue + 1 = -2147483648
// checked threw OverflowExceptionWhen to use it
- Pick long rather than int for a row identifier or a file size that can pass two billion.
- Store a colour channel or a byte of file content in a byte, which cannot hold a negative value by mistake.
- Wrap a running total in checked so a silent wrap becomes an exception your logs will show.
- Read int.MaxValue to find out whether a value will fit before you convert it.
More examples
Unsigned types and integer division
Two separate surprises in one file. An unsigned type wraps at zero rather than going negative, so subtracting one from a uint of 0 gives the largest uint. And 7 / 2 is 3, not 3.5, because both sides are whole numbers so the division is done in whole numbers. Reach for this when a count or an average comes out wrong by a fraction: one side has to be a floating-point type.
// Unsigned types hold no negative values. They wrap at zero instead.
uint stock = 0;
uint one = 1;
Console.WriteLine(stock - one);
Console.WriteLine(uint.MaxValue);
// Division between two whole numbers discards the remainder. It does not round.
Console.WriteLine(7 / 2);
Console.WriteLine(7 % 2); // % gives the remainder on its own
Console.WriteLine(7.0 / 2); // one side is a double, so the result is a double
// Output:
// 4294967295
// 4294967295
// 3
// 1
// 3.5Narrowing a long back to an int
The overflow-checking context covers conversions between whole-number types as well as arithmetic. Without checked, the extra high bits of the long are thrown away and you get 705032704, a plausible-looking number that is completely wrong. Wrap the conversion in checked whenever the value could be larger than the destination holds, which is most of the time when the number came from outside your program.
// long holds values that will not fit in an int. A byte count for a large
// file is the classic case.
long fileBytes = 5_000_000_000;
Console.WriteLine(fileBytes);
// Going back down to int is a cast you have to write, and it can lose data.
int truncated = (int)fileBytes;
Console.WriteLine(truncated);
// checked reports the loss instead of hiding it.
try
{
int safe = checked((int)fileBytes);
Console.WriteLine(safe);
}
catch (OverflowException)
{
Console.WriteLine("value does not fit in an int");
}
// Output:
// 5000000000
// 705032704
// value does not fit in an int
Discussion