C# / Basic: Typed Programs and Object Contracts / Lesson 2
Types, Conversions, and Nullability
Choose numeric and reference representations deliberately and use nullable analysis as a contract checker rather than runtime magic.
On this page
Lesson goals
- Distinguish value types, reference types, and nullable forms.
- Explain arithmetic conversions and checked overflow.
- Use null-state analysis and explicit absence handling.
A type defines operations and representable values
C# is statically typed: every expression has a type that controls valid operations. int is a signed 32-bit integer, double supports binary floating-point arithmetic, decimal targets decimal arithmetic, and bool is not an integer truth value. var asks the compiler to infer a local’s static type; it does not make the variable dynamically typed.
Choose types from the domain and intermediate ranges. Decimal arithmetic can suit many monetary calculations, but rounding and currency policy still need specification. A numeric conversion can narrow or round a value; it is not a validation rule by itself. Inspect external input before committing a narrowed result.
Conversion must occur before the relevant operation
Integer division truncates toward zero. Converting an already computed integer quotient to double does not recover its fraction. The first program contrasts conversion before and after division and formats output with invariant culture so the decimal separator is explicitly reproducible. Formatting is presentation, not a proof that every decimal fraction is exactly representable in binary.
Unchecked integer arithmetic can wrap under its rules, while checked contexts detect applicable integral overflow. Constant-expression overflow has compile-time rules as well. Decide whether an application rejects, saturates, or deliberately wraps; do not let a build setting silently choose the business meaning.
using System;
using System.Globalization;
int total = 7, people = 2;
double truncated = total / people;
double share = (double)total / people;
Console.WriteLine(truncated.ToString("F1", CultureInfo.InvariantCulture));
Console.WriteLine(share.ToString("F1", CultureInfo.InvariantCulture));
3.0
3.5
The first quotient is three before conversion; converting the operand first preserves one half.
Nullable references express intended absence
With nullable analysis enabled, string means a reference intended to be non-null and string? permits absence. Both are the same runtime reference type. The compiler tracks maybe-null and not-null states and issues warnings for inconsistent use; annotations do not inject automatic runtime checks or prevent malformed external data from arriving.
Nullable value types are different: int? is Nullable<int>, which can represent either an int or no value. Test HasValue or use pattern matching before obtaining its value. A non-nullable reference field can still be null at runtime when construction or interoperation violates the contract, so validate important boundaries.
Choose the domain’s absence behavior
The second example treats a missing optional label as the explicit display text unknown. The null-coalescing operator evaluates its right side only when the left side is null. A separate pattern check narrows the nullable count before it is read. These are deliberate policies, not blanket advice to hide every null with a default.
The null-conditional operator can make an optional chain return null, but it should not conceal a missing required dependency. The null-forgiving operator ! suppresses analysis for that expression; it does not construct an object or make a dereference safe. Prefer proving the state with a guard or fixing initialization.
using System;
string? label = null;
int? count = 3;
Console.WriteLine(label ?? "unknown");
if (count is int present)
{
Console.WriteLine($"count={present}");
}
unknown
count=3
The missing label uses a documented fallback, while the nullable int supplies its present value.
Initialization and parsing remain responsibilities
Fields and array elements receive default values, but local variables must be definitely assigned before reading. An array of a non-nullable reference type can initially contain null elements; the annotation does not populate it with objects. Constructors should establish required fields, and input parsers should report syntax and range failures instead of relying on casts.
Use TryParse for expected invalid text and an explicit domain check afterward. Define culture when a format is a protocol rather than user-local presentation. Test missing, empty, whitespace, boundary, and out-of-range inputs. Nullable warnings are useful evidence about intent, while tests and boundary checks establish the behavior that users observe.
Common errors and fixes
A fractional share becomes an integer-valued double.
- Why it happens
- Integer division completed before conversion.
- How to fix it
- Convert an operand before division.
- Prevent it next time
- Trace the type of each intermediate expression.
Adding ! removes a warning but a crash remains.
- Why it happens
- Suppression was mistaken for a runtime check.
- How to fix it
- Validate absence or initialize the required object.
- Prevent it next time
- Review every suppression against a concrete proof.
An array element is null despite a non-nullable annotation.
- Why it happens
- Default array initialization did not construct referenced objects.
- How to fix it
- Populate elements before use or model absence explicitly.
- Prevent it next time
- Test default-created collections and construction paths.
Check your understanding
1. Explain why string? and int? require different runtime mental models.
Show worked solution
string? adds compile-time nullable intent to an ordinary reference that can be null. int? is a Nullable<int> value containing a presence state and an int when present. Both need deliberate absence behavior, but only the latter changes the value-type representation. Do not infer that annotations alone prevent null references from entering through external or legacy code.
2. Design validation for a textual count accepted only from zero through one hundred.
Show worked solution
Use a suitable TryParse overload, reject conversion failure, then test the inclusive application range before updating state. Specify invariant culture if the input is a protocol. Keep the old valid state on failure and distinguish invalid syntax from an out-of-domain number when that improves the diagnostic. Test zero, one hundred, negative input, one hundred one, and very long digits.
Key takeaways
- Static types and null-state analysis express intent without removing runtime boundary checks.
- Conversions occur at specific operations and can lose information.
- Absence, invalid input, and overflow need explicit domain policies.