C# Basics: Types, Variables, and Control Flow
Learn C# basics for backend work: value vs reference types, var, string interpolation, if and switch expressions, loops, and pattern matching.
var is compile-time inference, strings are immutable, and switch is an expression. Everything else in the language builds on those five facts.You arrive from Java, Python, or JavaScript, open your first C# file, and see a mix that looks inconsistent: var on one line, an explicit type on the next, a switch that returns a value. Then your first real bug: two variables point at the same list, code appends through one of them, and both change. Add a NullReferenceException at runtime in a language that promised types, and the language starts to feel arbitrary. None of it is arbitrary. It is the value vs reference distinction, and internalizing it first makes everything else click.
C# basics here means the type system and control flow vocabulary you need before touching any framework: how values live in memory, how to declare variables, how strings work, and how to branch and loop. It assumes nothing about .NET; every example is a standalone console program.
The examples run on .NET SDK 10.0 with C# 14, the current LTS pairing. If you are still choosing which .NET release to target, the LTS vs STS guide settles it. To follow along, create a project with dotnet new console; the first-project guide walks through the install if anything is missing.
Having mentored several backend developers into the language, I have seen what matters: C# rewards learning the memory model first and the syntax second. Most “weird C# behavior” traces back to value vs reference, and most “unreadable C#” is just missing type names. Get those two right and the language stops feeling foreign within a week.
C# basics: value types vs reference types
Every C# type is one of two kinds, and the difference is about memory, not syntax. Value types hold their data directly; assigning one copies the data. Reference types hold a reference to an object on the heap. Assigning one copies the reference, so both variables point at the same object. Microsoft’s reference pages describe the exact rules for value types and reference types.
| Type | Kind | Example | Notes |
|---|---|---|---|
int, long |
Value | 42 |
32-bit and 64-bit integers |
double, decimal |
Value | 3.14 |
decimal for money, double for science |
bool |
Value | true |
Never “truthy” shortcuts like JavaScript |
char |
Value | 'a' |
Single character, single quotes |
string |
Reference | "hello" |
Immutable; behaves like a value in practice |
List<T>, arrays, classes |
Reference | new List<int>() |
Copies share the object behind them |
Watch the difference in code:
int a = 10;
int b = a; // copies the value
b++; // a is 10, b is 11
var list1 = new List<int> { 1, 2, 3 };
var list2 = list1; // copies the reference
list2.Add(4); // list1 AND list2 now contain 4
VALUE TYPES REFERENCE TYPES
int a = 10; var t1 = new TaskItem();
stack: [ a: 10 ] stack: [ t1: ref @42 ]
copy = a makes a NEW value heap: [ @42 TaskItem object ]
copy = t1 follows the SAME @42
This is also where the infamous NullReferenceException comes from. A reference variable can point at nothing (null), and calling a member on it throws. C# can make that risk visible at compile time; the guide on nullable reference types shows how to switch those checks on.
Declaring variables and the var keyword
var is the most misunderstood keyword in the language, mostly by people arriving from JavaScript. In C#, var is compile-time type inference. The compiler works out the type from the right-hand side and then the variable is that type, permanently. It is not dynamic; assigning a string to a var that was inferred as int is a compile error.
var count = 3; // int, forever
var title = "Ship the API"; // string
var done = false; // bool
count = "three"; // compile error: cannot implicitly convert
const int MaxRetries = 5; // constants are fixed at compile time
Use var when the type is obvious from the same line (var list = new List<int>();) and the explicit type name when a method call hides it (TaskSummary summary = service.Summarize();). The reader, not the compiler, is who benefits from the type name.
Strings and interpolation
Strings in C# are immutable reference types: every method returns a new string and leaves the original untouched. To build strings, use interpolation with the $ prefix, which is the idiomatic form:
var name = "deploy";
var count = 3;
var price = 19.99m;
var progress = 0.8333;
Console.WriteLine($"Task '{name}' has {count} items");
Console.WriteLine($"Total: {price:C}"); // currency, culture-aware
Console.WriteLine($"Done: {progress:P1}"); // percentage, 1 decimal
Format specifiers after a colon handle currency, percentages, and dates. Literal braces need doubling. For file paths, the @ prefix turns off escape sequences: @"C:\logs\app.log". For embedded JSON and multiline text, raw string literals use triple quotes:
var json = """
{
"service": "TaskApi",
"version": 1
}
""";
Console.WriteLine(json);
Because strings are immutable, concatenating inside a loop creates a new string per iteration. A StringBuilder is the fix when the loop is long. For one-off messages, interpolation is easier to read than any alternative, so leave it alone.
Control flow: if, else, and switch expressions
The if/else pair needs no translation from other languages:
if (count == 0)
{
Console.WriteLine("Nothing to do");
}
else if (count < 10)
{
Console.WriteLine("Small batch");
}
else
{
Console.WriteLine("Big batch");
}
The modern half of the language is the switch expression, introduced in C# 8. It maps an input to an output value, and its arms compose patterns: constants, ranges, and logical combinations:
var statusCode = 404;
string label = statusCode switch
{
200 => "OK",
301 or 302 => "redirect",
404 => "not found",
>= 500 => "server error",
_ => "other"
};
The _ arm is the fallback. Switch on an enum and the compiler even warns when a member is missing, so adding a new value surfaces at build time instead of in production. Pattern matching also works with the is keyword, which tests a type and binds a typed variable in one step:
object value = 42;
if (value is int number)
{
Console.WriteLine(number + 1); // 43
}
The official pattern matching tutorial extends these to property patterns and list patterns, which are the same idea with more conditions.
Loops: for, foreach, while
for (int i = 0; i < 3; i++) // counted loop
{
Console.WriteLine($"iteration {i}");
}
foreach (var task in tasks) // iterate a collection
{
Console.WriteLine(task);
}
while (tasks.Count > 10) // loop on a condition
{
tasks.RemoveAt(tasks.Count - 1);
}
The rule of thumb: foreach when you have a collection and no index, for when you need the index or a fixed count, while when the exit is a condition rather than a counter. One sharp edge: modifying a collection while enumerating it throws InvalidOperationException with the message “Collection was modified; enumeration operation may not execute”. Collect the changes first, apply them after.
Many loops are not loops at all in idiomatic C#: filtering and transforming a list is usually a LINQ query. LINQ in C# covers that style in depth.
A complete example: a tiny task tracker
Everything on this page in one runnable program. Create a console project with dotnet new console, replace Program.cs, and run dotnet run:
var tasks = new List<string>
{
"Ship the API",
"Write the migration",
"Review the pull request"
};
Console.WriteLine("Task tracker");
Console.WriteLine("------------");
foreach (var task in tasks)
{
Console.WriteLine($"- {task}");
}
while (true)
{
Console.Write("Add a task (blank line to quit): ");
var input = Console.ReadLine();
if (string.IsNullOrWhiteSpace(input))
{
break;
}
tasks.Add(input);
Console.WriteLine($"Added '{input}'. Total: {tasks.Count}");
}
string summary = tasks.Count switch
{
0 => "No tasks on the list.",
1 => "One task on the list.",
_ => $"{tasks.Count} tasks on the list."
};
Console.WriteLine(summary);
Expected start:
Task tracker
------------
- Ship the API
- Write the migration
- Review the pull request
Add a task (blank line to quit):
Value types (int, bool), a reference type shared through tasks, var declarations, interpolation, an if guard inside a while, and a switch expression at the end: the whole vocabulary in twenty lines.
How real systems do this
In production services these basics show up in three recurring shapes:
- Switch expressions map external values. Status codes, provider responses, and environment names arrive as integers or strings and leave as domain values. A switch expression keeps the mapping in one place with a compile-time check.
- TryParse guards every boundary. Configuration and request input never go through
int.Parsedirectly, because one malformed value throws aFormatException.int.TryParsereturns a bool and an out value instead:
var raw = Environment.GetEnvironmentVariable("MAX_RETRIES");
if (int.TryParse(raw, out var maxRetries))
{
Console.WriteLine($"Retries: {maxRetries}");
}
else
{
Console.WriteLine("MAX_RETRIES is missing or not a number");
}
A mistake I have seen in production is a helper method that sorted the List<T> a caller passed in. The sort worked, the tests passed, and the caller’s list silently changed order, producing a confusing bug two layers up. Copying the list before sorting fixed it. What stuck with me: when a method mutates a reference it does not own, somebody far away pays for it.
Decision framework: which construct to reach for
- Shared or self-contained? If two parts of the program must see changes to the same object, a reference type is exactly right. If the value just travels, a value type or a fresh copy avoids aliasing surprises.
- Is the type visible on the assignment line? Use
varwhen the right side names the type. Write the type out when a method call hides it. - Mapping or deciding? One input to one output, no side effects: a switch expression. Actions, nesting, or complex guards:
if/else. - Iterating or counting? A collection you only read:
foreach. An index or fixed count:for. An exit condition:while. - Building strings across many iterations? One-off: interpolation. Inside a loop:
StringBuilder.
When NOT to follow the idioms
- When var hides the type. A line like
var result = service.Process(input);forces the reader to jump to a definition. Write the type name instead.varis for lines where the type is visible, not a shortcut for skipping thought. - When branches carry side effects. Switch expressions read as pure mapping. If a case needs to log, call a service, or throw selectively, an
if/elsestatement says what happens more honestly. - When you need the index.
foreachhas no counter. Aforloop beats maintaining a manual index variable beside a foreach, which is a common source of off-by-one bugs.
Common mistakes
- Treating var as dynamic.
varfixes the type at compile time. Reassigning a different type fails the build, exactly like an explicit declaration. - Ignoring string immutability.
text.Replace("a", "b")returns a new string; discarding the return value changes nothing and ships a silent no-op. - Mutating a collection inside foreach. The runtime throws
InvalidOperationException: “Collection was modified”; collect changes first and apply them after. - Integer division surprises.
5 / 2is2, not2.5. Cast one side todoublewhen fractions matter, a classic bug in progress and rate calculations. - Parsing raw input with int.Parse. One malformed environment variable and the process throws
FormatException.int.TryParsetreats bad input as a normal outcome. - Sharing a List and mutating it. Reference types mean the caller sees your changes. Copy the list when you don’t own it.
Key takeaways
- Value types copy on assignment; reference types share the same object;
stringis an immutable reference type. varis compile-time inference, not dynamic typing; the variable’s type is fixed from the moment of declaration.- Build strings with
$"..."interpolation, format specifiers, and raw string literals; switch toStringBuilderinside loops. - Prefer switch expressions for mapping values to values; keep side effects in
if/elsestatements. foreachfor collections,forfor indexes,whilefor conditions; never mutate a collection you are enumerating.- Every boundary value goes through
TryParse, and aNullReferenceExceptionalways means a reference variable heldnull.
FAQ
Is var in C# the same as var in JavaScript?
No. In C# the compiler infers the exact type at compile time and the variable keeps that type forever. Assigning a different type is a build error. JavaScript’s var is dynamically typed and lets the same variable hold anything, which C# never allows.
What is the difference between value and reference types in C#?
Assigning a value type copies the data, so the two variables are independent. Assigning a reference type copies the reference, so both variables point at one shared object and changes are visible through either side. int, bool, and double are value types. Classes, arrays, and lists are reference types.
How do I format and concatenate strings in C#?
Use interpolation: prefix the string with $ and put expressions in braces, like $”Total: {price:C}”. Format specifiers after a colon handle currency, percentages, and dates. For long loops, use StringBuilder, because strings are immutable and concatenation creates a new string each iteration.
What is a switch expression in C#?
A form of switch introduced in C# 8 that produces a value instead of executing cases. It maps an input through patterns, including constants like 200, ranges like >= 500, and combinations like 301 or 302, and returns one result per arm. It suits mapping decisions without side effects.
Should I use if/else or switch in C#?
Use if/else for conditions with actions or complex guards, and a switch expression when you map an input to an output value. As soon as you compare the same variable against many constants, the switch expression version is shorter and the compiler checks the cases for you.
Learn the memory model first
Everything else in the language builds on these five facts: value types copy, reference types share, var infers at compile time, strings never change, and switch maps values to values. When behavior surprises you, ask which of the five facts you are standing on. The natural next step is object-oriented C#: classes, inheritance, and interfaces, and every guide in the series is on the .NET series index. The rule to carry forward: value types copy, reference types share, and when in doubt, print what the variable actually points at.
Last updated on 12 October 2026
