← C#: language and application decisions
01 / 10 · 45 MIN

Types, conversions and numeric boundaries

Trace each operation’s types, preserve numeric bounds and distinguish absence, parsing and validation.

The type belongs to the expression

In C#, var does not mean that a variable changes type with every assignment. With var attempts = 6, the compiler infers int. Later assigning "6" neither turns that variable into a string nor automatically parses text. Source and destination representations still have a contract.

This path uses C# 14 with .NET 10, without preview features. The language defines available operations; the target framework determines usable types and APIs. No SDK installation is needed to answer assessments: snippets are interpreted by reading. ASP.NET Core, HTTP routes and web applications belong to another path.

When diagnosing a result, first note each operand’s type and then the result type. A long or double variable on the left of an assignment does not make every operation on the right use that type.

Converting is not validating

An implicit conversion lets an int be used where long is expected, for example. Not every implicit conversion promises to retain every digit: int to float can lose precision. An explicit conversion makes the choice visible, but can also lose information or fail.

A byte represents values from 0 through 255. If an int variable contains 300, checked((byte)value) rejects conversion with OverflowException. unchecked((byte)value) keeps the lower eight bits and produces 44. That differs from clamping input to 255. If 300 represents a configuration error, wraparound is not a recovery policy.

Constants have their own rules. byte level = 12 compiles because the constant literal fits the destination; byte level = 300 does not. Do not automatically transfer that permission to an int variable merely because its value today is small.

Conversion placement changes the calculation

With int ok = 9 and int seen = 20, expression ok / seen performs integer division and produces zero. Storing that result in double preserves zero. To obtain the fraction, (double)ok / seen converts an operand before division. By contrast, (double)(ok / seen) arrives too late: the integer result has lost the fractional part.

The same reasoning applies to capacity. Multiplying two ints computes the product as int. For a batch of 80,000 units of 40,000 bytes, long bytes = units * size does not prevent intermediate multiplication overflow. long bytes = (long)units * size computes as long and preserves the total of 3,200,000,000.

Choosing long does not remove the need for bounds. If the factors were already very large longs, their product could overflow long as well. Capacity must be justified from input bounds, rather than from one test with small values.

Checked context and decimal intent

checked applies to operations written within its textual scope. A call such as checked(Add(x)) does not change Add’s body. If that body uses unchecked(x + 1), the addition remains unchecked. Place the rule at the calculation you need to control, and distinguish a compile-time constant error from an exception involving runtime values.

For decimal values, suffix m expresses the choice directly: decimal rate = 0.075m. Unsuffixed literal 0.075 is double and does not implicitly convert to decimal. Mixing decimal and double requires an explicit representation decision. Casts should not be added merely until the compiler accepts the calculation.

Casting decimal to int truncates toward zero. (int)(-6.8m) produces -6, not -7. If the business needs a specific rounding rule, implement that rule deliberately. Monetary precision and rounding modes are developed further in the data-contracts module.

Absence is not zero

int? can represent an int or absence. In int? retries = null, HasValue is false and reading Value throws InvalidOperationException. Expression retries ?? 2 returns 2 without changing retries. It is appropriate only if two attempts are the explicit policy for an absent setting.

Ordered comparisons contain a trap: with int? reading = null, both reading < 10 and reading >= 10 are false. A decision using only those two branches can leave absence untreated. Decide what to do when a reading is missing before comparing a present value.

Do not confuse absence with an observed zero either. In a stock count, zero can be a valid measurement; in an optional request, null can mean "leave unchanged". Converting both to zero too early removes information needed by the next operation.

Guided case: accepting a stock row

A stock importer receives quantities as text and accepts values from 0 through 500. It must distinguish three outcomes: text that cannot be parsed as int, an integer outside the range and an accepted row. A numeric field alone cannot carry those three outcomes.

The snippet uses TryParse to obtain a conversion decision and returns reason as "parse-failed" or "out-of-range" for the two rejection types. It checks the range only after that decision is true. "0" is accepted as zero; "26" as 26; "800" is rejected by the bound; "many" is rejected by parsing. A later statement may apply stock only if TryQuantity returns true. The wrapper explicitly leaves out quantity at zero on failure, but that zero is not approval.

This example receives simple integers and uses the ordinary TryParse overload. A protocol imposing particular separators or culture needs to specify those rules through appropriate overloads. This does not imply that numeric text from every system shares one format.

Review before the decision

To review a calculation, write down input types, the type of each intermediate operation, possible bounds and the effect of failure. If conversion comes after precision loss or overflow, move the decision to the right operand. If input may be missing, do not use a value rule as proof of presence.

At work, these distinctions appear in counters, capacity limits, imports and configuration. A useful diagnosis identifies where information was lost: parsing, calculation, conversion or fallback. A correction should preserve the business rule and distinguish invalid data from legitimate values.

Summary: var infers a type; a cast is not validation; checked has textual scope; int? represents absence; TryParse returns a decision that must be respected. Later modules apply these rules to parameters, collections and data contracts.

static bool TryQuantity(string? text, out int quantity,
    out string? reason)
{
    quantity = 0;
    reason = null;
    if (!int.TryParse(text, out int parsed))
    {
        reason = "parse-failed";
        return false;
    }
    if (parsed < 0 || parsed > 500)
    {
        reason = "out-of-range";
        return false;
    }
    quantity = parsed;
    return true;
}
IN PRACTICE

Read the method inside a class with nullable context enabled. The Boolean indicates acceptance; reason distinguishes rejections and is null on success. Use quantity as accepted data only when the result is true. It does not write to the stock system.

Common pitfalls

Relying on destination type to prevent overflow; converting after division; ignoring the TryParse Boolean; treating null as zero; using checked only in the caller.

Related topics: References, nullability and text · Methods, parameters and patterns · Decimals, time and data contracts

Take this idea with you

Preserve information before calculating, converting or applying fallbacks.

Create account

Reference: Built-in numeric conversions · C# 14 / .NET 10; no preview features