C# is a statically-typed language — every variable has a type fixed at compile time, and the compiler rejects type mismatches before the program ever runs. This catches an entire class of bugs that dynamic languages only discover at runtime. C# has two fundamental categories of types: value types (stored directly on the stack, copied on assignment) and reference types (allocated on the heap, assigned by reference). Understanding this distinction is essential to writing correct C# across every part of this series.
Built-In Value Types
// ── Integer types ────────────────────────────────────────────────────────────
int age = 30; // 32-bit signed — most common whole-number type
long views = 9_000_000L; // 64-bit signed — L suffix marks the literal as long
short s = 32_767; // 16-bit signed (rarely needed)
byte b = 255; // 8-bit unsigned: 0–255 (e.g. RGB colour channels)
// ── Floating-point types ──────────────────────────────────────────────────────
double pi = 3.14159265358979; // 64-bit — DEFAULT for decimal literals
float temp = 98.6f; // 32-bit — f suffix is REQUIRED
decimal price = 19.99m; // 128-bit exact decimal — USE FOR CURRENCY
// ── Other value types ─────────────────────────────────────────────────────────
bool isActive = true;
char grade = 'A'; // single Unicode character (single quotes)
// ── Implicit typing with var — type still fixed at compile time ───────────────
var count = 0; // inferred as int
var message = "Hello"; // inferred as string
var total = 100.0m; // inferred as decimal
Note: Always use
decimal for monetary values — never double or float. Binary floating-point types cannot represent many decimal fractions exactly: 0.1 + 0.2 as a double yields 0.30000000000000004. The decimal type uses base-10 arithmetic and represents values like 0.1 exactly, which is essential for price calculations, tax rates, and financial reports. This is one of the most common bugs in enterprise applications. The m suffix is required: decimal price = 19.99m — without it the compiler sees a double literal and refuses the assignment.Tip: Use
var when the type is obvious from context — var users = new List<User>() is perfectly clear. Avoid it when the right-hand side does not reveal the type: var result = GetData() forces the reader to look up the return type. In ASP.NET Core controller methods and EF Core queries, var is idiomatic because the types are obvious from the method being called. Visual Studio and VS Code both show the inferred type on hover.Warning: Numeric literals have default types: integer literals like
42 are int; decimal literals like 3.14 are double. Suffixes override this: 42L is long, 3.14f is float, 3.14m is decimal. Forgetting the m suffix causes a compile error: decimal price = 19.99 fails because 19.99 is a double literal and cannot be implicitly converted to decimal. This error appears frequently when developers first work with the decimal type.Reference Types and Nullable
// Reference types are allocated on the heap; variables hold a reference (pointer)
string name = "Alice"; // immutable reference type
int[] scores = { 95, 87, 92 }; // array on the heap
// ── Nullable value types (Nullable<T> shorthand: T?) ─────────────────────────
int? optionalAge = null; // can hold an int value or null
double? rating = null;
if (optionalAge.HasValue)
Console.WriteLine($"Age: {optionalAge.Value}");
Console.WriteLine($"Age: {optionalAge ?? 0}"); // 0 if null
// ── Null operators ────────────────────────────────────────────────────────────
string? middleName = null;
string display = middleName ?? "N/A"; // ?? returns right side if left is null
int? len = middleName?.Length; // ?. returns null if left side is null
middleName ??= "Unknown"; // ??= assigns only if currently null
// ── Nullable reference types (enabled by <Nullable>enable in .csproj) ─────────
string required = "must have a value"; // non-nullable — compiler warns on null
string? optional = null; // explicitly nullable — must check before use
// ── Type conversion ────────────────────────────────────────────────────────────
int i = 42;
double d = i; // implicit widening — safe, no data loss
int back = (int)d; // explicit narrowing cast — may truncate
string text = i.ToString(); // every type supports ToString()
int parsed = int.Parse("42"); // throws on invalid
bool ok = int.TryParse("abc", out int result); // returns false, result = 0
Value vs Reference — Assignment Behaviour
// Value types are COPIED — independent after assignment
int a = 10;
int b = a; // b is a separate copy
b = 20;
Console.WriteLine(a); // 10 — a is unchanged
// Reference types SHARE the same heap object after assignment
int[] arr1 = { 1, 2, 3 };
int[] arr2 = arr1; // both point to the SAME array
arr2[0] = 99;
Console.WriteLine(arr1[0]); // 99 — arr1 sees the change!
// string is a reference type but IMMUTABLE — effectively behaves like a value type
string s1 = "hello";
string s2 = s1;
s2 = "world"; // creates a NEW string object — s1 is unchanged
Console.WriteLine(s1); // "hello"
Quick Reference
| Type | Category | Use For |
|---|---|---|
int |
Value / 32-bit | Counts, IDs, general whole numbers |
long |
Value / 64-bit | Large whole numbers, file sizes |
decimal |
Value / 128-bit | Currency and financial values |
double |
Value / 64-bit | Scientific floats — NOT money |
bool |
Value | True/false flags |
string |
Reference (immutable) | All text |
T[] |
Reference (fixed size) | Typed fixed-size collections |
int? |
Nullable value | Optional numbers, nullable DB columns |
Common Mistakes
Mistake 1 — Using double for currency
❌ Wrong:
double subtotal = 10.10 + 9.90;
Console.WriteLine(subtotal); // 20.000000000000004 — floating-point imprecision!
✅ Correct:
decimal subtotal = 10.10m + 9.90m;
Console.WriteLine(subtotal); // 20.00 — exact
Mistake 2 — NullReferenceException on nullable strings
❌ Wrong:
string? name = null;
Console.WriteLine(name.Length); // NullReferenceException at runtime!
✅ Correct:
Console.WriteLine(name?.Length ?? 0); // null-safe: prints 0