📁 Sangam Dhakal - Portfolio v2.0 - Netscape Navigator 4.0
_
□
×

πŸ“š Interview question

What are the built-in numeric data types in C#? When do you use int, long, double or decimal, and how do overflow, checked arithmetic and floating-point precision affect real code?

Choosing numeric types in C#: integral versus binary floating point versus decimal, silent overflow and checked contexts, NaN and Infinity, rounding, and money calculations.

Junior Coding Intermediate csharp data-types decimal floating-point language-basics overflow

🎯 Key points

  • Integral types: sbyte/byte, short/ushort, int/uint, long/ulong, nint/nuint, Int128/UInt128; int is the default
  • double and float are binary IEEE 754: 0.1 + 0.2 != 0.3, and they produce NaN and Infinity instead of throwing
  • decimal is base-10 with 28-29 significant digits: exact for money, slower, and it throws on overflow and divide by zero
  • Integer arithmetic is unchecked by default at run time: int.MaxValue + 1 silently wraps to int.MinValue
  • checked blocks or CheckForOverflowUnderflow turn overflow into OverflowException; constant expressions are checked at compile time
  • Integer divide by zero throws DivideByZeroException; floating-point divide by zero gives Infinity or NaN
  • Math.Round uses banker's rounding (ToEven) by default; state MidpointRounding explicitly for money
  • Compare doubles with a tolerance, never with ==

⏱ 30-second answer

C# has integral types (byte to long, plus nint, Int128), binary floating point (float, double, Half) and decimal. I use int by default, long for IDs and counters that can pass 2.1 billion, double for measurements and science, and decimal for money because it is base-10 and exact for values like 0.1. Integer overflow is unchecked by default, so int.MaxValue + 1 silently wraps; I use checked where wrapping would be a bug. Doubles never throw: dividing by zero gives Infinity or NaN, and Math.Round defaults to banker's rounding.

πŸ—£ Full answer (senior)

How I would answer this in an interview

I group the numeric types by what they represent β€” exact integers, binary floating point, and decimal floating point β€” then talk about the two production failure modes: silent overflow and floating-point rounding.

1. The built-in numeric types

Type .NET type Size Range / precision Typical use
sbyte / byte SByte / Byte 1 byte -128..127 / 0..255 Binary data, protocols
short / ushort Int16 / UInt16 2 bytes Β±32,767 / 0..65,535 Interop, compact storage
int / uint Int32 / UInt32 4 bytes Β±2.1 billion / 0..4.29 billion Default integer, counts, indexes
long / ulong Int64 / UInt64 8 bytes Β±9.2 Γ— 10¹⁸ IDs, ticks, file sizes, totals
nint / nuint IntPtr / UIntPtr 4 or 8 Pointer-sized Interop, low-level memory
Int128 / UInt128 (.NET 7+) 16 bytes Β±1.7 Γ— 10³⁸ Hashing, very large counters
Half (.NET 5+) 2 bytes ~3 significant digits ML, graphics
float Single 4 bytes ~6–9 significant digits Graphics, sensors
double Double 8 bytes ~15–17 significant digits Science, statistics, measurements
decimal Decimal 16 bytes 28–29 significant digits, base 10 Money, rates, quantities that must be exact

2. How I choose

  • int is the default for counting and indexing. Arrays, List<T>.Count and Span<T>.Length are all int.
  • long when values can exceed ~2.1 billion: database bigint identity columns, byte counts of files, DateTime.Ticks, aggregated totals over large tables. Many systems have hit outages when an int identity column passed 2,147,483,647.
  • double for measured or scientific quantities where tiny relative error is fine and speed matters (it is hardware-native and SIMD-friendly).
  • decimal for anything a human expects to add up exactly in base 10: currency, GST, interest rates, invoice lines. It is ~10–20Γ— slower than double but exact for decimal fractions. It maps cleanly to SQL Server decimal(p,s) in EF Core (configure precision explicitly, e.g. HasPrecision(18, 2)).
  • Unsigned types rarely in public APIs β€” they are not CLS-compliant and mixing them with int causes awkward casts. Use them for bit manipulation and binary formats.

3. Overflow and checked arithmetic

  • Integer arithmetic in C# is unchecked by default: int.MaxValue + 1 silently wraps to int.MinValue. No exception, no log.
  • Wrap risky arithmetic in checked { ... } or checked(expr) to throw OverflowException instead. You can turn checking on project-wide with <CheckForOverflowUnderflow>true</CheckForOverflowUnderflow> (often done in Debug builds or for financial code).
  • Casting is also affected: (int)someLong truncates silently unless checked; checked((int)someLong) throws.
  • decimal always throws OverflowException on overflow, and float/double never throw β€” they produce Infinity or NaN.
  • Division by zero: integers and decimal throw DivideByZeroException; double returns Β±Infinity or NaN.
  • Since C# 11 you can define user checked operators, and generic math (INumber<T>) exposes T.CreateChecked, CreateSaturating and CreateTruncating for explicit conversion policies.

4. Floating-point precision

double stores a base-2 fraction, so most base-10 fractions (0.1, 0.2) are approximations:

Console.WriteLine(0.1 + 0.2 == 0.3);     // False
Console.WriteLine(0.1m + 0.2m == 0.3m);  // True

Rules I follow:

  • Never compare double with == for computed values; compare with a tolerance appropriate to the domain (Math.Abs(a - b) < 1e-9, or relative tolerance for large values).
  • NaN is not equal to anything, including itself β€” use double.IsNaN.
  • Round money explicitly with Math.Round(value, 2, MidpointRounding.AwayFromZero) (default is banker's rounding, ToEven, which surprises finance teams).

5. A production example

A typical bug in a billing system: line totals were computed in double, multiplied by a GST rate of 0.1, summed across thousands of invoices and then rounded. Reports differed from the accounting system by a few cents per thousand invoices β€” enough to fail reconciliation. The fix was converting the whole money path to decimal (domain model, EF Core mapping decimal(18,2), JSON), defining a single rounding policy, and adding tests with known edge values like 0.005. A separate incident class is integer overflow: a counter of bytes processed stored as int wrapped negative after 2 GB, and a progress percentage went to -200%. Switching to long and enabling checked in that calculation fixed it and made any future overflow loud.

πŸ” What the interviewer is checking

The interviewer is checking whether the candidate will introduce silent numeric bugs into financial or counting code.

Green flags

  • Says decimal for money and explains why (base-10 representation).
  • Knows integer overflow wraps by default and how to turn on checking.
  • Knows floating-point division by zero gives Infinity or NaN rather than throwing.
  • Mentions banker's rounding.

Red flags

  • Uses double for prices.
  • Believes int overflow always throws.
  • Compares floating-point values with == without hesitation.

πŸ“š Learn in depth

1. Integer representation: two's complement

Signed integers use two's complement: the top bit has negative weight. For sbyte, 0111_1111 is 127 and 1000_0000 is -128. Adding 1 to 127 just carries into the top bit, giving -128 β€” that is why overflow "wraps". The CPU sets an overflow flag, but in an unchecked context the JIT ignores it.

In a checked context the compiler emits different IL opcodes β€” add.ovf, mul.ovf, conv.ovf.i4 β€” and the JIT turns those into an instruction plus a conditional jump to a throw helper (jo on x64). The cost is small (a predictable branch), which is why checking money or quota arithmetic is usually affordable.

checked(a + b)   ->  add.ovf      ->  add eax, edx ; jo ThrowOverflow
unchecked(a + b) ->  add          ->  add eax, edx

Constant expressions are always checked at compile time: int x = int.MaxValue + 1; is error CS0220 unless wrapped in unchecked.

2. IEEE 754 binary floating point

double is 64 bits: 1 sign bit, 11 exponent bits, 52 fraction bits (plus an implicit leading 1). Its value is Β±1.fraction Γ— 2^(exponent βˆ’ 1023).

  • Only fractions with a power-of-two denominator are exact (0.5, 0.25, 0.375). 0.1 becomes 0.1000000000000000055511151231257827....
  • Precision is relative: near 1.0 the gap between adjacent doubles is ~2.2e-16 (double.Epsilon is not that β€” it is the smallest positive subnormal, ~4.9e-324). Near 1e16 the gap is 2.0, so 1e16 + 1 == 1e16.
  • Special values: +0 and -0, Β±Infinity, and NaN (many bit patterns). NaN != NaN, which breaks naive sorting and dictionary keys.
  • Since .NET Core 3.0, double.ToString() produces the shortest round-trippable string, so 0.1 + 0.2 prints as 0.30000000000000004 rather than hiding the error.

Operations are not associative: (a + b) + c can differ from a + (b + c). Summing a large array in a different order (or in parallel, or with SIMD) can change the last digits. For stable totals, sort ascending or use Kahan summation.

3. How decimal works

System.Decimal is a 128-bit decimal floating-point number:

  • a 96-bit unsigned integer coefficient,
  • a scale from 0 to 28 (the power of ten to divide by),
  • a sign bit.

So 19.95m is stored as 1995 Γ— 10^-2. Any base-10 fraction with up to 28–29 digits is exact. Operations are implemented in software (no hardware instruction), which makes them roughly an order of magnitude slower than double, but for business code that cost is negligible compared with I/O.

decimal preserves scale: 1.10m and 1.1m are equal but print differently. Division can still round: 1m / 3m is 0.3333333333333333333333333333. Exactness is about base-10 fractions, not about infinite precision.

4. Implicit and explicit conversions

  • Widening conversions are implicit when no information is lost in magnitude: int β†’ long, int β†’ double, float β†’ double.
  • Some implicit conversions lose precision silently: long β†’ double (doubles have only 53 bits of mantissa, so large IDs get rounded) and long β†’ float.
  • There is no implicit conversion between double and decimal in either direction β€” the language forces you to decide.
  • Explicit casts from floating point to integer truncate toward zero ((int)2.9 == 2, (int)-2.9 == -2). Out-of-range casts from double to int were platform-dependent historically; .NET 9 standardised saturating behaviour on x86/x64, but code should never rely on it β€” validate the range first.

5. Generic math (.NET 7+)

Interfaces such as INumber<T>, IBinaryInteger<T> and IFloatingPoint<T> let you write one algorithm for all numeric types:

static T Sum<T>(ReadOnlySpan<T> values) where T : INumber<T>
{
    T total = T.Zero;
    foreach (var v in values) total += v;
    return total;
}

They also make conversion policy explicit: int.CreateChecked(x) throws, CreateSaturating clamps, CreateTruncating wraps.

6. Database and serialisation boundaries

  • SQL Server int/bigint map to int/long; decimal(p,s) maps to decimal β€” configure precision in EF Core, otherwise you get a warning and the provider default decimal(18,2), which may truncate rates like 0.0725.
  • SQL float is a double; never store money in it.
  • JSON numbers have no type; JavaScript clients parse them as double, so a long ID above 2⁡³ (9,007,199,254,740,992) loses precision in the browser. Serialise large IDs as strings (JsonNumberHandling.WriteAsString).

Try it yourself

  1. Print 0.1 + 0.2 and 0.1m + 0.2m, then compare each with 0.3.
  2. Run int x = int.MaxValue; x++; with and without <CheckForOverflowUnderflow>.
  3. Sum 1,000,000 copies of 0.1 as double and as decimal and compare to 100000.
  4. Cast long id = 9_007_199_254_740_993; to double and back and observe the change.

πŸ’» Code

namespace Basics;

public static class Numbers
{
    public static void Main()
    {
        // 1. Binary floating point vs decimal
        Console.WriteLine(0.1 + 0.2 == 0.3);     // False
        Console.WriteLine(0.1m + 0.2m == 0.3m);  // True

        // 2. Silent wrap vs checked
        int count = int.MaxValue;
        Console.WriteLine(count + 1);            // -2147483648
        try
        {
            Console.WriteLine(checked(count + 1));
        }
        catch (OverflowException ex)
        {
            Console.WriteLine($"Caught: {ex.Message}");
        }

        // 3. Division by zero differs by type
        double d = 0;
        Console.WriteLine(1 / d);                // ∞
        Console.WriteLine(double.IsNaN(0 / d));  // True
        // Console.WriteLine(1 / 0);             // CS0020 for constants; DivideByZeroException for variables

        // 4. Rounding money explicitly
        decimal lineTotal = 2.345m;
        Console.WriteLine(Math.Round(lineTotal, 2));                                 // 2.34 (ToEven)
        Console.WriteLine(Math.Round(lineTotal, 2, MidpointRounding.AwayFromZero));  // 2.35
    }

    // Money calculation: decimal end to end, round once per line
    public static decimal InvoiceTotal(IEnumerable<(decimal UnitPrice, int Qty)> lines, decimal gstRate = 0.10m)
    {
        decimal subtotal = 0m;
        foreach (var (unitPrice, qty) in lines)
        {
            subtotal += checked(Math.Round(unitPrice * qty, 2, MidpointRounding.AwayFromZero));
        }
        return Math.Round(subtotal * (1 + gstRate), 2, MidpointRounding.AwayFromZero);
    }
}

⚠ Common mistakes

1. Storing money in double or float

What happens: prices, tax and totals are computed in double and rounded at the end. Why it is wrong: most decimal fractions are not representable in binary, so sums drift by fractions of a cent and fail reconciliation with finance systems. Do instead: use decimal end to end β€” domain model, EF Core mapping with explicit precision, API contracts β€” and define one rounding policy.

2. Assuming integer overflow throws

What happens: int counters, quotas or byte totals exceed 2,147,483,647 and silently wrap negative. Why it is wrong: C# is unchecked by default; the bug surfaces far from the cause (negative percentages, wrong pagination, security checks passing). Do instead: use long for anything that can grow large, and wrap critical arithmetic in checked (or enable CheckForOverflowUnderflow) so overflow is loud.

3. Comparing floating-point values with ==

What happens: if (total == 0.3) or unit tests that Assert.Equal(0.3, result) on computed doubles. Why it is wrong: tiny representation errors make equal-looking values unequal. Do instead: compare with a domain-appropriate tolerance, or use decimal if exact equality is required. xUnit's Assert.Equal(expected, actual, precision) helps in tests.

4. Ignoring NaN and Infinity

What happens: division by zero in double produces Infinity or NaN, which flows into averages, JSON or the database. Why it is wrong: NaN poisons every calculation it touches, NaN != NaN breaks sorting and lookups, and System.Text.Json throws when serialising it by default. Do instead: guard divisors, check double.IsFinite, and decide explicitly what an undefined result means in the domain.

5. Casting instead of converting with a policy

What happens: (int)someLong, (int)price or (int)Math.Round(x) scattered through the code. Why it is wrong: casts truncate silently and floating-point-to-integer casts round toward zero, not to nearest. Do instead: use checked casts, Math.Round with an explicit MidpointRounding, or generic-math CreateChecked/CreateSaturating so intent is clear.

6. Forgetting banker's rounding

What happens: Math.Round(2.5m) returns 2, and Math.Round(0.125m, 2) returns 0.12. Why it is wrong: .NET defaults to MidpointRounding.ToEven; customers and finance usually expect half away from zero, so invoices disagree with spreadsheets. Do instead: always pass MidpointRounding.AwayFromZero (or whatever the business rule says) and centralise rounding in one helper.

7. Losing precision at boundaries

What happens: long IDs larger than 2⁡³ are sent as JSON numbers; EF Core maps decimal without precision; rates are stored in decimal(18,2). Why it is wrong: JavaScript clients round large IDs, and SQL truncates extra decimal places silently. Do instead: send large IDs as strings, configure HasPrecision for every decimal property, and choose scale from the business rule (rates often need 4–6 decimal places).

8. Using unsigned types in public APIs to "prevent negatives"

What happens: uint quantity on a public method or DTO. Why it is wrong: unsigned types are not CLS-compliant, force casts everywhere, and 0u - 1 wraps to 4,294,967,295 instead of failing. Do instead: use int with validation (guard clauses, data annotations or FluentValidation) and keep unsigned types for bit-level work.

9. Integer division by accident

What happens: double ratio = completed / total; with both operands int returns 0 until completion. Why it is wrong: the division happens in integer arithmetic before conversion. Do instead: convert one operand first: (double)completed / total, or use decimal for percentages that are shown to users.

10. Assuming parallel or SIMD sums are deterministic

What happens: a report total changes slightly between runs after introducing Parallel.For or vectorised summation. Why it is wrong: floating-point addition is not associative, so a different order gives different last digits. Do instead: use decimal for financial totals, or accept a tolerance and document it; use Kahan summation where precision matters.

πŸ› Senior insight: trade-offs

Seniors pick types from the domain, then make overflow and rounding explicit.

Domain Type Why
Money, tax, interest decimal Exact base-10, throws on overflow
Counts, quantities int (or long if it can grow) Fast, exact
Database identity keys long int identity columns do run out on busy tables
Sensor data, statistics, ML double / float Speed, SIMD, NaN semantics
Durations TimeSpan Units are part of the type
  • Consider a Money value object (amount plus currency) so rounding rules live in one place.
  • Enable checked arithmetic in domain projects where silent overflow would be a financial bug; the cost is tiny.
  • Align C# and SQL Server precision (decimal(19,4) is common for money) and configure it in EF Core with HasPrecision.
  • For GST in Australia (10%), agree with finance whether rounding happens per line or per invoice; it changes totals.

πŸ”— Follow-ups the interviewer may ask

  1. When: The candidate says int.MaxValue + 1 wraps to a negative number. Why: Goes one level down to bits, two's complement and why wrapping happens.
    How to answer

    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).

  2. When: The candidate talks about division by zero or rounding. Why: Integer division and modulo are where numeric types cause everyday bugs.
    How to answer

    Day to day I use arithmetic, comparison, logical and the null operators. && and || short-circuit, which is what makes x != null && x.IsValid safe. ?. returns null instead of throwing, ?? gives a fallback, ??= assigns only when null, and in C# 14 ?. also works on the left of an assignment. The bugs come from integer division (5 / 2 is 2), precedence (?? binds looser than +), using & instead of &&, and == meaning reference equality on classes that do not overload it. I prefer is null because it cannot be hijacked by an overloaded ==.

πŸ›  Real-world problems: how you handle them

Invoice totals off by a few cents after a double-based refactor
Situation
A B2B billing service in Azure Australia East generated about 40,000 invoices a month. A refactor moved the pricing engine from decimal to double for 'performance'.
Signals
Finance reconciliation reports showed totals differing from the ERP by 1-3 cents on roughly 2% of invoices; customer support tickets about GST rounding.
Investigation
Compared invoice lines between the two systems, reproduced with unit tests using the failing line items, printed values with the R format to expose binary error, and found mixed MidpointRounding defaults.
Resolution
Restored decimal end to end (domain, EF Core HasPrecision(19,4), API contracts), introduced a Money value object, and rounded once per line with MidpointRounding.AwayFromZero as agreed with finance.
Prevention
Property-based tests comparing totals against a reference calculation, an analyser rule banning double in the Billing namespace, and a reconciliation alert on any cent difference.

How to tell it (STAR)

**S:** Our billing service [at company] produced invoices that disagreed with the ERP by a few cents after a refactor. **T:** I had to find the cause and stop the drift before month-end close. **A:** I reproduced it with real line items, showed the double representation errors and the default banker's rounding, moved the money path back to decimal with a Money type, and agreed one rounding rule with finance. **R:** Reconciliation differences went to zero [timeframe], and the analyser rule stopped it coming back.

↩ Usually asked after

More in Language Basics

Last updated 30 Oct 2026.

🌐 Sangam's Webring

βͺ Previous 🏠 Main Next ⏩
πŸ’¬ Chat with me