Syntax, Values & Types
Phase 1 got a program to print and run - the warm-up. Real programs hold things: a name, a count, a price - and the rules C# uses to store those things shape almost everything you'll write next. Two ideas do the heavy lifting: the compiler knows the type of every value before your code runs, and C# splits all values into two camps - ones that copy when passed around, and ones that share. Get that split into your head now and a whole category of "why did this change?" bugs never happens to you.
Static typing - the compiler checks before you run
What it actually is. C# is statically typed: every variable has a fixed type locked in when declared, and the compiler verifies every use of it before the program runs. A variable holding a whole number can never later hold text - try it, and the build fails with a red squiggle, no runtime crash.
📝 Static means "checked at compile time," as opposed to dynamic ("checked while running"). C# checks up front, so by the time your program runs, "what type is this?" is already answered and can't go wrong.
int count = 0;
count = count + 1; // fine: an int plus an int is an int
count = "hello"; // compile error: cannot convert string to int
What just happened: int count = 0; told the compiler "count is an integer, forever." Adding to it is
fine; assigning the text "hello" is a contradiction the compiler catches instantly with
error CS0029: Cannot implicitly convert type 'string' to 'int'. A whole class of "I thought this was a
number but it was text" mistakes simply cannot ship.
Value types vs reference types - copy or share?
This is the idea in this phase.
📝 C# splits every type into two families:
- Value types hold their data directly. When you assign or pass one, you get a copy - a separate,
independent value. These include
int,double,bool,char, everystruct, and everyenum. - Reference types hold a reference (a pointer) to data that lives elsewhere in memory. When you assign
or pass one, you copy the reference, not the data - so both names now point at the same object.
These include every
class, plusstringand arrays.
Why the split? Small, simple values (a number, a flag) are cheapest to copy. Big or shared things (an object with many fields, a list everyone needs the same version of) are cheapest to pass by reference. The catch: copy-vs-share changes behavior, not just performance - what trips people up.
A struct is a value type; a class is a reference type:
struct PointVal { public int X; } // value type
class PointRef { public int X; } // reference type
var a = new PointVal { X = 1 };
var b = a; // COPY - b is independent
b.X = 99;
// a.X is still 1, b.X is 99
var c = new PointRef { X = 1 };
var d = c; // SHARE - d points at the same object as c
d.X = 99;
// c.X is now 99 too - same object
PointVal: a.X = 1, b.X = 99
PointRef: c.X = 99, d.X = 99
What just happened: b = a copied the whole struct, so b is independent - changing b.X left a
untouched. But d = c only copied the reference; c and d name one object, so writing through d
changed what c sees too. Same line of code (x = y), opposite result - the only difference is whether
the type is a struct (value) or a class (reference).
⚠️ The classic surprise: passing to a method. The same rule applies when you hand a value to a method. Pass a struct and the method works on a copy - your original is safe. Pass a class and it works on your actual object - changes leak back out.
struct Counter { public int N; }
class Box { public int N; }
void BumpStruct(Counter c) { c.N++; } // bumps a copy
void BumpClass(Box b) { b.N++; } // bumps the caller's object
var counter = new Counter { N = 0 };
BumpStruct(counter);
// counter.N is still 0 - the method changed its own copy
var box = new Box { N = 0 };
BumpClass(box);
// box.N is now 1 - the method reached the real object
counter.N = 0
box.N = 1
What just happened: BumpStruct received a copy of counter, incremented it, and threw it away -
counter never changed. BumpClass received a copy of the reference, still pointing at the caller's
box, so the increment stuck. A method that "didn't change my data" is almost always a value type; one
that "changed my data behind my back" is a reference type.
💡 Key point. Ask two questions about any type: Does assigning it copy or share? Does passing it to a
method protect my original or expose it? Value types copy and protect; reference types share and expose.
null only enters the picture for reference types and nullable value types (next) - only a reference can
point at "nothing."
var - let the compiler infer the type
Spelling out the type twice gets old fast: Dictionary<string, List<int>> map = new Dictionary<string, List<int>>(); is a mouthful. var lets the compiler infer the type from the right side:
var name = "Ada"; // compiler sees "Ada" is text → name is a string
var age = 36; // compiler sees 36 is a whole number → age is an int
var price = 9.99; // → double
var ready = true; // → bool
What just happened: Each var told the compiler to figure out the type from the right-hand side. name
is still a string - fully static, fully checked - you just didn't write the word. var isn't "any type"
or dynamic; it's shorthand for a type the compiler can already see. name = 42; afterward is still the
same compile error, because name's type was fixed the moment it was inferred.
💡 When to use it. Reach for var when the type is obvious from the right side (var user = new User();) or genuinely long to spell out. Prefer the explicit type when the value alone doesn't make it
clear (var result = Process(); - what is result?). The goal is readability, not saving keystrokes.
Nullability - the war on NullReferenceException
📝 null means "this reference points at nothing." Read a value off a null reference and you get a
NullReferenceException - historically C#'s single most common crash. Modern C# fights back on two fronts.
Nullable value types. Value types like int can't normally be null - they always hold a real number.
But sometimes you need "a number, or nothing yet" (an unanswered survey field, say). Add a ? to make a
nullable value type:
int score = 0; // always a number; cannot be null
int? maybeScore = null; // a number OR null - note the ?
if (maybeScore.HasValue)
Console.WriteLine(maybeScore.Value);
else
Console.WriteLine("no score yet");
no score yet
What just happened: int refuses null outright. int? (shorthand for Nullable<int>) adds one extra
state - "nothing" - modeling "not answered yet" plainly instead of faking it with -1 or 0.
Nullable reference types. Reference types have always been able to be null, exactly why crashes
were so common. Modern C# (<Nullable>enable</Nullable> in your project file, default for new projects)
flips this: you must opt in to nullability. A plain string means "never null"; string? means "might
be null" - and the compiler warns you whenever you risk dereferencing a possible null.
#nullable enable
string name = "Ada"; // promises: never null
string? note = null; // allowed to be null
Console.WriteLine(name.Length); // fine - name can't be null
Console.WriteLine(note.Length); // warning CS8602: possible null dereference
What just happened: With nullability enabled, name is non-nullable - the compiler trusts it's never
null and lets you use it freely. note is a string?, so reading note.Length earns a warning: "this
might be null, you didn't check." The compiler moves the NullReferenceException from a 3-a.m.
production crash to a squiggle in your editor. ⚠️ These are warnings, not errors, so it's tempting to
ignore them. Don't: each is a real crash the compiler spotted for you. (Deep dive on nullability and the
?. / ?? operators in Phase 13.)
Strings - text done the C# way
Strings get their own section for a couple of quietly important behaviors.
📝 A string is a reference type, but an immutable one: once created, its characters never change.
Every operation that looks like it modifies a string (uppercasing, replacing, concatenating) builds a
brand-new string and leaves the original alone.
The most useful everyday feature is string interpolation - prefix with $ and drop expressions right
inside { }:
var name = "Ada";
var age = 36;
var greeting = $"Hello {name}, you are {age} years old.";
Console.WriteLine(greeting);
Console.WriteLine($"Next year: {age + 1}"); // expressions work too
Hello Ada, you are 36 years old.
Next year: 37
What just happened: The $ turns the string into a template; each { ... } is evaluated and its result
dropped in. {age + 1} shows real expressions work inside, not just variable names - far cleaner than
gluing strings with +, and what you'll use almost every time.
⚠️ The equality gotcha - == on strings compares value. Coming from Java, this surprises people: in
Java, == compares references (same object?), so you need .equals(). In C#, == on strings is
overloaded to compare the actual text:
var a = "hello";
var part = "hel";
var b = part + "lo"; // built at runtime - a genuinely different object, same text
Console.WriteLine(a == b); // True - compares the characters
True
What just happened: a and b are different objects in memory (b is built at runtime, so it isn't the
same interned literal as a), but == on string compares their contents, so you get True. This is special to string - your own classes compare references by
default, a different story told in Phase 9. For now: strings compare by value
with ==, and that's the one you want.
Two more string forms: verbatim strings (@"...") turn off escape sequences, so backslashes and line
breaks are literal - perfect for Windows paths and regex. Raw string literals ("""...""") let you
paste multi-line text (JSON, SQL, HTML) without escaping anything:
var path = @"C:\Users\Ada\file.txt"; // no doubled backslashes needed
var json = """
{ "name": "Ada", "age": 36 }
"""; // quotes inside need no escaping
Console.WriteLine(path);
Console.WriteLine(json);
C:\Users\Ada\file.txt
{ "name": "Ada", "age": 36 }
What just happened: In a normal string, "C:\Users" would treat \U as an escape and break. @ says
"take this literally," so backslashes stand as-is. The """ raw string holds a block with embedded "
quotes and newlines, no escaping required - the compiler even strips leading indentation up to the closing
""". Reach for @"..." for paths and """...""" for chunks of structured text.
Recap
- C# is statically typed - every variable's type is fixed at declaration and checked at compile time, so type mismatches fail the build instead of crashing at runtime.
- Value vs reference is the big split. Value types (
int,double,bool,struct,enum) hold data directly and copy on assignment and method calls. Reference types (class,string, arrays) hold a reference and share - both names point at the same object. - ⚠️ Passing a struct to a method protects your original (it gets a copy); passing a class exposes it (the method reaches your real object).
varinfers the type from the right-hand side - still fully static, just less typing. Use it when the type is obvious.- Nullability:
int?adds "nothing" to a value type; modern nullable reference types makestringnon-null andstring?maybe-null, with compiler warnings to killNullReferenceExceptionearly. - Strings are immutable reference types. Use
$"..."interpolation; remember==compares value (unlike Java); use@"..."for paths and"""..."""for multi-line text.
Next, we move from single values to collections - arrays, the List<T> you'll actually use, and
dictionaries for looking things up by key.
Quick check
Test yourself on the idea that matters most - copy versus share:
[
{
"q": "You have a `struct Point` and write `var b = a;` then `b.X = 99;`. What is `a.X`?",
"choices": [
"Unchanged - a struct is a value type, so `b = a` made an independent copy",
"99 - `b` and `a` point at the same object",
"0 - assigning a struct resets its fields",
"A compile error - you can't copy a struct"
],
"answer": 0,
"explain": "A struct is a value type: assignment copies the whole value, so `b` is independent of `a`. Changing `b.X` leaves `a.X` exactly as it was. If `Point` were a `class` (reference type), `b = a` would share the object and `a.X` would become 99 too."
},
{
"q": "With nullable reference types enabled, what's the difference between `string` and `string?`?",
"choices": [
"`string` is non-nullable (the compiler warns if it might be null); `string?` is allowed to be null",
"`string` is a value type and `string?` is a reference type",
"There is no difference - the `?` is just a style preference",
"`string?` is faster because it skips null checks"
],
"answer": 0,
"explain": "Under `<Nullable>enable</Nullable>`, a plain `string` promises it's never null and the compiler warns when you risk dereferencing a possible null. `string?` explicitly permits null. The point is to surface NullReferenceException risks as compile-time warnings instead of runtime crashes."
},
{
"q": "In C#, what does `==` do when both operands are strings?",
"choices": [
"Compares the actual text (their characters) for equality",
"Compares whether they're the same object in memory, like Java",
"Always returns false unless they're literally the same literal",
"Throws an exception - you must use `.Equals()` for strings"
],
"answer": 0,
"explain": "`==` is overloaded for `string` to compare value (the characters), so two different string objects with the same text are equal. This differs from Java, where `==` compares references. Note that for your own classes, `==` compares references by default - string is the special case."
}
]
Before the quiz: without looking back, say (or jot down) the core idea of this phase in your own words.
Check your understanding 3 questions
1. You have a `struct Point` and write `var b = a;` then `b.X = 99;`. What is `a.X`?
2. With nullable reference types enabled, what's the difference between `string` and `string?`?
3. In C#, what does `==` do when both operands are strings?