1A classification that defines what kind of data a value is, what it can hold, and which operations are legal on it.
e.g.5 is an int, "hello" is a string, true is a bool. You can add two ints — but C# won't let you add an int and a bool.
Programming in Unity · Module 01 · Part 2
Types, operators, functions, control flow and memory — the core machinery of C#. Almost everything a program does, it does by combining these eight ideas.
Lorenzo Morini · Lead Unity Developer
C# Fundamentals · June 2026
Section 06
Types
1A classification that defines what kind of data a value is, what it can hold, and which operations are legal on it.
e.g.5 is an int, "hello" is a string, true is a bool. You can add two ints — but C# won't let you add an int and a bool.
Types
C# is statically typed: it verifies every type at compile time — before the program runs — instead of discovering mismatches mid-execution. Moving the check to the very start buys four things:
Types
Every type in C# is either a value type (the variable holds the data itself) or a reference type (the variable holds an address pointing to data that lives elsewhere). That single split shapes how data is copied, compared and stored.
flowchart TD
T["C# type"] --> V["Value type — holds the data"]
T --> R["Reference type — holds an address"]
V --> ST["struct"]
V --> E["enum"]
R --> C["class"]
R --> I["interface"]Types
Assigning a value type copies the data; the two variables are now independent. Assigning a reference type copies the address, so both names point at the same object.
int a = 10;
int b = a; // b gets a copy
b = 99; // a is untouched
// a is still 10
int[] x = { 1, 2, 3 };
int[] y = x; // same array
y[0] = 99;
// x[0] is now 99 too
Types
This one idea quietly decides everyday behaviour:
Keep it in the back of your mind — and we'll make it physical in the Memory section, where you'll see exactly where each family lives.
Types
C# bakes in a handful of simple types for the most basic data — whole numbers, decimals, single characters, true/false — each with a short keyword name. Every keyword is just a friendly alias for a .NET type: int really is System.Int32.
int health = 100;
double speed = 5.5;
char grade = 'A';
bool isAlive = true;Simple Types
People borrow the word "primitive" from languages like Java, but in C# it's not quite right. C# has no formal "primitive" category — its simple types are really structs: full value types with methods of their own.
You can write 5.ToString() or read int.MaxValue; a true primitive couldn't do that. So the accurate term is simple type.
Types
Picking a number type is a trade between range (how big) and precision (how exact).
32-bit whole number, roughly ±2.1 billion. No suffix. Your default for counting — scores, lives, indices.
32-bit decimal, ~6–7 digits of precision. Suffix f. Unity's default for positions, speeds, distances.
64-bit decimal, ~15–16 digits. The default for any decimal literal. General-purpose maths.
128-bit, exact decimal, 28–29 digits. Suffix m. Money — no binary rounding error.
Numeric Types
A bare decimal literal is a double; a bare whole number is an int. The suffix tells the compiler you meant something else.
int lives = 3;
float gravity = -9.81f; // f — without it, 9.81 is a double
double pi = 3.14159; // double by default
decimal price = 19.99m; // m — exact, for currency
double pi = 3.14159; // fine, 3.14159 is already double
float gravity = -9.81; // ERROR — can't assign double to float without losing precision
decimal price = 19.99; // ERROR — can't implicitly convert double to decimal
Integer division truncates (7 / 2 is 3), so promote to a decimal type when you need the fraction: 7 / 2.0 is 3.5.
Types
1A value type that holds exactly one of two values — true or false.
e.g.A bool named isGameOver is false while the player is alive, and becomes true the moment they lose.
bool
A bool usually comes from a comparison, and it drives every conditional and loop your program runs.
bool isAlive = true;
bool canEnter = health > 0; // a comparison produces a bool
if (isAlive)
{
Console.WriteLine("Keep playing");
}Types
1A value type that holds exactly one character, written between single quotes.
e.g.'A', '7', '?' and ' ' (a space) are all valid char values.
char
A char is the building block strings are made of. Its literal uses single quotes — 'A' — while a string uses double quotes — "A". Don't mix them up.
Under the hood a char is a 16-bit Unicode (UTF-16) code unit — so each character is a number you can do arithmetic on.
char letter = 'A';
int code = letter; // 65 — the Unicode value of 'A'Types
1A value type that defines a named set of related constants, making code more readable and harder to get wrong.
e.g.enum Direction { North, East, South, West } lets you write Direction.North instead of a bare 0.
Enums
Declare the set of names, then use them by name. The code reads like English, and the compiler refuses any value outside the set.
enum Direction { North, East, South, West }
Direction facing = Direction.North;
if (facing == Direction.North)
{
Console.WriteLine("Heading north");
}Enums
Each member is really a number — by default an int starting at 0. You can set the values, change the underlying type, or mark it [Flags] to combine members like switches (give each a distinct power of two).
enum Difficulty : byte
{
Easy = 1,
Normal = 2,
Hard = 4
}
[Flags]
enum Toppings
{
None = 0,
Cheese = 1,
Bacon = 2
}
var o = Toppings.Cheese | Toppings.Bacon;
Types
A string is a sequence of characters — player names, dialogue, anything textual. Unlike the numeric and char types it's a reference type: the variable holds a reference to the text, which lives elsewhere.
Yet it behaves like a simple value, because strings are immutable — once created they never change. You can share one freely without fear someone will alter it underneath you. (Full treatment comes in its own section later.)
string
Write strings in double quotes. Join them with +, or — better — use $ interpolation to drop values straight in.
string name = "Aria";
string greeting = "Hello, " + name;
string line = $"{name} scored {1500} points";
That's enough to get going. Immutability, memory cost and StringBuilder get a section of their own.
Types
An array holds a fixed-size, ordered collection of values that all share one type — a row of numbered boxes. It's a reference type, so the variable points to the block of elements; copying the variable shares the data.
int[] scores = new int[3]; // three slots, all 0
scores[0] = 100;
string[] names = { "Aria", "Ben", "Cleo" };
Console.WriteLine(names[1]); // Ben — indexing starts at 0
Recognise the type[] syntax and the zero-based indexing for now; arrays get a dedicated section later.
Section 07
Operators
An operator is a symbol that performs an action on one or more values — its operands. You've already met = and +. The common ones fall into a few families.
Operators
int sum = 5 + 3; // 8</div>
<div class="column"><h3>Assignment</h3>
<pre><code>int x = 10;</div>
</div>
int diff = 5 - 3; // 2
int prod = 5 * 3; // 15
int quot = 7 / 2; // 3 (truncates)
int rem = 7 % 2; // 1 (remainder)
x += 5; // x = x + 5 -> 15
x -= 3; // -> 12
x *= 2; // -> 24
x++; // increment -> 25
% (modulo) gives the remainder of a division — perfect for "every Nth" logic and wrapping values. The compound forms (+=, *=, …) change a variable based on its own current value.
Operators
5 == 5 // equal true</div>
<div class="column"><h3>Logical — combine bools</h3>
<pre><code>hasKey && isUnlocked // both</div>
</div>
5 != 3 // not equal true
5 > 3 // greater true
3 <= 2 // less or equal false
isSafe || hasShield // either
!isFriendly // flip
Comparisons are the source of nearly every condition you write. && and || short-circuit — they stop the moment the answer is certain, which also lets you guard against errors (obj != null && obj.IsReady).
Operators
Often you need a value as a different type — an int used as a double, or text turned into a number. C# is strict about when this happens automatically and when you must ask for it.
Type Conversion
A smaller type widens into a larger one automatically.
int whole = 7;
double precise = whole; // 7 -> 7.0
When a conversion might lose data, you must cast with (type).
double d = 9.8;
int n = (int)d; // 9 — truncated
Casting between number types truncates; it doesn't round.
Type Conversion
To move between text and numbers, use library methods rather than casts:
int n = int.Parse("42"); // string -> int (throws on bad input)
bool ok = int.TryParse("42", out n); // safe: reports failure, never crashes
string s = 42.ToString(); // int -> string
double d = Convert.ToDouble("3.14");
Prefer TryParse for anything a user typed — it returns false instead of throwing.
Operators
Languages differ along two independent axes, and together they decide how many mistakes the compiler can catch for you:
Two Axes of Typing
Every variable has a known type at compile time, and illegal mixes are rejected before the program ever runs.
int x = 5;
x = "hello"; // compile error — caught immediately
A weakly typed language might have quietly allowed that and misbehaved much later. The trade is a little more typing up front, in exchange for fewer runtime surprises and far better tooling. For games, catching a type bug at compile time beats finding it during a live demo.
Section 08
Functions
1A named, reusable block of code that takes optional inputs, performs a task, and optionally returns a result.
e.g.A Square function takes a number and returns it multiplied by itself — write it once, call it anywhere.
Functions
Strictly, C# has no free-floating functions — every function belongs to a type and is called a method (we meet those properly in the OOP section).
But top-level statements let us declare local functions and call them as if they stood alone. It's a convenient pretence, so we can learn the idea before learning classes.
Functions
Parameters are the named inputs in a function's definition. Arguments are the actual values you pass when you call it.
void Greet(string name) // 'name' is a parameter
{
Console.WriteLine($"Hello, {name}");
}
Greet("Aria"); // "Aria" is the argumentFunctions
A function's return type declares what it hands back; return sends that value and ends the function. A function that acts but hands nothing back has the return type void.
int Square(int n)
{
return n * n;
}
int r = Square(5); // 25
void PrintScore(int score)
{
Console.WriteLine($"Score: {score}");
}
PrintScore(1500);
Functions
Pass arguments by name to make a call self-documenting and order-free. Give a parameter a default value and callers may omit it — optional parameters must come after all required ones.
void Attack(int damage, bool critical = false) { /* ... */ }
Attack(10); // critical defaults to false
Attack(10, critical: true); // named — clear at the call site
Attack(damage: 25); // order wouldn't matterFunctions
params lets a function accept any number of arguments of a type, gathered into an array — great when the count isn't fixed.
int Sum(params int[] numbers)
{
int total = 0;
foreach (int n in numbers) total += n;
return total;
}
Sum(1, 2, 3); // 6
Sum(10, 20, 30, 40); // 100Functions
Normally value-type arguments are copied. ref passes a variable by reference, so the function can change the caller's variable. out is similar, used to hand back extra results.
void Double(ref int n) { n *= 2; }
int x = 5;
Double(ref x); // x is now 10
bool ok = int.TryParse("42", out int parsed); // out returns the parsed valueFunctions
When a function is a single expression, => gives a compact one-line form — no braces, no return.
int Square(int n) => n * n;
string Greet(string name) => $"Hello, {name}";
Identical behaviour to the block form, just tidier for short functions.
Functions
flowchart TD A["Factorial(4)"] --> B["4 × Factorial(3)"] B --> C["3 × Factorial(2)"] C --> D["2 × Factorial(1)"] D --> E["1 — base case"] E -->|unwind| F["= 24"]
A recursive function calls itself to solve a smaller version of the same problem. Every recursion needs a base case that stops it — or it loops forever.
int Factorial(int n)</div>
</div>
{
if (n <= 1) return 1; // base case
return n * Factorial(n - 1); // recursive step
}
Factorial(4); // 24
Section 09
Scope and Lifetime
Scope is where a name is visible; lifetime is how long its value exists in memory. The two move together — a variable usually lives exactly as long as the block it's declared in is running.
Outside its region, the compiler doesn't even know the name exists. That's what stops unrelated parts of a program from stepping on each other's names.
Scope and Lifetime
void Play()</div>
<div class="column"><h3>Local to a block</h3>
<pre><code>if (isAlive)</div>
</div>
{
int score = 0; // local to Play
score += 10;
} // score is gone here
{
int bonus = 50; // only inside { }
score += bonus;
}
// bonus not accessible here
A block is any region between { } — a loop body, an if, or a bare pair of braces. Each function call gets its own fresh copy of its locals.
Scope and Lifetime
Shadowing is when an inner scope declares a name that already exists in an outer one, hiding it for that region. C# forbids it for locals to prevent confusion.
int value = 10;
{
int value = 20; // error — already declared in an enclosing scope
}
It can still happen between a field and a local — one more reason clear names matter.
Section 10
Conditionals
A conditional runs code only when a condition is true. It's the difference between a script that always does the same thing and one that reacts — the heart of every behaviour you'll write.
Conditionals
flowchart TD
A([score]) --> B{"score >= 90 ?"}
B -->|yes| C["Gold"]
B -->|no| D{"score >= 50 ?"}
D -->|yes| E["Silver"]
D -->|no| F["Try again"]if runs its block when the condition is true. else if offers another test; else catches everything left. Only the first matching branch runs.
if (score >= 90)</div>
</div>
{
Console.WriteLine("Gold");
}
else if (score >= 50)
{
Console.WriteLine("Silver");
}
else
{
Console.WriteLine("Try again");
}
Conditionals
For a simple either/or that produces a value, the ternary operator ?: is a compact inline if/else. Read it as "condition ? value-if-true : value-if-false".
int health = 30;
string status = health > 0 ? "Alive" : "Dead"; // "Alive"
Perfect for short assignments. For anything with real branching, reach back for a full if.
Conditionals
An if can live inside another, checking a second condition only once the first holds. Useful — but deep nesting hurts readability, so keep it shallow.
if (isLoggedIn)
{
if (hasPermission)
{
OpenEditor();
}
}Conditionals
When you compare one value against many fixed options, switch is cleaner than a long if/else if chain. The modern expression form returns a value directly.
switch (direction)
{
case "N": Move(0, 1); break;
case "S": Move(0, -1); break;
default: Stay(); break;
}
string label = score switch
{
>= 90 => "Gold",
>= 50 => "Silver",
_ => "Try again"
};
The expression form uses => arms and _ as the catch-all — concise for mapping one value to another.
Section 11
Arrays
You met arrays earlier; now the details. Size an array up front and fill it later, or initialize it with values immediately. Its length is fixed once created — it can't grow.
int[] a = new int[5]; // five zeros
int[] b = new int[] { 1, 2, 3 }; // sized from the values
int[] c = { 10, 20, 30 }; // shorthand for the sameArrays
Reach an element by its index, counting from 0. The last index is Length - 1; going out of range throws at run time.
string[] names = { "Aria", "Ben", "Cleo" };
Console.WriteLine(names[0]); // Aria
names[2] = "Cara"; // overwrite Cleo
Console.WriteLine(names.Length); // 3
// names[3] -> IndexOutOfRangeExceptionArrays
Arrays can have more than one dimension — handy for grids, boards and tile maps. A 2-D array uses [,] and two indices: row and column.
int[,] grid = new int[2, 3]; // 2 rows, 3 columns
grid[0, 0] = 5;
grid[1, 2] = 9;
int[,] board = { { 1, 2 }, { 3, 4 } };
Section 12
Loops
A loop runs a block of code repeatedly — to process every item in a collection, count to a number, or keep a game running frame after frame. C# offers four, each suited to a different question.
Loops
flowchart TD
A([Start]) --> B["i = 0"]
B --> C{"i < 5 ?"}
C -->|yes| D["print i"]
D --> E["i++"]
E --> C
C -->|no| F([Done])The for loop is ideal when you know how many times to repeat. It bundles three parts: a start value, a continue condition, and a step.
for (int i = 0; i < 5; i++)</div>
</div>
{
Console.WriteLine(i);
}
Start at 0, keep going while i < 5, add 1 each pass.
Loops
flowchart TD
A([Start]) --> B{"countdown > 0 ?"}
B -->|yes| C["print countdown"]
C --> D["countdown--"]
D --> B
B -->|no| E([Done])A while loop repeats as long as a condition stays true, checking it before each pass. Use it when you don't know the count in advance.
int countdown = 3;</div>
</div>
while (countdown > 0)
{
Console.WriteLine(countdown);
countdown--;
}
Make sure something changes inside, or it never ends.
Loops
flowchart TD
A([Start]) --> B["read input"]
B --> C{"input != quit ?"}
C -->|yes| B
C -->|no| D([Done])A do-while loop checks its condition after the body, so the body always runs at least once. Perfect for menus and input prompts.
string input;</div>
</div>
do
{
input = Console.ReadLine();
}
while (input != "quit");
Loops
flowchart TD
A([Start]) --> B{"more items ?"}
B -->|yes| C["name = next item"]
C --> D["print name"]
D --> B
B -->|no| E([Done])The foreach loop walks every item in a collection, with no index to manage. It's the cleanest way to read each element in turn.
string[] names = { "Aria", "Ben", "Cleo" };</div>
</div>
foreach (string name in names)
{
Console.WriteLine(name);
}
Use it when you want each item but not its position.
Loops
Sometimes you need to control a loop mid-flight — leaving early, or skipping a single pass. Two keywords do it: break stops the loop entirely; continue skips to the next iteration.
Loop Control
for (int i = 0; i < 10; i++)</div>
<div class="column"><h3>continue — skip this pass</h3>
<pre><code>for (int i = 0; i < 6; i++)</div>
</div>
{
if (i == 5) break;
Console.WriteLine(i); // 0..4
}
{
if (i % 2 != 0) continue;
Console.WriteLine(i); // 0,2,4
}
break leaves the loop the moment you've found what you need; continue ignores items that don't qualify and moves on.
Loop Control
flowchart TD
A([Start]) --> B{"row < 3 ?"}
B -->|no| H([Done])
B -->|yes| C["col = 0"]
C --> D{"col < 3 ?"}
D -->|yes| E["print row,col"]
E --> F["col++"]
F --> D
D -->|no| G["row++"]
G --> BA loop inside another is a nested loop — the inner one runs fully for each pass of the outer. Essential for grids and tables. A break only exits the loop it's in.
for (int row = 0; row < 3; row++)</div>
</div>
{
for (int col = 0; col < 3; col++)
{
Console.Write($"({row},{col}) ");
}
Console.WriteLine();
}
Section 13
Memory
While your program runs, all its data lives in memory (RAM) — picture an enormous wall of numbered lockers, each holding a small piece of data. The CPU fetches and stores values by those numbers, called addresses.
Variables are really just friendly names for locations in that memory. Your code never juggles raw addresses — C# handles that for you.
Memory
Fast, orderly, automatic. Holds local variables and tracks which function is running. Grows and shrinks neatly as functions are called and return.
A larger, flexible pool for data whose size or lifetime isn't fixed — the objects that reference types point to. Tended by the garbage collector.
This is the overview; we'll revisit both in depth once you've met classes.
Memory
flowchart LR
subgraph Stack
v["hero — holds an address"]
end
subgraph Heap
o["the Player object"]
end
v --> oHere the value/reference split becomes concrete:
That's exactly why copying a value type duplicates the data, while copying a reference type shares it.
Memory
In some languages you must manually free every piece of memory you allocate — and forgetting causes leaks and crashes. C# is managed: the garbage collector automatically reclaims heap objects you're no longer using.
You focus on logic; the runtime handles cleanup. We'll see how the GC actually works in the later deep-dive section.
Section 14
Introduction to OOP
1A fundamental style for structuring programs and reasoning about them.
e.g.Procedural, object-oriented and functional are three of the most influential.
Programming Paradigms
A sequence of steps and functions acting on loose data. Exactly what we've written so far — direct, but it drifts apart as it grows.
Bundles data and the behaviour acting on it into objects. Models real things — a Player, an Enemy — naturally.
Computation as evaluating functions, favouring immutable data and no hidden state. Predictable and testable.
C# is multi-paradigm: object-oriented at its core, fully procedural (your top-level statements), and happy to borrow functional ideas — lambdas, LINQ, immutability.
Introduction to OOP
Object-oriented programming structures a program as a set of cooperating objects, each combining state (data) with behaviour (methods). It exists to tame the complexity that sinks large procedural programs:
Introduction to OOP
Bundle data with the code that uses it, and hide the internals.
Build new types on existing ones, reusing what they offer.
Treat different types through one interface, each behaving its own way.
Expose the essentials, hide needless detail.
Each pillar gets its own treatment in the sections ahead. C# implements them with class/struct, access modifiers, : for inheritance, virtual/override, and abstract/interface.
Section 15
Classes and Objects
1A class is the blueprint; an object is a specific thing built from it. One class, many objects.
e.g."House blueprint" is the class; the three houses built from it are three objects, each with its own address and paint colour.
Classes and Objects
class Player</div>
<div class="column"><h3>The object — a real instance</h3>
<pre><code>Player hero = new Player();</div>
</div>
{
public string Name;
public int Health;
public void TakeDamage(int amount)
{
Health -= amount;
}
}
hero.Name = "Aria";
hero.Health = 100;
hero.TakeDamage(30); // Health -> 70
Defining a class creates no data. new instantiates it — allocating one object on the heap and handing you a reference. Each new is independent.
Classes and Objects
Remember the compiler wrapping your code in a class with a Main method? Now it makes sense: there is no code outside a class in C#. Your program was always a class.
class Program
{
static void Main(string[] args)
{
// your top-level code lived here
}
}
The training wheels are off — everything is objects and classes.
Classes and Objects
An object's raw data lives in fields, but we usually expose it through properties — a gateway that looks like a field but can run code on read or write.
public int health; // anyone can set
// any value, no control
private int health;
public int Health
{
get => health;
set => health = value < 0 ? 0 : value;
}
Rule of thumb: keep fields private, expose data through public properties.
Fields and Properties
When you need no custom logic, an auto-property gives the property syntax with a hidden backing field generated for you — the everyday default. The set receives the new value through the implicit value keyword; omit or restrict it to control access.
class Player
{
public string Name { get; set; }
public int Health { get; private set; } // read-only from outside
}Classes and Objects
A constructor runs once, when you new an object, to set it up — guaranteeing it starts life valid. It's named after the class and has no return type. If you write none, C# supplies a hidden default constructor; the moment you write your own, that freebie disappears.
class Player
{
public string Name;
public int Health;
public Player(string name, int health)
{
Name = name;
Health = health;
}
}
Player hero = new Player("Aria", 100);Constructors
One constructor can call another with : this(...), avoiding duplicated setup. This is chaining.
public Player() : this("Unnamed", 100)
{
// delegates to the constructor below
}
public Player(string name, int health)
{
Name = name;
Health = health;
}Classes and Objects
Access modifiers decide who can see each member, turning encapsulation from an idea into a rule the compiler enforces.
Visible to everyone — the deliberate, public-facing surface of a type.
Visible only inside the same class. The default, and right for internal details.
Visible in the class and any class that inherits it. The bridge to descendants.
Visible anywhere in the same assembly, but not to other projects.
Keep fields private and expose a small public surface: objects stay valid, internals stay free to change, and the type is easy to use correctly.
Classes and Objects
Most members belong to an object. Static members belong to the class itself — one shared copy, usable with no object at all (Console.WriteLine, int.MaxValue).
class Enemy
{
public static int Count; // shared by all enemies
public Enemy() { Count++; }
public static void ResetCount() => Count = 0;
}
Ask: "is this about one object, or the whole type?" That answers whether it should be static. A class marked static can't be instantiated and holds only static members — a tidy home for stateless utilities.
Classes and Objects
A method is a function that lives inside a class — everything you learned about functions applies, plus it can act on its object's own data. Several methods can share a name through overloading, as long as their parameters differ; the compiler picks the right one.
class Player
{
public int Health;
public void Heal(int amount) => Health += amount; // acts on this object
}
void Log(string message) { /* ... */ }
void Log(int code) { /* ... */ }
void Log(string message, int code) { /* ... */ } // overloads
Section 16
Structs
Copied on assignment; each variable independent. Usually on the stack. No inheritance.
struct Point</div>
<div class="column"><h3>class — reference type</h3>
<p>Shared by reference; copies point to the same object. Lives on the heap. Supports inheritance.</p>
<pre><code>Point a = new Point(1, 1);</div>
</div>
{
public int X, Y;
public Point(int x, int y)
{ X = x; Y = y; }
}
Point b = a; // a full copy
b.X = 99; // a.X is still 1
A struct defines a custom type just like a class — fields, properties, methods — but that one difference (value vs reference) changes how it copies, stores and behaves.
Structs
null.Unity's Vector3 and Color are structs for exactly these reasons. When in doubt, use a class.
Section 17
Generics
Generics let you write a class or method with a type placeholder — written <T> by convention — filled in when it's used. You get reuse and full type safety: no casting, no losing track of types. You've used them every time you wrote List<...>.
List<int> numbers = new List<int>(); // T is int
List<string> names = new List<string>(); // T is stringGenerics
class Box<T></div>
<div class="column"><h3>A generic method</h3>
<pre><code>void Swap<T>(ref T a, ref T b)</div>
</div>
{
private T contents;
public void Put(T item) => contents = item;
public T Get() => contents;
}
Box<int> b = new Box<int>();
b.Put(42);
{
T temp = a;
a = b;
b = temp;
}
int x = 1, y = 2;
Swap(ref x, ref y); // T inferred as int
A method can be generic even when its class isn't — the type parameter goes right after the method name, and is often inferred from the arguments.
Generics
Sometimes T must meet a requirement — be a class, have a constructor, implement an interface. The where clause adds those constraints, unlocking what you can do with T.
T CreateAndReturn<T>() where T : new()
{
return new T(); // allowed: T must have a constructor
}
void Print<T>(T item) where T : IComparable { /* ... */ }
Section 18
Memory in Depth
Grows and shrinks in strict last-in, first-out order. Each call pushes a frame of locals and parameters; returning pops it off, instantly freeing them. Blazing fast — allocation is just moving a pointer — but limited and tied to calls.
A larger, flexible pool for data whose lifetime isn't tied to one call — every object made with new. Allocation is more involved, and memory stays alive as long as something references it.
Reference-type variables on the stack simply hold the address of their object on the heap.
Memory in Depth
So "value types are on the stack" is a useful simplification, not an absolute rule.
Memory in Depth
flowchart LR
A([new object]) --> B[Lives on the heap]
B --> C{Still reachable?}
C -->|Yes| B
C -->|No| D[Marked as garbage]
D --> E[("Memory reclaimed")]
Memory in Depth
You allocate and free by hand. Maximum control and speed — but mistakes cause leaks, dangling pointers and crashes.
The runtime manages the heap; the GC frees memory automatically when an object becomes unreachable. Far safer and faster to write, at the cost of some control and occasional GC pauses.
That trade — safety and productivity over manual control — is central to why C# suits games and apps. The catch: collections take time, so in hot code paths we avoid creating needless garbage.
Section 19
Collections
Fixed size, chosen at creation. Slightly faster and lighter. Best when the count is known and stable.
Resizes automatically, with rich helper methods. Best when items come and go over time.
Real programs rarely know their item count up front — inventories grow, enemies spawn and die. Collections (in System.Collections.Generic, all generic) handle that dynamism so you never manually resize and copy arrays.
Collections
A List<T> is an ordered, resizable sequence of same-typed items — like an array that can grow and shrink, fully type-safe. If you reach for one collection by default, it's this.
List<string> party = new List<string> { "Aria", "Ben" };
party.Add("Cleo"); // append
party.Insert(0, "Zed"); // at a position
party.Remove("Aria"); // by value
party.RemoveAt(0); // by index
Console.WriteLine(party.Count);List
foreach (string name in party)
Console.WriteLine(name);
for (int i = 0; i < party.Count; i++)
Console.WriteLine($"{i}: {party[i]}");
Add / AddRange — append.Remove / Clear — take out.Contains / IndexOf — search.Sort / Reverse — reorder.Use foreach when you want each item, a for loop when you need the index.
Collections
A Dictionary<TKey, TValue> stores key → value pairs, so you fetch a value instantly by its key instead of scanning positions. Keys are unique; values needn't be.
Dictionary<string, int> scores = new Dictionary<string, int>
{
["Aria"] = 1500,
["Ben"] = 1200
};
scores["Cleo"] = 900; // add or update
scores.Remove("Ben"); // remove by key
int aria = scores["Aria"]; // read (throws if missing)
if (scores.TryGetValue("Cleo", out int c))
Console.WriteLine(c); // safe lookupDictionary
Loop with foreach; each item is a KeyValuePair with .Key and .Value.
foreach (KeyValuePair<string, int> entry in scores)
{
Console.WriteLine($"{entry.Key}: {entry.Value}");
}Collections
First in, first out, like a checkout line. Enqueue at the back, Dequeue from the front.
var q = new Queue<string>();</div>
<div class="column"><h3>Stack — LIFO</h3>
<p>Last in, first out, like a stack of plates. <code>Push</code> on top, <code>Pop</code> from the top. Great for undo.</p>
<pre><code>var s = new Stack<string>();</div>
<div class="column"><h3>HashSet — unique</h3>
<p>Unordered, unique values, with very fast membership checks. Adding a duplicate does nothing.</p>
<pre><code>var seen = new HashSet<string>();</div>
</div>
q.Enqueue("load");
q.Dequeue(); // "load"
s.Push("move1");
s.Pop(); // "move1"
seen.Add("room1");
seen.Contains("room1"); // true
Pick the collection whose rules match your access pattern, and the code reads its own intent.
Section 20
Inheritance
1The base (parent) class is inherited from; the derived (child) class inherits and extends it.
e.g.Animal is the base; Dog is a derived class that gains Name and Eat() while adding Bark().
Inheritance
Inheritance lets one class build on another, reusing its data and behaviour and adding or changing what it needs. It models "is-a": a Dog is an Animal. Declare it with a colon — a class inherits exactly one base.
class Animal
{
public string Name;
public void Eat() => Console.WriteLine($"{Name} eats");
}
class Dog : Animal // Dog is an Animal, plus more
{
public void Bark() => Console.WriteLine("Woof");
}
A derived class inherits public, protected and internal members — but not private ones or constructors (though it can call them).
Inheritance
base refers to the parent — use it to call the base constructor, or to invoke behaviour you're extending rather than fully replacing.
class Boss : Enemy
{
public Boss(string name) : base(name) { } // call Enemy's constructor
public override void Attack()
{
base.Attack(); // the normal attack...
Console.WriteLine("...then a special move!");
}
}Inheritance & Polymorphism
class Animal</div>
<div class="column"><h3>override — "here's my version"</h3>
<pre><code>class Dog : Animal</div>
</div>
{
public virtual void Speak()
=> Console.WriteLine("...");
}
{
public override void Speak()
=> Console.WriteLine("Woof");
}
By default a derived class can't change an inherited method; virtual/override is the opt-in. Calls then run the derived version even through a base-typed variable — the basis of polymorphism. sealed does the opposite: it stops further inheritance or overriding.
Animal a = new Dog();
a.Speak(); // "Woof" — the Dog version runs
Inheritance & Polymorphism
Can't be instantiated; exists to be inherited. Holds shared fields and code, plus abstract methods (signature only) every subclass must implement.
abstract class Shape</div>
<div class="column"><h3>interface — a pure contract</h3>
<p>No fields, (traditionally) no implementation — just members a type promises to provide. A class can implement <strong>many</strong>.</p>
<pre><code>interface IDamageable</div>
</div>
{
public abstract double Area(); // no body
}
class Circle : Shape
{
public double Radius;
public override double Area()
=> 3.14159 * Radius * Radius;
}
{
void TakeDamage(int amount);
int Health { get; }
}
class Player : Character, IDamageable, IMovable
{ /* implements all members */ }
Rule of thumb: an interface for a capability ("what can it do?"), an abstract class for a shared identity ("what is it?"). Multiple interfaces give C# the flexibility of multiple inheritance without its pitfalls.
Inheritance & Polymorphism
Polymorphism — "many forms" — lets one piece of code work with many types through a shared base or interface, each responding its own way. With virtual/override, C# decides at run time which method to call from the object's real type, not the variable's declared type.
List<Animal> zoo = new List<Animal> { new Dog(), new Cat() };
foreach (Animal a in zoo)
{
a.Speak(); // Woof, then Meow — each correct, no type checks
}
It's what lets a render loop draw every IDrawable, a damage system hit anything IDamageable, a save system serialise every ISaveable — treating wildly different objects uniformly.
Section 21
String in Depth
We met string early as "text". Its real nature: a reference type whose data lives on the heap — yet immutable, so once created the characters never change. Operations that seem to modify a string actually return a new one.
string s = "hello";
s.ToUpper(); // returns "HELLO" but s is unchanged
s = s.ToUpper(); // reassign to keep the result
Immutability is why a string feels like a value: you can share one freely, certain no one will alter it underneath you.
String in Depth
Because every change makes a new string, building text with + in a loop allocates a throwaway string each pass — extra work for the GC.
string result = "";</div>
<div class="column"><h3>The fix — StringBuilder</h3>
<p>A mutable buffer you append to, producing the final string just once.</p>
<pre><code>var sb = new StringBuilder();</div>
</div>
for (int i = 0; i < 1000; i++)
{
result += i; // wasteful
}
for (int i = 0; i < 1000; i++)
sb.Append(i);
string result = sb.ToString();
Common methods — each returning a new string or a value: Length, ToUpper/ToLower, Trim, Substring, Replace, Split, Contains, IndexOf.
Section 22
Exception Handling
Even correct code meets bad input, missing files and broken connections. An exception is an object representing a runtime error: code throws one, and unless it's caught, it stops the program.
int[] a = { 1, 2, 3 };
Console.WriteLine(a[10]); // throws IndexOutOfRangeExceptionException Handling
Wrap risky code in try, handle failures in catch, and put cleanup that must always run in finally.
try
{
int result = 10 / divisor;
}
catch (DivideByZeroException ex)
{
Console.WriteLine("Can't divide by zero");
}
finally
{
Console.WriteLine("This always runs");
}Exception Handling
NullReferenceException — using something that's null.IndexOutOfRangeException — an array index past the end.DivideByZeroException — integer division by zero.FormatException — parsing text that isn't the expected format.ArgumentException — a method got an invalid argument.Exception Handling
Inherit from Exception for errors specific to your program — self-describing and catchable on their own.
class OutOfManaException : Exception</div>
<div class="column"><h3>Re-throwing</h3>
<p>Catch to log or react, then let it keep propagating with a bare <code>throw;</code> — preserving the original stack trace.</p>
<pre><code>try { LoadSave(); }</div>
</div>
{
public OutOfManaException(string m)
: base(m) { }
}
throw new OutOfManaException("Not enough mana");
catch (Exception ex)
{
Log(ex);
throw; // not "throw ex;" — that loses the trace
}
Exception Handling
flowchart TD
A[System.Exception] --> B[SystemException]
A --> C[Your custom exceptions]
B --> D[NullReferenceException]
B --> E[IndexOutOfRangeException]
B --> F[ArgumentException]
F --> G[ArgumentNullException]
Section 23
Week 1 Recap
Week 1 Recap
You now have the whole C# language under your belt: the types, the control flow, the objects and the memory model. That's the foundation everything in Unity is built on.
From here, the same C# you've written all week becomes game behaviour — scripts attached to objects in a scene, reacting frame by frame. Next module: the Unity engine itself.