Numbers and Booleans
By the end of this lesson
Pick the right numeric type and understand overflow and precision limits.
C# offers a dozen numeric types. You will use four of them regularly, and the choice between them comes down to three questions: how large can the value get, how exact does it need to be, and what should happen at the edges.
Getting this wrong does not usually produce an error. It produces a number that looks reasonable and is not.
The types worth knowing by heart:
- int
- 32-bit whole number, about -2.1 billion to 2.1 billion. The default for counts, identifiers and loop positions.
- long
- 64-bit whole number, about plus or minus 9.2 quintillion. For values that accumulate: bytes transferred, milliseconds, ids from a busy database.
- decimal
- 128-bit base-10 number with 28 to 29 significant digits. For money, and anything a person will check with a calculator.
- double
- 64-bit binary floating point, roughly 15 to 17 significant digits with an enormous range. For measurements, averages of measurements and scientific work.
- float
- 32-bit binary floating point, roughly 6 to 9 significant digits. Rare in business code; common in graphics and when talking to other systems.
- bool
- true or false, and nothing else. C# will not accept a number as a condition.
Whole-number division discards the remainder
int totalSeats = 7;
int rows = 2;
int seatsPerRow = totalSeats / rows; // 3, not 3.5
int leftOver = totalSeats % rows; // 1
decimal exact = (decimal)totalSeats / rows; // 3.5
Console.WriteLine($"{seatsPerRow} per row, {leftOver} left over, exact {exact}");- When both sides of / are whole numbers, C# performs whole-number division and throws the remainder away. It does not round: 7 / 2 is 3, and 9 / 10 is 0.
- The % operator gives you the remainder that division dropped.
- Converting one side to decimal makes the whole expression decimal, so the fraction survives. Converting the result instead is too late — the information has already gone.
double loose = 0.1 + 0.2;
Console.WriteLine(loose); // 0.30000000000000004
Console.WriteLine(loose == 0.3); // False
decimal tight = 0.1m + 0.2m;
Console.WriteLine(tight); // 0.3
Console.WriteLine(tight == 0.3m); // True- decimal stores digits in base 10, so a value you typed as 0.1 stays 0.1 and comparisons behave the way a person expects.
- The cost is speed and range. decimal arithmetic is several times slower than double and covers a smaller span of magnitudes. For money that trade is worth making every time.
- The m suffix is not decoration. Without it the literals are doubles, and assigning a double result to a decimal variable is a build error — which is C# stopping you mixing the two by accident.
- Comparing two doubles with == is rarely what you want. Compare the difference against a tolerance you choose, or use decimal if exactness is the requirement.
Overflow is silent by default
int bytesToday = int.MaxValue; // 2,147,483,647
int wrapped = bytesToday + 1;
Console.WriteLine(wrapped); // -2147483648
try
{
int guarded = checked(bytesToday + 1);
Console.WriteLine(guarded);
}
catch (OverflowException)
{
Console.WriteLine("Overflow reported instead of wrapping.");
}- Adding 1 to the largest int wraps around to the smallest. There is no exception and no warning — the extra bit has nowhere to go, so the sign flips.
- checked turns that wrap into an OverflowException, which is far easier to diagnose than a nonsensical negative total.
- A whole project can be switched to checked arithmetic with CheckForOverflowUnderflow in the project file. Most teams leave the default and choose a type with room to spare instead.
bool is stricter in C# than in some other languages:
- bool holds exactly true or false. if (itemCount) does not compile — write if (itemCount > 0)
- There is no truthiness. An empty string is not false, 0 is not false, and null is not false
- bool? adds a third state for "not answered yet", which is useful for an optional approval flag
- A bool? cannot go straight into an if. Compare it: if (approved == true), which is false when the value is null
Summary
- int for counts, long for values that accumulate, decimal for money, double for measurements
- Whole-number division discards the remainder — convert before dividing, never after
- double is base 2, so decimal fractions are approximations; decimal is base 10 and keeps the digits you typed
- Integer overflow wraps silently unless you ask for checked arithmetic
- bool is strictly true or false; C# has no truthiness to fall back on
Practice
Attempt each one before opening the solution. Getting it wrong first is how the idea sticks.
Try it yourself
Try it yourself
Take five whole-pound order values, then work out the average twice: once with int arithmetic and once keeping the fraction.
Print both, then write one sentence explaining where the missing amount went.
Show solution
94 divided by 5 is 18.8. Integer division keeps 18 and discards 0.8, so the reported average is around four per cent low.
The conversion has to happen before the division. (decimal)(intSum / count) converts 18 into 18m, which is the wrong answer in a more expensive type — a mistake that is easy to make and hard to see in review.
List<int> orderValues = new List<int> { 19, 24, 31, 12, 8 };
int sum = 0;
foreach (int value in orderValues)
{
sum += value;
}
int integerAverage = sum / orderValues.Count; // 18
decimal exactAverage = (decimal)sum / orderValues.Count; // 18.8
Console.WriteLine($"Integer average: {integerAverage}");
Console.WriteLine($"Exact average: {exactAverage}");Challenge
Challenge
A nightly job records how many bytes it processed, adding to a running total held in an int. Each run handles roughly 300 million bytes.
Work out roughly how many runs it takes before the total is wrong, then decide which type you would use and what it costs.
Show solution
An int tops out at 2,147,483,647, so seven runs of 300 million fit. The eighth wraps to a large negative number, and the report looks absurd rather than slightly off — which is the one piece of luck here.
A long holds roughly 9.2 quintillion, which at that rate is more nights than the business will ever see. The cost is 8 bytes per value instead of 4.
The habit worth taking from this: for any value that accumulates, ask what it looks like after a year of running, not after one test.
long bytesProcessed = 0;
const long bytesPerRun = 300_000_000;
for (int run = 1; run <= 365; run++)
{
bytesProcessed += bytesPerRun;
}
Console.WriteLine($"After a year: {bytesProcessed:N0} bytes");
Console.WriteLine($"An int would have stopped being correct at {int.MaxValue:N0}");Knowledge check
Nothing is recorded and there is no score. The explanation appears either way.
Saved in this browser only.