Programming in Unity · Module 01

C# Fundamentals

From "what is a program", through variables, types, operators, conditionals, loops and functions. The building blocks of the C# language, and the foundation for everything that comes after.

C# Fundamentals

Navigation

Right and left move between topics. Down and up move through the detail of each. Space goes through everything in order.

Topic 1
detail
detail
Topic 2
detail
Topic 3
detail
detail
detail
Topic 4
detail

Topics ← → Detail ↑ ↓

C# Fundamentals

Shortcuts

Mouse and keyboard shortcuts for easier navigation.

/
Next / previous topic
/
Into / out of the detail
Space / N
Next slide
Shift + Space / P
Previous slide
Wheel
Next / previous slide
Shift + Wheel
Next / previous topic
F
Fullscreen
B / .
Black out the screen
Esc / Middle-click
Slide overview

C# Fundamentals

Roadmap

C# Fundamentals
Variables, types, operators, control flow, and functions
C# Intermediate
Classes and objects, structs, collections, generics, and exceptions

C# Fundamentals

Contents

  1. What Is Programming?
  2. Introduction to C#
  3. Environment Setup
  4. Hello, World!
  5. Variables
  6. Types
  7. Operators
  8. Conditionals
  9. Loops
  10. Functions
  11. Scope
  12. Wrapping Up

 

What Is Programming?

Section 01

Starting from the very basics

Gustave Doré engraving — divine creative power bringing order out of chaos
Him the Almighty PowerGustave Doré

What Is Programming?

Algorithm

ˈæl.ɡə.rɪ.ðəm

1A finite, well-defined sequence of instructions that takes one or more inputs and produces one or more outputs.

e.g.Binary search: given a sorted list, repeatedly halve the search space until the target is found or ruled out.

What Is Programming?

Algorithms Are Everywhere

A recipe as an everyday algorithm
  • An algorithm exists independently of any language or machine. It's any defined procedure that turns a clear input into a clear output, whether written in code or followed by hand.
  • Quicksort, Dijkstra's pathfinding, and a recipe all qualify. Each takes something in, and produces something out, by following a fixed set of steps.

What Is Programming?

Program

ˈproʊ.ɡræm

1A set of instructions, written in a programming language, that a computer can execute to perform a task.

e.g.A calculator app, a web browser, or a video game.

What Is Programming?

From Algorithm to Code

  • A program is a complete set of instructions, written in a language a computer can actually run. In this course, that language will be C#.
  • An algorithm is just the idea; a program makes it real, tied to a specific language and platform.
  • Programs put algorithms to work, but they do much more: reacting to input, moving data around, drawing to the screen. Much of programming is about wiring these pieces together.
int[] nums = { 7, 2, 9, 4 };
int max = nums[0];

foreach (int n in nums)
{
    if (n > max) max = n;
}

Console.WriteLine(max); // 9

What Is Programming?

Programming Language

1A formal language with strict, unambiguous rules for writing instructions that can be translated into something a computer executes.

e.g.C#: a general purpose language, used for videogames, desktop apps, enterprise software, and web services.

What Is Programming?

The Right Tool for the Job

No language is the best at everything. Each was built for certain problems, and tends to do that job better than the rest:

  • Python — highly readable, popular for scripting, data analysis, and machine learning.
  • JavaScript — runs in every web browser, but it can also power servers via Node.js.
  • C — manual memory management and good performance.
  • Java — object-oriented, used for enterprise software and Android apps.

They differ in syntax and purpose, but share the same core ideas you are learning now.

 

Introduction to C#

Section 02

Meet the language: History & Characteristics

Gustave Doré drawing of a gnarled monster
Gnarled MonsterGustave Doré

Introduction to C#

What Is C#?

C# (pronounced "see-sharp") is a modern, general-purpose programming language: powerful enough for serious software, yet approachable enough to learn first. It has grown into one of the most widely used languages in the world.

It belongs to the C family, so its punctuation, braces and semicolons, looks familiar if you've seen C, C++ or Java.

C# code runs on .NET, Microsoft's development platform, which provides the runtime that executes your code and a vast library of ready-made tools.

TIOBE Index

Programming popularity · 2026

Python22.6%
C11.0%
Java8.7%
C++8.7%
C#7.4%
▲ Language of the Year 2025

TIOBE Index

Introduction to C#

Who Made It?

Anders Hejlsberg, the designer of C#
Photo: DBegley · CC BY 2.0
  • C# was created by Microsoft and designed by Anders Hejlsberg, the same engineer behind Turbo Pascal and Delphi, and later TypeScript. He drew on the lessons of C, C++ and Java, keeping what worked and smoothing over what didn't.
  • Today both the language and the .NET platform it runs on are open source, developed by Microsoft together with a large community.
  • And it's used by an enormous population of developers, from solo game makers to enterprise teams, which is a big part of why help is never far away while you're learning.

Introduction to C#

When Did It Appear?

timeline
    2002 : C# 1.0 ships with .NET Framework 1.0
    2007 to 2012 : Generics, LINQ, and async/await arrive, making the language far more expressive
    2016 : .NET goes open source and cross-platform, running on Windows, macOS and Linux
    Today : A new version lands roughly every year, placing it among the world's most popular languages

Introduction to C#

Where Is It Used?

C# reaches far beyond games. The same language and skills carry across four big domains:

Games

The scripting language of both Unity and Godot.

Desktop apps

Windows software through WPF and WinForms.

Web services

Back-end APIs and websites with ASP.NET.

Mobile & cross-platform

Apps for many devices from one codebase with .NET MAUI.

Introduction to C#

Why Choose C#?

Put it together and the appeal is clear: C# is safe, productive, and fast enough for demanding work like games.

  • Safe by design — statically and strongly typed, so a whole class of mistakes is caught at compile time, before the program ever runs.
  • No manual memory management — a garbage collector frees unused memory for you, removing crashes that haunt lower-level languages like C or C++.
  • Productive — a huge standard library and fast iteration: try an idea, see it run, tweak, and repeat.
  • Fast enough for real-time — performant enough for frame-by-frame gameplay, the sweet spot games need.
  • Well-supported — a vast ecosystem of libraries, tools and community knowledge.

Which is exactly why Unity, the engine we'll use, chose C# as its scripting language.

Introduction to C#

Computers Don't Understand Code

Deep down, a computer's processor understands only one thing: machine code, composed by long strings of 0s and 1s. It cannot read C#. And if you saw machine code yourself, you couldn't read it either.

There is a gap.

You write in C#, but the processor speaks only machine code. Something has to translate between the two, like two people who share no language needing an interpreter.

Introduction to C#

From Source to CPU

flowchart TD
  A([C# source]) -->|compile| B["CIL — intermediate code"]
  B -->|JIT compile| C["native machine code"]
  C --> D([CPU runs it])

Your code changes form twice before the processor can run it:

  • C# source — the code you write, saved in a .cs file.
  • CIL (Common Intermediate Language) — a compiler translates your C# into this intermediate, "halfway" code, ahead of time. It's no longer C#, but it isn't machine code yet either.
  • Machine code — when you actually run the program, the runtime (using its JIT compiler) turns that CIL into the native instructions your CPU executes.

So there are two translations: C# → CIL ahead of time, and CIL → machine code while the program runs.

Introduction to C#

Why Translate Twice?

Stopping at a "halfway" code instead of going straight to machine code looks like extra work, but it buys two big things:

  • It runs anywhere. The halfway code (CIL) is identical on Windows, Mac, and Linux. Each computer only has to handle the final step its own way, so the very same program runs everywhere without rewriting.
  • It runs fast. That final step happens on your actual machine, right when the code is needed, so the result is tuned to your exact computer.

You can take a look at the same one-line program, in both C# and CIL:

// C#
Console.WriteLine("Hello, World!");
// CIL
ldstr    "Hello, World!"
call     void [mscorlib]System.Console::WriteLine(string)
ret

Introduction to C#

Runtime

ˈrʌn.taɪm

1A program whose job is to run other programs.

e.g.The .NET runtime (Common Language Runtime or CLR) is the one that runs your compiled C#. Java has its own, the JVM, and Mono is another runtime that also runs C#, used by Unity.

Introduction to C#

Terminology

Compiler

The translator: it turns the C# you wrote into CIL (the halfway code) ahead of time.

CIL — Common Intermediate Language

The halfway code itself: not C#, not machine code, but something in between.

CLR — Common Language Runtime

The program that runs your compiled code and manages memory, so you don't have to.

JIT Compiler — Just In Time Compiler

The part of the runtime that does the final translation "just in time", right when each piece of code is first needed.

Introduction to C#

Mono, IL2CPP, and CoreCLR

Unity has to turn the C# you write into something each device can actually run. Over the years it has done this in a few different ways, and you'll hear all three names.

Mono is another version of that .NET runtime, its own independent take on the same idea. Not the CLR itself, but a sibling that does the equivalent job. It was Unity's runtime for years, and a big reason C# could power games across so many platforms. With Mono, the halfway code (CIL) ships inside your game, and the runtime translates it into machine code on the device while the game runs.

IL2CPP ("Intermediate Language to C++") is the newer approach. Instead of translating on the device as it runs, it takes your compiled code and converts it into C++ ahead of time, which is then built into native machine code for each platform. Some platforms, like iOS, don't allow on-the-device translation at all, so IL2CPP became the way to reach them. It also tends to run faster, which is why many modern games use it.

CoreCLR is the modern, cross-platform runtime from today's mainstream .NET (.NET Core and .NET 5 and up). It's the same lineage as the original CLR, but rebuilt to run on Windows, Mac, and Linux, and it's where Microsoft's active development is currently focused. Unity has been working to adopt CoreCLR to replace the aging Mono backend, bringing modern .NET and a much better JIT and garbage collector to the engine, so it's the direction things are heading.

 

Setting up the environment

Section 03

The SDK, an editor, and your first running project

Gustave Doré engraving of a magnificent castle — the structure you set up to build in
Magnificent CastleGustave Doré

Setting Up the Environment

What You Need

An IDE showing C# source code being edited

To write C#, you need two things, although in practice they often come together:

  • The .NET SDK — the Software Development Kit. It holds the compiler that turns your C# into CIL, the runtime that runs it, and the standard library. Nothing builds or runs without it.
  • A text editor or an IDE — where you write. A good one understands C#, flags mistakes as you type, and runs your program at the press of a button.

The good news: most editors bring the SDK along with them, so installing one often gets you both. The next slides cover the three you'll meet most.

Setting Up the Environment

Your Three Options

Visual Studio

Microsoft's full IDE. Heaviest, but also the most complete, with the SDK bundled in. The free Community edition is the most hand-holding start.

Windows · free (Community)

VS Code

A lightweight, cross-platform editor. Add the C# Dev Kit extension for full C# support; the .NET SDK installs separately. Leaner, a little more setup.

Win / Mac / Linux · free

JetBrains Rider

JetBrains' polished C# IDE, with a bundled SDK and first-class, officially-supported Unity tooling. Free for non-commercial use.

Win / Mac / Linux · free (non-commercial)

All three compile and run your C# the same way, and all three are free while you're learning. On Windows, Visual Studio is the editor Unity installs by default. VS Code is the lighter, cross-platform option. Rider is JetBrains' full IDE, free for non-commercial use.

Setting Up the Environment

Your First Project

> dotnet new console
> dotnet run

Hello, World!

Once your editor and SDK are installed, these two commands are all it takes to create and run a C# program. Run them in an empty folder from a terminal:

  • dotnet new console — scaffolds a new project: a .csproj file describing it, and a Program.cs already holding the "Hello, World!" line.
  • dotnet run — hands your code to the SDK's compiler, then runs the result, in one step.

See the greeting printed back? Your environment is ready, and that single run set the whole pipeline from the last section in motion: C# → CIL → machine code.

 

Hello, World!

Section 04

Your first program

Gustave Doré engraving of jugglers and acrobats — a first joyful performance for the world
Jugglers and Acrobats at the CastleGustave Doré

Hello, World!

Your First Program

Console.WriteLine("Hello, World!");
Hello, World!

You've already run this. Now, let's look at what you did.

When dotnet new console created your project, it left a single line of code for you.

That line is the traditional "Hello, World!". It's the first program you learn how to write when learning a new language.

Hello, World!

Reading the Line

Console.WriteLine("Hello, World!");
Hello, World!

Let's take a look at it. Don't worry about the terms below yet: class, method, and operator each get their own section later.

This whole line is one single statement, a single instruction the program runs. Its pieces are:

  • Console — the class for the terminal, where text is written and read.
  • . — an operator that reaches into Console.
  • WriteLine — a method that writes a line of text.
  • () — an operator which calls the method.
  • "Hello, World!" — a string indicating the text to show.
  • ; — the statement terminator, ending the statement.

Hello, World!

Wait, That's the Whole Program?

Console.WriteLine("Hello, World!");
Hello, World!

If you have ever used another object-oriented language, you would expect for that line to live inside a class and a Main method, like this:

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("Hello, World!");
    }
}

When you write it on its own, it's called a top-level statement. C# lets you skip the wrapper and just write code directly.

Hello, World!

Comments

A comment is text the compiler ignores completely. It's there for programmers, to explain why code does something or to leave a note for later. You'll spot them throughout the code examples from here on.

// A single-line comment runs to the end of the line
Console.WriteLine("Hi"); // It can also sit after code

/* A block comment
   can span several lines */

Use comments to explain intent, not to restate the obvious. Clear code with good names usually needs few of them.

Hello, World!

Statements and Blocks

Two structural rules shape every C# program, and you just met both in "Hello, World!":

  • A statement is a single instruction. It ends with a semicolon ;. That's why every line so far finishes with one.
  • A block is a group of statements wrapped in curly braces { }. Methods, loops, and if clauses all attach a block of code to run. A block is also a statement itself.
{
    Console.WriteLine("First");  // A statement
    Console.WriteLine("Second"); // Another statement
}                                // The block groups them together, making a new statement

 

Variables

Section 05

Named, reusable storage for the data your program works with

Gustave Doré engraving of many large fish — a haul of distinct stored values
Many Large FishGustave Doré

Variables

Variable

ˈvɛə.ri.ə.bəl

1A named storage location, with a fixed type, that holds a value you can read and change while the program is running.

e.g.a player's score (number), their name (text), whether the game is over (true/false).

Variables

Declaration & Initialization

string firstName;           // Declaration
firstName = "Al";           // Initialization

string secondName = "John"; // Declaration & initialization

var thirdName = "Jack";     // var: type inferred (string)

Declaring a variable states its type and name. A variable's type (string, int, bool) fixes what kind of value it can hold and what you can do with it; types will be explained in the next section. Initialising gives the variable its first value, and most of the time you do both in one line.

var lets the compiler infer the type from the value, so you don't write it twice. The variable is still strongly typed, var doesn't make the type flexible, it's used mostly to make the code easier to read, or for some special types.

Variables

Using a Variable

Once a variable has a value, its name stands in for that value wherever you use it; you can read it, compute with it, and reassign it. The most recent value is always the one that counts.

int score = 10;
Console.WriteLine(score); // 10

score = score + 5;        // Reassign from its own value
Console.WriteLine(score); // 15

string name = "Aria";
Console.WriteLine($"{name} has {score} points");
10
15
Aria has 15 points

Variables

Locking a Value — const & readonly

const

A true constant, fixed at compile time. Must be set right where it is declared.

const int MaxPlayers = 4;

readonly

Set once at runtime rather than compile time, then frozen. Use it when the value isn't known until the program runs. There's more to readonly than fits here, so we'll explore it properly later in the course.

readonly DateTime startedAt = DateTime.Now;

Variables

Default Values

A field you never assign isn't left as random memory. C# starts it at a default value decided by its type, so every field begins in a known, safe state. Each type's default:

Numbers — int, double, float

0

bool

false

char

'\0'

Reference types (classes, arrays, strings)

null

Locals are the exception: they get no default. The compiler makes you assign a value before first use.

Variables

Naming Variables

A variable's name is for humans first; choose one that says what it holds. Descriptive names make code read like an explanation of itself, vague ones force the reader to guess.

Clear

playerHealth
enemyCount
isGameOver

Vague

x
temp
data

Beyond clarity, C# follows naming conventions: local variables use camelCase (lowercase first word, capitalize each word after), while constants and other members use PascalCase (capitalize each word). A name cannot start with a digit or be a C# keyword.

Variables

Reading Input

Console.Write("What is your name? ");
string name = Console.ReadLine();

Console.WriteLine($"Hello, {name}!");

Programs get more interesting when they react to the user. Console.ReadLine() waits for the user to type a line and press Enter, then hands back what they typed as a string.

Store that string in a variable and you can use it like any other value. Console.Write (no "Line") prints the prompt without moving to a new line, so the answer sits right after it.

Input always arrives as text. Turning it into a number takes a small conversion step. It will be explained later alongside types and operators.

 

Types

Section 06

How C# classifies every piece of data

Gustave Doré engraving of Brother Bruin the bear
Brother BruinGustave Doré

Types

Type

taɪp

1A classification that specifies what kind of data a value is, what it can hold, and which operations are legal on it.

e.g.5 is an integer, "hello" is a string, true is a boolean.

Types

The Benefits of Static Typing

C# is statically typed: it verifies every type at compile time, before the program runs, instead of discovering mismatches at runtime (mid-execution).

Moving this check to the compilation phase gives you four major advantages:

  • Find errors immediately: If you try to do an operation on incompatible data, like strings and integers, the code will not compile, so a whole class of bugs never ships.
  • Faster execution: the data types are already locked in, the runtime doesn't have to waste time looking up what the data is while the program is running.
  • Predictable memory layout: each type has a known size and structure. The runtime can lay out and allocate memory for your data precisely, rather than determining it at runtime.
  • Better coding tools: your code editor knows your data types ahead of time, so it can display highly accurate autocomplete.

Types

Two Families of Type

The two main types in C# are value type (the variable holds the data itself) and reference type (the variable holds an address pointing to data that lives elsewhere). That single split shapes how data is copied, compared and stored. Language specification.

flowchart TD
    T["Types"] --> V["Value type"]
    T --> R["Reference type"]
    T --> TM["[...]"]

    V --> ST["struct"]
    V --> E["enum"]

    R --> C["class"]
    R --> I["interface"]
    R --> A["array"]
    R --> D["delegate"]
    R --> RM["[...]"]

Types

Copy vs Share

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.

Value type (copied)

int a = 10;
int b = a; // b gets a copy
b = 99;    // a is untouched
// a is still 10

Reference type (shared)

int[] x = { 1, 2, 3 };
int[] y = x; // Same array
y[0] = 99;
// x[0] is now 99 too

Types

Why it is important

This one idea defines everyday behaviour:

  • Whether changing one variable affects another.
  • What a function can and can't change when you pass data into it.
  • How equality and copying behave.

It comes down to one distinction: value types hold the data, reference types point to it. Hold onto that, because many behaviours in C# trace back to which side of the line a type sits on.

Types

The null Reference

Because a reference type holds an address rather than the data itself, it can also point to nothing at all. That "points to nothing" value is null, and it is the default for every reference type until you assign one.

string name = null;             // Refers to nothing yet
name = "Aria";                  // Now refers to a real string
Console.WriteLine(name.Length); // 4

string other = null;
Console.WriteLine(other.Length); // NullReferenceException

Using a value through a null reference fails at run time with a NullReferenceException, one of the most common errors in C#. Testing for null first is how you guard against it.

Types

Simple Types

C# bakes in a handful of simple types for the most basic kinds of data: whole numbers, decimals, single characters and true/false values. Each one comes with a convenient keyword name. Under the hood, every keyword is just a friendly alias for a type in the .NET library: int is really System.Int32.

int health = 100;
double speed = 5.5;
char grade = 'A';
bool isAlive = true;

Types

Not Primitives

People often call these "primitive types", borrowing the term from languages like Java. In C# that word is not quite accurate.

C# has no formal category called "primitive". Its simple types are really structs, full value types with methods of their own. You can write 5.ToString() or int.MaxValue; a true primitive could not do that. So "simple type" is the correct term.

In Java, int is a true primitive with no methods. To call methods you need its object wrapper, Integer:

int N = 5;
Integer boxedN = 5; // Integer is the object wrapper

N.toString();       // Won't compile, int is a primitive
boxedN.toString();  // "5"

Types

The Four Main Numeric Types

Picking a number type is a trade between range (how big) and precision (how exact).

int

32-bit. No suffix. Whole numbers (scores, indices).

float

32-bit. Suffix f. Inexact. Operations where rounding errors are fine.

double

64-bit. No suffix. Default for decimal literals. Still inexact, but far more headroom. Precise math.

decimal

128-bit. Suffix m. Base-10, exact for decimal values. Money (no rounding errors).

Types

Suffixes Decide the Type

A bare decimal literal is a double. A bare whole number is an int. The suffix tells the compiler you meant something else (float or decimal).

int lives = 3;          // Integer by default
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

float gravity = -9.81;  // Error: cannot assign double to float
decimal price = 19.99;  // Error: cannot 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

bool

buːl

1A value type that can hold only one of two values: true or false.

e.g.A bool named isGameOver is false while the player is alive and becomes true when they lose.

Types

true and false

A bool has exactly two possible values: true and false. They usually come from a comparison, and they drive conditionals and loops.

bool isAlive = true;
bool hasKey = false;
bool canEnter = health > 0; // A comparison produces a bool
if (isAlive)
{
    Console.WriteLine("Keep playing");
}

Types

char

tʃɑːr

1A value type that holds exactly one character, written between single quotes.

e.g.'A', '7', '?' and ' ' (a space).

Types

Character Literals and Encoding

A char holds a single character. It is the building block strings are made of. Its literal (the notation) uses single quotes ('A'), while a string uses double quotes ("A"). Do not mix them up.

Under the hood, a char is a 16-bit number representing a Unicode (UTF-16) code unit, so each character maps to a number you can even do arithmetic on.

char letter = 'A';
char digit = '7';
int code = letter; // 65 — the Unicode value of 'A'

Types

enum

ˈiː.nʌm

1A value type that defines a named set of related constants, making code more readable and less error-prone.

e.g.Direction { North, East, South, West }.

Types

Declaration and Usage

Declare the set of names, then use them by name. Code reads like English, and printing an enum gives you the name.

enum Direction { North, East, South, West }

Direction facing = Direction.North;

Console.WriteLine(facing);
North

Types

Underlying Types

Each enum member is actually a number, by default an int, starting at 0 and counting up. You can set the values explicitly, or change the underlying type.

enum Difficulty : byte
{
    Easy = 1,
    Normal = 2,
    Hard = 4
}

Here, for example, the underlying type is changed to byte, and each name has a custom value.

Types

The Flags Attribute

[Flags] marks an enum as a bit field, so combined values print as a list of member names, instead of a bare number. Assign each member a distinct power of two so the bits combine cleanly.

[Flags]
enum Toppings
{
    None     = 0,
    Cheese   = 1,
    Bacon    = 2,
    Mushroom = 4
}

Toppings order = Toppings.Cheese | Toppings.Bacon;
Console.WriteLine(order); // "Cheese, Bacon" (without [Flags]: "3")

The symbol "|" is the bitwise OR operator. It, and the rest of C#'s operators, are explained in the next section.

Types

Nullable Value Types

A value type like int always holds a value, it can never be null. When you need to represent "no value yet" (a missing score, an unanswered field), you can add a ? to the type to make it nullable.

int count = 0;     // Always a number
int? score = null; // A number, or null

if (score.HasValue)
    Console.WriteLine(score.Value);

int shown = score ?? 0; // Falls back to 0 when null

An int? holds any int or null. Use HasValue to test it, Value to read it, and ?? to supply a fallback. Only value types need the ?, reference types can already be null.

Types

string

A string is a sequence of characters: names, dialogue, file paths, anything textual. It's written in double quotes like this: "Pisa".

Unlike the numeric and char types, string is a reference type: the variable holds a reference to the text, which lives elsewhere in memory. Yet, it behaves like a simple value, because strings are immutable: once created they never change, so you can share one freely without fear that something will change it.

Types

Building Strings

Join strings with + (concatenation), or use string interpolation with $ to drop values straight into the text (far cleaner than a chain of +).

string name = "Aria";
string greeting = "Hello, " + name;                 // Concatenation
string line = $"{name} scored {1500 + 250} points"; // Interpolation

Inside an interpolated string, any expression put in { } is evaluated and turned into text.

Types

Escapes and Verbatim Strings

Inside double quotes, a backslash starts an escape sequence for characters you can't type directly: \n (new line), \t (tab), \" (a quote), \\ (a literal backslash).

string path = "C:\\Users\\Aria";    // Each \\ is one backslash
string verbatim = @"C:\Users\Aria"; // @ : backslashes are literal
string quote = "She said \"hi\"";

A verbatim string (@"...") takes the text exactly as written, escapes and all. Ideal for Windows paths and multi-line text, where escaping every backslash gets tedious.

Types

Immutability

Strings are immutable: once created, their characters never change. Methods that look like they modify a string actually return a new one and leave the original untouched.

string s = "hello";
s.ToUpper();     // Returns "HELLO", but s is still "hello"
s = s.ToUpper(); // Reassign to actually keep the result

This is exactly what lets you pass a string around safely: nobody you share it with can change it on you.

Types

Strings in Memory

Because a string can be any length, its characters live elsewhere in memory and the variable holds a reference to them. Immutability is what makes that safe to share, but it has a cost.

Since every "change" produces a new string, building text by repeatedly concatenating with + in a loop creates a pile of throwaway strings the runtime must later clean up.

Types

Common String Methods

Strings carry a rich toolkit. Each returns a new string or a value, never modifying the original:

  • Length — number of characters; index with s[0] to read one char.
  • ToUpper / ToLower — change case.
  • Trim — remove surrounding whitespace.
  • Substring — extract part of it.
  • Replace, Split, Contains, IndexOf — search and transform.

Equality compares text, not identity: "hi" == "hi" is true, because string customises (overloads) == to compare characters.

Types

Arrays

An array holds a fixed-size, ordered collection of values that all share the same type, like a row of numbered boxes. Once created, its length never changes.

Arrays are reference types: the variable holds a reference to the block of elements, not the elements themselves. Copying an array variable copies the reference, so both names point to the same data.

0
"Aria"
1
"Ben"
2
"Cleo"
string[] names = { "Aria", "Ben", "Cleo" };

Types

Declaration and Initialization

An array type is the element type followed by []: int[], string[]. To make one, either ask for a size (slots filled with the type's default), or list the values and let C# count them.

int[] a = new int[5];            // Five slots, all 0
int[] b = new int[] { 1, 2, 3 }; // Sized from the values
int[] c = { 10, 20, 30 };        // Shorthand

new int[5] reserves five slots and fills them with 0 (the default for int). The { ... } form skips new entirely, the length comes from how many values you wrote.

Types

The new Keyword

The new keyword in new int[5] creates the array, reserving a block of memory for its elements and handing back a reference to it (arrays are reference types).

int[] a = new int[5]; // new builds the array, a refers to it
int[] b = { 1, 2, 3 }; // Shorthand, here new is implied

The variable and the array are two separate things: new produces the array, and the variable holds a reference to it. You will meet new constantly from here on, as it is the keyword used to create objects, which are the basis of Object Oriented Programming (OOP).

Types

Accessing Elements

Reach an element by its index, written in [] after the variable and counting from 0. The first element is [0] and the last is [Length - 1]. Going out of range throws an exception at run time.

string[] names = { "Aria", "Ben", "Cleo" };
Console.WriteLine(names[0]);     // Aria (first)
names[2] = "Cara";               // Overwrite Cleo
Console.WriteLine(names.Length); // 3
Console.WriteLine(names[3]);     // IndexOutOfRangeException

Types

Multidimensional arrays

A multidimensional array (also called a rectangular array) is a single array with two or more dimensions. It uses one set of brackets with commas [,] and is accessed with multiple indices. Every dimension has a fixed length, which is what makes it "rectangular."

int[,] grid = new int[2, 3]; // 2 rows, 3 columns
grid[0, 0] = 5;
grid[1, 2] = 9;

// Initialize with values directly
int[,] board = { { 1, 2 }, { 3, 4 } };

// Read dimensions
int rows = grid.GetLength(0); // 2
int cols = grid.GetLength(1); // 3

All elements occupy one contiguous block of memory, making rectangular arrays compact and efficient for evenly shaped data.

Types

Jagged Arrays

A jagged array is an array whose elements are themselves arrays, an "array of arrays." Each element array can be a different length, hence the jagged edge. It uses multiple sets of brackets [][] and is accessed one index at a time.

int[][] jagged = new int[3][]; // 3 rows, lengths set later
jagged[0] = new int[] { 1, 2 };
jagged[1] = new int[] { 3, 4, 5 };
jagged[2] = new int[] { 6 };

int x = jagged[1][2]; // 5

// Initialize with values directly
int[][] data = {
    new int[] { 1, 2 },
    new int[] { 3, 4, 5 }
};

Because each row is a separate array, rows can vary in length and be built or replaced independently. Jagged arrays are often faster for row-by-row access, while rectangular arrays use memory more compactly.

 

Operators

Section 07

The symbols that combine, compare and transform your values

Gustave Doré engraving of a fierce charge, forces combining and acting on each other
They Fiercely Charged ForwardGustave Doré

Operators

What an Operator Is

An operator is a symbol that performs an action on one or more values (its operands) and produces a result. For example, 5 + 3 applies the + operator to the operands 5 and 3 to produce 8. Combining operators and operands forms an expression, a fundamental building block of computation.

  • Unary, one operand: -x (negation), !ready (logical NOT).
  • Binary, two operands: a + b (addition), x < y (comparison).
  • Ternary, three operands: cond ? a : b (the conditional operator, of which C# has exactly one).

Just like in math, precedence decides which operator runs first (2 + 2 * 2 is 6, not 8), associativity breaks ties between operators of equal precedence, and ( ) overrides both. The families below appear in roughly the order you will use them.

Operators

Every Operator at a Glance

Arithmetic

+-*/%++--

Comparison

<><=>=

Equality

==!=

Boolean logic

!&&||

Bitwise & shift

~&|^<<>>>>>

Assignment

=+=-=*=/=%=&=|=^=<<=>>=>>>=

Null handling

????=?.?[]x!

Conditional

? :

Member & access

.( )[ ]^..::

Type & convert

(T)isastypeofsizeofnameof

Expression

=>newawaitdefault

Unsafe & special

&*->checkedstackallocdelegate

Operators

Arithmetic Operators

+-*/%

These do basic arithmetic, with one quirk worth knowing. When both operands are integers, division truncates: 7 / 2 is 3, not 3.5. The % (modulo) operator gives the remainder, useful for even/odd checks and "every Nth" logic.

int sum  = 5 + 3; // 8  addition
int diff = 5 - 3; // 2  subtraction
int prod = 5 * 3; // 15 multiplication
int quot = 7 / 2; // 3  division (truncates)
int rem  = 7 % 2; // 1  remainder (modulo)
int neg  = -sum;  // -8 unary minus
int pos  = +sum;  // 8  unary plus

Operators

Increment and Decrement

++--

++ adds one to a variable and -- subtracts one: the shorthand for the very common "change by one", and the usual way to step a loop counter.

The bump is applied the moment that piece of the expression is evaluated, not after the whole line.

int score = 0;
score++; // +1

int x = 5;
int a = x++; // a = 5, then x becomes 6 (postfix)
int b = ++x; // x becomes 7, then b = 7 (prefix)

x = 5;
int r = x++ + x; // 11: x++ is 5, bumps x to 6, 2nd x is 6

Operators

Assignment and Compound Assignment

=+=-=*=/=%=&=|=^=<<=>>=>>>=??=

= stores a value in a variable. The compound forms combine an operation with assignment, updating a variable from its own current value. There is a compound form for nearly every binary operator.

int x = 10;
x += 5;           // x = x + 5 -> 15 (also -= *= /= %=)
x &= 6;           // x = x & 6       (also |= ^=)
x <<= 1;          // x = x << 1      (also >>= >>>=)
name ??= "Guest"; // Assign only if name is null (??=)

Operators

Comparison Operators

<><=>=

These order two values and always produce a bool. They are the source of nearly every condition you write.

bool c = 5 >  3; // Greater than          -> true
bool d = 5 <  3; // Less than             -> false
bool e = 5 >= 5; // Greater than or equal -> true
bool f = 3 <= 2; // Less than or equal    -> false

Operators

Equality Operators

==!=

C# treats equality as its own family, separate from ordering. == and != also produce a bool.

For simple values this compares the values themselves. For objects it compares identity by default, unless the type customises it, as string does to compare text.

bool a = 5 == 5; // Equal to     -> true
bool b = 5 != 3; // Not equal to -> true

Operators

Boolean Logical Operators

!&&||

These combine bool values. && (and) is true only if both sides are, || (or) if either is, and ! (not) flips one.

&& and || short-circuit: they stop the moment the answer is certain. The & and | forms (see Bitwise) do the same logic without short-circuiting.

bool canEnter = isLoggedIn && hasPermission; // Both true
bool useCache = fromCache || canRetry;       // Either true
bool isHidden = !isVisible;                  // Flip it

Operators

Conditional (Ternary) Operator

? :

? : is C#'s only ternary operator, a compact if/else that produces a value. Read it as condition, then the value if true, then the value if false.

Both branches must produce a compatible type, because the whole expression yields a single value.

string label = score >= 50 ? "Pass" : "Fail";
// Condition ? if true : if false
int limit = isPremium ? 500 : 100;

Operators

Bitwise and Shift Operators

~&|^<<>>>>>

On whole numbers these work bit-by-bit, used for flags, masks, and low-level work. You will reach for them rarely as a beginner. The &, | and ^ forms double as non-short-circuiting boolean operators.

int and = 0b1100 &  0b1010;  // 0b1000  AND
int or  = 0b1100 |  0b1010;  // 0b1110  OR
int xor = 0b1100 ^  0b1010;  // 0b0110  XOR
int not = ~0b1100;           // flip all bits
int lsh = 1 << 4;            // 16   x * 2^n
int rsh = 32 >> 2;           // 8    x / 2^n
uint ur = 0xF0000000u >>> 4; // unsigned right shift

Operators

Member Access: the Dot

.?.

The . operator reaches into a value to use its members (fields, properties, and methods), and to qualify names inside namespaces.

Its null-safe sibling ?. only accesses the member if the left side is not null (see Null-Handling).

account.Balance;                // Read a property or field
account.Withdraw(10);           // Call a method
order.Customer.Name;            // Chain: member of a member
System.Console.WriteLine("hi"); // Qualify in a namespace

Operators

Invocation and Grouping: Parentheses

( )

As the invocation operator, parentheses call a method or delegate, passing arguments. As grouping, they override precedence, forcing part of an expression to run first.

Casts (int)x and tuples (1, 2) reuse parentheses too, but those are separate features.

Greet("Aria");       // Invocation: call with one arg
int n = Square(5);   // Call, then use the result

int a = 2 + 3 * 4;   // 14: * runs before +
int b = (2 + 3) * 4; // 20: grouping forces + first

Operators

Element Access: Square Brackets

[ ]?[]^..

The [ ] operator reads or writes an element by position or key, across arrays, lists, dictionaries, and any type with an indexer.

The null-safe ?[] only indexes if the value is not null. The index-from-end ^ and range .. operators are used inside [ ].

int first = scores[0];      // Zero-based
scores[0] = 99;             // Assignment
int qty = prices["apple"];  // Dictionary: by key
char c  = name[^1];         // ^  last element (from end)
int[] slice = scores[1..3]; // .. a range (sub-array)

Operators

Null-Handling Operators

????=?.?[]x!

These make working with values that might be null both safe and concise.

?., ?[] and ?? short-circuit: if the left side is null, the rest is not evaluated.

int len = name?.Length ?? 0; // ?. then ?? : 0 if null
int first = list?[0] ?? -1;  // ?[] index if not null
name ??= "Guest";            // ??= assign if null
string s = maybeNull!;       // ! trust: not null

Operators

Type-Testing and Cast Operators

isas(T)typeofsizeofnameof

These inspect or change a value's type. Casting itself gets a fuller treatment just ahead.

is also powers pattern matching: if (o is string s) tests and names the value in one step.

object o = "hi";
bool   isStr = o is string;   // is      type test
string str   = o as string;   // as      or null
int    n     = (int)9.8;      // (T)x    cast -> 9
Type   t     = typeof(int);   // typeof  the Type
string nm    = nameof(count); // nameof  "count"
int    sz    = sizeof(int);   // sizeof  4 bytes

Type Conversion

Implicit vs Explicit Conversion

Implicit: widening

When there is no risk of losing data, C# converts automatically. A smaller type widens into a larger one, the way an int fits perfectly inside a double.

int whole = 7;
double precise = whole; // 7 -> 7.0 auto

Explicit: narrowing

When a conversion might lose data, C# refuses to do it silently. You must cast with (type) in front of the value, accepting the risk.

double precise = 9.8;
int whole = (int)precise; // 9, dropped

Casting between number types truncates rather than rounding, so (int)9.8 is 9, not 10.

Type Conversion

Conversion Methods

int n = int.Parse("42");             // string -> int (throws)
bool ok = int.TryParse("42", out n); // Safe: false on bad
string s = 42.ToString();            // int -> string
double d = Convert.ToDouble("3.14"); // Many target types

Casts move between related numeric types, but turning text into numbers (and back) calls for library methods instead.

  • Parse throws a FormatException on invalid text.
  • TryParse reports success as a bool and writes the result through out; prefer it for user input.
  • ToString renders almost any value as text.

Operators

Lambda and Other Operators

=>newawaitdefault::

A grab-bag of operators that each build a specific kind of expression. new creates an object, and :: qualifies a name through a namespace alias.

switch and with read like these but are expressions, not operators, so they are not listed here.

var list = new List<int>();           // new     create an object
numbers.Where(x => x > 0);            // =>      lambda
await LoadAsync();                    // await   async result
int d = default;                      // default value (0)
var con = global::System.Console.Out; // ::      alias

Operators

Rarely-Used and Unsafe Operators

&*->checkedstackallocdelegate

You will likely never write these in everyday code, but they complete the set.

  • checked / unchecked: turn integer overflow checking on or off for an expression.
  • delegate: the older anonymous-method form, mostly replaced by lambdas (=>).
  • stackalloc: allocate a small array on the stack, as in stackalloc int[8].
  • true / false: a type can define these so its values work directly in if and &&.
  • & (address-of), * (indirection), -> (pointer member): pointers, only inside an unsafe block.

Operators

Operators vs Punctuators

( )[ ].{ }

Some symbols look like operators but are really punctuators: they shape code rather than compute a value.

  • Operators: ( ) invocation and grouping, [ ] element access, and . member access. These act on operands and yield a value.
  • Punctuators: { } blocks and initialisers, ; end of statement, , separator, : labels and the second half of ? :, and the $"…" / @"…" string prefixes. These are structure, not operators.

So ( ), [ ] and . are operators, while { } is not.

Operators

Strong vs Weak Typing

Languages differ in how strictly they enforce types, and that shapes how many mistakes the compiler can catch for you. In a strongly typed language, types are enforced: you cannot accidentally treat text as a number. In a weakly typed language, types bend silently, which is flexible but error-prone.

There is a separate axis. Static typing checks types at compile time, while dynamic typing checks them at run time. C# sits firmly on the strict side of both: it is strongly and statically typed.

Operators

C# vs JavaScript

C#: strong and static

An illegal mix is rejected before the program ever runs, so the bug is caught at compile time instead of at run time.

int x = 5;
x = "hello";     // Type error
int n = 5 + "3"; // Error: no coercion

JavaScript: weak and dynamic

The same code runs, but types bend silently. + quietly turns the number into text, and the mistake surfaces much later.

let x = 5;
x = "hello";     // fine, now a string
let n = 5 + "3"; // "53": coerced to text
let m = 5 * "3"; // 15: coerced to number

Neither result is "wrong" in JavaScript; its rules simply let unrelated types mix. C# trades that flexibility for catching the error up front.

Operators

Why It Matters

  • Fewer runtime surprises: many bugs become compile errors.
  • Better tooling: autocomplete and refactoring rely on known types.
  • A little more typing up front: you state your types, but gain safety in return.

Catching a type error at compile time is far cheaper than discovering it once the program is already running.

 

Conditionals

Section 08

Letting your program decide

Gustave Doré engraving of a forking road to perdition — choosing one branch over another
The Road to PerditionGustave Doré

Conditionals

Conditionals

A conditional runs code only when a certain condition is true.

Every conditional is driven by a boolean expression that evaluates to true or false. C# gives you a few branching tools, each suited to a different shape of decision:

  • if / else if / else: the general-purpose branch: run one block out of several.
  • The ternary ? :, a compact if/else that produces a value.
  • switch statement & expression: clean branching when one value is tested against many options.

Conditionals

if

The if is the simplest branch. On its own, it does one thing:

  • It tests the boolean condition inside the round parentheses.
  • If that condition is true, the statement runs.
  • If it's false, the statement is skipped and execution carries on below.

There's no fallback here, a lone if either runs its statement or does nothing.

int score = 60;

if (score >= 50)
{
    Console.WriteLine("You passed!");
}

Console.WriteLine("Done.");
You passed!
Done.

Conditionals

else

int score = 40;

if (score >= 50)
{
    Console.WriteLine("You passed!");
}
else
{
    Console.WriteLine("Try again.");
}
Try again.

An if often needs a fallback. That's what else provides:

  • else is optional and attaches to an if.
  • It runs only when the if's condition was false.
  • Exactly one of the two blocks always runs, never both, never neither.

Conditionals

else if

To choose between more than two outcomes, you can chain extra tests with else if:

  • Each else if adds another condition, checked only if the ones above it failed.
  • The final else is a catch-all when nothing matches.
int score = 35;

if (score >= 90)
{
    Console.WriteLine("Gold");
}
else if (score >= 50)
{
    Console.WriteLine("Silver");
}
else
{
    Console.WriteLine("Bronze");
}
Bronze

Conditionals

Reading a Flow Diagram

flowchart TD
  A([Start]) --> B{"score >= 90 ?"}
  B -->|true| C["print Gold"]
  B -->|false| D{"score >= 50 ?"}
  D -->|true| E["print Silver"]
  D -->|false| F["print Bronze"]
  C --> Z([Done])
  E --> Z
  F --> Z

A flow diagram shows the route a program takes as it runs. You start at the top and follow the arrows. Diamonds are decision points, each one has two arrows out, labelled true and false. Rectangles are actions.

  • If the first check returns true, gold is printed, and the program ends.
  • If the result is false, it sends you down to the next check.
  • If the second check succeeds, it prints Silver, otherwise, it prints Bronze.

Because the first match skips the rest, order matters: always try to put the highest-priority conditions first. Every path ends at Done.

Conditionals

Nested Conditionals

Since the if has a block of instructions, you can put another if inside that block. The inner one is reached only if the outer one holds, so both conditions must be true to reach the innermost block.

if (isLoggedIn)
{
    if (hasPermission)
    {
        OpenEditor();
    }
}

Useful, but deep nesting quickly gets hard to read. You can often flatten it, combine the tests with &&, or return early. Keep nesting shallow.

flowchart TD
  A([Start]) --> B{"isLoggedIn ?"}
  B -->|no| E([Done])
  B -->|yes| C{"hasPermission ?"}
  C -->|no| E
  C -->|yes| D["OpenEditor()"]
  D --> E

Conditionals

The Conditional (Ternary) Operator

The ternary operator ? : is a compact if/else that produces a value rather than executing a statement. It's written as "condition ? value if true : value if false"

The distinction in one line: a ternary returns a value, an if performs an action.

  • Use ? : when choosing between two values for one variable or argument.
  • Use a full if when a branch does something: multiple statements, side effects, or more than two outcomes.
int health = 30;
string status = health > 0 ? "Alive" : "Dead"; // "Alive"

// The long way, as if/else:
string label;
if (health > 0) label = "Alive";
else label = "Dead";

Conditionals

switch Statement

When you're comparing one value against many fixed options, a switch is cleaner than a long if / else if chain. Each case labels a value to match, break ends that case, and default catches anything left over.

Unlike C and C++, C# does not let one case silently fall through into the next. Each case must end (usually with break), so a case can't be skipped by accident.

switch (direction)
{
    case 'N':
        Console.WriteLine("Moved North");
        break;
    case 'S':
        Console.WriteLine("Moved South");
        break;
    case 'E':
        Console.WriteLine("Moved East");
        break;
    default:
        Console.WriteLine("Unknown");
        break;
}

Conditionals

How a switch Branches

flowchart TD
  A([direction]) --> B{"match?"}
  B -->|"N"| C["print Moved North"]
  B -->|"S"| D[" print Moved South"]
  B -->|"E"| E["print Moved East"]
  B -->|else| F["print Unknown"]
  C --> G([Done])
  D --> G
  E --> G
  F --> G

A switch evaluates the value once, then jumps straight to the matching case. It doesn't walk through a series of separate conditions the way an if chain does.

  • Each case is a single fixed value to match.
  • default is the catch-all, the switch equivalent of a final else.
  • Exactly one path runs, then execution continues after the closing brace.

Conditionals

switch vs if / else if

Both express the same logic. An if chain re-states the variable in every test, so it's the better fit when the conditions genuinely differ — ranges, several variables, complex boolean logic. A switch names the value once and lists the options, staying readable as cases multiply — ideal when matching one value against many constants.

if / else if chain

if (direction == 'N')
{
    Move(1);
}
else if (direction == 'S')
{
    Move(-1);
}
else
{
    Console.WriteLine("Unknown");
}

switch

switch (direction)
{
    case 'N':
        Move(1);
        break;
    case 'S':
        Move(-1);
        break;
    default:
        Console.WriteLine("Unknown");
        break;
}

Conditionals

Pattern Matching

Pattern matching checks a value’s shape, not just whether it equals something: its type, range or structure (or whether it is null), then optionally captures it in the same step, something a plain == check cannot do.

The is operator tests a single value; a switch tests it against many patterns at once.

Pattern matching — C# reference

object value = "Aria";

// Type pattern: test the type AND capture it
if (value is string text)
{
    Console.WriteLine(text.Length); // 4
}

// Type, constant and null patterns in a switch
string kind = value switch
{
    int n    => $"int: {n}",
    string s => $"string: {s}",
    null     => "nothing",
    _        => "other"
};

Conditionals

switch Expression

Patterns show up most in the switch expression: a modern, more advanced form of switch. Where the switch statement runs code, a switch expression evaluates to a value, using => arms and _ as the catch-all. Each arm is a pattern — the >= 90 below is a relational pattern, not a plain equality check.

string label = score switch
{
    >= 90 => "Gold",
    >= 50 => "Silver",
    _     => "Try again"
};

It belongs to C#'s functional programming features, the same family as lambdas and LINQ, which favour expressions that produce values over statements that perform actions. Next we see how patterns combine to do far more than match constants.

Conditionals

Combining Patterns

Patterns get expressive when combined, reading almost like English:

  • Relational: < > >= <= test a range, as in >= 90.
  • Logical: and, or, not join patterns, as in >= 0 and <= 100 or not null.
  • Guards: a when clause adds an extra condition to a case, e.g. case > 30 when isSummer:.

That is why a switch can do far more than match constants.

string grade = score switch
{
    < 0 or > 100 => "invalid", // logical: or
    >= 90        => "A",       // relational
    >= 50        => "pass",
    _            => "fail"
};

Conditionals

List Patterns

Newer C# can also match the shape of a whole array or list. A list pattern puts a sub-pattern in [ ] for each position.

  • .. matches any number of elements.
  • var captures an element into a name.
  • _ matches one element but ignores it.
int[] roll = { 1, 2, 3 };

// Match an exact shape
bool straight = roll is [1, 2, 3]; // true

// Capture pieces; .. skips the rest
if (roll is [var first, .., var last])
{
    Console.WriteLine($"{first}..{last}"); // 1..3
}

 

Loops

Section 09

Repeating work without repeating yourself

Gustave Doré engraving of Munchausen pulling on his own braid — a self-referential repeating act
Pulled on My Own BraidGustave Doré

Loops

Loops

Programs constantly need to repeat work: deal a card to each player, step over every tile on a map, keep updating a game until it ends. A loop does that for you, running a block of code (its body) over and over, each pass called an iteration.

Every loop is driven by a boolean condition, just like an if. The difference: an if runs its block at most once, while a loop keeps re-running it as long as the condition stays true.

  • while — repeat while a condition holds, the most basic form.
  • do-while — the same, but always run the body at least once.
  • for — repeat a known number of times, with its counter built in.
  • foreach — visit every item in a collection, no counter needed.

Loops

while Loop

flowchart TD
  A([Start]) --> R["roll a die"]
  R --> B{"roll != 6 ?"}
  B -->|yes| C["print; roll again"]
  C --> B
  B -->|no| E([Done])

The while loop is the most basic: give it a condition and it repeats its body as long as that condition stays true, checking it before every pass. Reach for it when the repetition count depends on something you cannot predict: user input, randomness, the state of the world.

Random rng = new Random();
int roll = rng.Next(1, 7); // A random 1-6
while (roll != 6)
{
    Console.WriteLine($"Rolled {roll}");
    roll = rng.Next(1, 7); // Roll again
}

Every run takes a different number of rolls, there is no count to calculate in advance.

Loops

Infinite Loops

A loop ends only when its condition becomes false. If the body never makes that happen, it forgets to change the variable, or changes it but skips past the target, the loop runs forever: this is an infinite loop, a classic beginner bug.

int n = 0;
while (n != 10)
{
    Console.WriteLine(n);
    n += 3; // 0, 3, 6, 9, 12... never exactly 10
}

Sometimes an endless loop is exactly what you want, a game’s main loop, for instance. In that case, you can write it on purpose with while (true) and step out with break when you are done.

while (true)
{
    string cmd = Console.ReadLine();
    if (cmd == "quit") break; // The way out
    Console.WriteLine($"You said: {cmd}");
}

Loops

do-while Loop

flowchart TD
  A([Start]) --> B["read input"]
  B --> C{"input != quit ?"}
  C -->|yes| B
  C -->|no| D([Done])

A do-while is a while with one twist: it checks the condition after the body, so the body always runs at least once. Ideal for menus and input prompts, where you must act before you can test.

string input;

do
{
    input = Console.ReadLine();
}
while (input != "quit");

Loops

for Loop

A for loop runs a block of code a set number of times. It packs three parts into one header, separated by semicolons:

  • Initializer int i = 0 — runs once, at the start.
  • Condition i < 5 — checked before each pass.
  • Step i++ — runs after each pass.
for (int i = 0; i < 5; i++)
{
    Console.WriteLine(i); // 0, 1, 2, 3, 4
}
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])

Loops

foreach Loop

The foreach loop walks through every item in a collection (an array, a list) in turn, handing you each one with no index to manage. It is the cleanest way to read a collection.

string[] names = { "Aria", "Ben", "Cleo" };

foreach (string name in names)
{
    Console.WriteLine(name);
}

Reach for for instead when you need the index, or want to replace elements.

flowchart TD
  A([Start]) --> B{"more items ?"}
  B -->|yes| C["item = next"]
  C --> D["print item"]
  D --> B
  B -->|no| E([Done])

Loops

Choosing a Loop

They all repeat, but each answers a different question. Pick by what you know going in:

for

You know the count, or need the index. "Do this 10 times."

while

Repeat while a condition holds, count unknown. "Until the player quits."

do-while

Like while, but run the body at least once. "Show the menu, then ask again."

foreach

Visit every item in a collection. "For each enemy in the wave."

When more than one fits, choose the one that best states your intent.

Loops

Loop Control

By default a loop runs its whole body every pass, then re-checks its condition at the top. Sometimes you need to steer it from the inside: bail out early, or skip the rest of a single pass. Two keywords do that, and both work in every loop (for, while, foreach):

break

Stops the loop entirely and jumps to the code after it. "I am done, no point continuing."

continue

Skips the rest of this pass and jumps straight to the next iteration. "Not this one, on to the next."

Both are almost always guarded by an if, so they fire only in the case you care about. The next two slides show each one in action.

Loop Control

break

flowchart TD
  A([Start]) --> B{"i < 10 ?"}
  B -->|no| F([Done])
  B -->|yes| C{"i == 5 ?"}
  C -->|yes| F
  C -->|no| D["print i"]
  D --> E["i++"]
  E --> B

break exits the loop immediately, skipping any remaining iterations. Great for stopping as soon as you've found what you need.

for (int i = 0; i < 10; i++)
{
    if (i == 5) break;    // Stop the whole loop at 5
    Console.WriteLine(i); // Print 0..4
}

Loop Control

continue

flowchart TD
  A([Start]) --> B{"i < 6 ?"}
  B -->|no| G([Done])
  B -->|yes| C{"i is odd ?"}
  C -->|yes| E["i++"]
  C -->|no| D["print i"]
  D --> E
  E --> B

continue skips the rest of the current pass and jumps to the next iteration. Use it to ignore items that don't qualify.

for (int i = 0; i < 6; i++)
{
    if (i % 2 != 0) continue; // Skip odd numbers
    Console.WriteLine(i);     // Print 0, 2, 4
}

Loop Control

Nested Loops

A loop inside another loop 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 innermost loop it's currently in.

for (int row = 0; row < 3; row++)
{
    for (int col = 0; col < 3; col++)
    {
        Console.Write($"({row},{col}) ");
    }
    Console.WriteLine();
}
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 --> B

 

Functions

Section 10

The first step towards organised code

Gustave Doré engraving of a parrot urging her on — a named helper called to act
The Parrot Urged Her OnGustave Doré

Functions

Function

ˈfʌŋk.ʃən

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.

Functions

Why Functions

So far our programs have been one long run of statements. That works until it does not: the same code is present three times, and a single screen of code becomes hard to follow. Functions fix both, giving blocks of logic a name, which you can write and call wherever you need.

Reuse

Write once, call anywhere (Don't Repeat Yourself).

Readability

A good name like CalculateDamage says what the code does, so the reader grasps it at a glance without tracing through the how.

Decomposition

Break a big problem into small, named steps you can reason about one at a time.

It is the first real tool for taming complexity, and the foundation programming is built upon.

Functions

C# Has No Standalone Functions

Strictly speaking, C# has no free-floating functions: every function belongs to an object, and is called a method (we'll meet methods properly in the OOP section). For now, top-level statements let us declare local functions and call them as if they were standalone.

A top-level statement

Code that simply runs, top to bottom. Fine once, but to repeat the work you repeat yourself.

Console.WriteLine("Hi, Aria!");
Console.WriteLine("Hi, Ben!");

A local function

Name the work once, then call it by name as often as you like.

void Greet(string who)
    => Console.WriteLine($"Hi, {who}!");

Greet("Aria");
Greet("Ben");

Functions

Anatomy of a Function

int Add(int a, int b)
{
    return a + b;
}

int sum = Add(3, 4); // 7

Every function declaration is built from the same parts:

  1. Return type int — the kind of value it hands back (void if none).
  2. Name Add — how you call it; pick a verb that says what it does.
  3. Parameters (int a, int b) — the named inputs, in parentheses.
  4. Body — the { } block that runs when it is called.
  5. return — sends a value back and ends the function.

The name and parameter list together form the function's signature, what identifies it (the return type isn't part of it, so two functions can't differ by return type alone). The call Add(3, 4) then runs it and becomes 7.

Functions

Parameters and Arguments

Parameters are the named inputs in a function's definition. Arguments are the values that get passed once the function is called.

void Greet(string name) // name is a parameter
{
    Console.WriteLine($"Hello, {name}");
}

Greet("Aria"); // "Aria" is the argument

A parameter is just a variable. Each call fills it with whatever argument you hand over:

Greet("Aria"); // name = "Aria"
Greet("Ben");  // name = "Ben"
Greet("Cleo"); // name = "Cleo"

Same parameter, a different argument every call.

Functions

Returning a Value

A function's return type declares the kind of value it hands back. The return keyword sends that value to the caller and ends the function on the spot.

int Add(int a, int b)
{
    return a + b; // Hands an int back to the caller
}

int total = Add(3, 4) + Add(1, 1); // 7 + 2 = 9

Because the call becomes the value it returns, you can use it anywhere that value fits: store it, print it, or combine it in a bigger expression. Think of a returning function as a question that gives an answer.

Functions

void: Do or Return

Returns a value

Computes an answer you use elsewhere. Its type is the kind of value it gives back.

int Square(int n)
{
    return n * n;
}

int area = Square(5); // 25

Returns nothing: void

Performs an action (print, move, save) with no value to hand back. Its type is void.

void PrintScore(int score)
{
    Console.WriteLine($"Score: {score}");
}

PrintScore(1500);

Functions

Passing Arguments

The basic call passes values in order: Move(10, 5). But C# offers several ways to make calls clearer and more flexible. The next slides cover each:

  • Named arguments — label each value at the call site.
  • Optional parameters — give a default so callers can omit it.
  • params — accept any number of arguments at once.
  • ref / out — pass the variable itself, not just a copy.

The first three are about calling convenience; ref and out change how the argument is passed.

Functions

Named Arguments

void Move(int x, int y)
{
    // ...
}

Move(10, 5);       // Positional, order matters
Move(x: 10, y: 5); // Named, declared order
Move(y: 5, x: 10); // Named, reordered (same result)

A named argument labels a value with its parameter's name right at the call site. It makes a call self-documenting — Move(x: 10, y: 5) reads better than a bare Move(10, 5).

You can either keep the parameters' declared order, or list them in a different order — with names, it's the label, not the position, that decides where each value goes.

That reordering is only possible because they're named. Plain positional arguments must follow the parameter order exactly; you can't shuffle them around without giving each one a name.

Functions

Optional Parameters

Give a parameter a default value and callers may leave it out. When they do, the default is used in its place.

Optional parameters must come after all the required ones, so the compiler can tell which arguments you've supplied.

Pair them with named arguments to skip over one optional parameter while still setting another.

void Attack(int damage, bool critical = false)
{
    int dealt = critical ? damage * 2 : damage;
    Console.WriteLine($"Hit for {dealt}");
}

Attack(10);                 // Hit for 10 (critical omitted → false)
Attack(10, true);           // Hit for 20 (critical → true)
Attack(10, critical: true); // Hit for 20 (same, but clearer)

Functions

The params Keyword

params lets one parameter accept any number of arguments: the compiler gathers them into an array for you, so the caller just lists values instead of building the array first. It's how methods like Console.WriteLine and string.Format take a variable number of inputs.

void Log(string tag, params string[] messages)
{
    foreach (string m in messages)
        Console.WriteLine($"[{tag}] {m}");
}

Log("INFO", "started", "loading", "ready"); // many arguments
Log("WARN", "low memory");                  // just one
Log("DEBUG");                               // none is still valid

It coexists with ordinary parameters (tag here), but with two rules: a method may have only one params parameter, and it must be the last one. Callers can pass a comma-separated list, a ready-made array, or nothing at all.

Functions

ref and out Parameters

By default an argument is passed by value: the function gets a copy, so changes inside it never reach the caller's variable (the value-vs-reference split from Types). ref and out pass the variable itself, letting the function write back to it.

  • ref — the variable must be initialised before the call. The function may read it and change it.
  • out — the variable need not be set first, but the function must assign it before returning. It's the way to hand back extra results.

The keyword goes on both the parameter and the argument, so the hand-off is visible at the call site.

// ref: change an existing value</div>
    </div>
    void Double(ref int n)
{
    n *= 2;
}

int x = 5;
Double(ref x); // x is now 10

// out: hand back a second result
bool ok = int.TryParse("42", out int result);
// ok is true, result is 42

Functions

Expression-Bodied Functions

When a function is a single expression, => gives a compact one-line form — no braces, no return keyword.

int Square(int n) => n * n;
string Greet(string name) => $"Hello, {name}";

Identical behaviour to the block form, just tidier for short functions.

Functions

Recursion

flowchart TD
  A["Factorial(4)"] --> B["4 * Factorial(3)"]
  B --> C["3 * Factorial(2)"]
  C --> D["2 * Factorial(1)"]
  D --> E["1 — base case, stop"]

A recursive function is one that calls itself, solving a problem by reducing it to a smaller version of the very same problem.

Every recursive function is built from two parts:

  • A base case — the simplest input, answered directly with no further call. It's what stops the recursion.
  • A recursive case — calls the function again on a smaller input, always moving toward the base case.

Each call pauses, waiting on the one inside it; nothing resolves until the base case is finally reached.

A function that calls itself.

Functions

Unwinding the Stack

int Factorial(int n)
{
    if (n <= 1) return 1;        // Base case
    return n * Factorial(n - 1); // Recursive case
}

Factorial(4); // 24

Factorial(4) can't finish until it knows Factorial(3), which waits on Factorial(2), and so on. Each unfinished call is stacked up, paused part-way through.

At Factorial(1) the base case returns 1 outright. Now the stack unwinds, each waiting call multiplies and returns in turn: 1 → 2 → 6 → 24.

Forget the base case, or fail to shrink the input toward it, and the calls never stop. The pile of paused calls grows until it runs out of room and the program crashes with a stack overflow, recursion's version of the infinite loop.

 

Scope

Section 11

Where in your code a name can be seen

Gustave Doré engraving of a labyrinth — bounded regions where a name is visible
In Labyrinth of Many a RoundGustave Doré

Scope

Scope

skoʊp

1The region of source code in which a name is visible and can be used. Outside that region, the compiler doesn't recognize the name.

e.g.A counter declared inside a loop can be used within the loop, but not on the line after it.

Scope

Scopes Nest

Scopes are nested: a smaller region sits inside a larger one. A name declared in a scope is visible there and in every scope inside it, but not in the code outside.

  • A block { } — the smallest scope: a loop body, an if, or a bare pair of braces.
  • A function — its parameters and locals are visible to every block written inside it.

Wider scopes exist too, but they belong to the Object Oriented part of C#. For now, we work with these two levels.

Scope

Block Scope

A name declared inside a block { } is visible only within that block, from its declaration to the closing brace. This holds for every block: an if, a loop body, or braces on their own.

if (isAlive)
{
    int bonus = 50; // Exists only inside these braces
    score += bonus;
}
// bonus is not accessible here

for (int i = 0; i < 3; i++)
{
    // i is scoped to the loop
}
// i is not accessible here

A function's parameters follow the same rule, they're locals scoped to the whole function body.

Scope

Inner Scopes See Outer Ones

void Run()
{
    int outer = 1;            // Function scope

    if (outer == 1)
    {
        int inner = 2;        // Block scope
        outer += inner;       // Outer is visible here
    }

    Console.WriteLine(inner); // Error: inner left scope
}

Visibility flows inward, never outward. Code in an inner block can freely use names from the blocks that enclose it, because those outer scopes are still active around it.

The reverse fails: an outer scope cannot see names declared in a block nested inside it. By the time you're back at the outer level, the inner name is already gone.

Think of each { } as a one-way window: you can look out, but not in.

Scope

Variable Shadowing

Shadowing is when an inner scope declares a name that already exists in an enclosing scope, "hiding" the outer one for that region. To prevent silent confusion, C# forbids it between locals. You can't redeclare a name an enclosing block already introduced:

int value = 10;
{
    int value = 20; // Error: value is already in scope here
}

The same applies to a loop counter or any nested block. Clashing names are a common source of bugs, and C# would rather stop you up front. Clear, distinct names avoid the problem entirely.

Scope

Why Scope Matters

  • Smaller scopes are safer. A name visible only where it's needed can't be misused elsewhere. Declare variables as close to their use as you can.
  • Fewer name clashes. Because names are confined to their scope, unrelated parts of a program can reuse simple names like i or count without colliding.
  • Easier to read. A tight scope tells the reader exactly where a variable is relevant — they never have to wonder whether it's used somewhere far away.

Scopes help you keep the program under control: they let each part of the code reason about its own names without worrying about anything else.

 

Wrapping Up

Section 12

What we covered, and where you go next

Gustave Doré engraving of a vast, wide river — the journey opening out ahead
The River Was So WideGustave Doré

Wrapping Up

What You've Learned

Starting from "what is a program", you've assembled the core of the C# language, the foundation everything else in the course builds on:

How C# Runs

Source → CIL → machine code, the runtime, and the compiler.

Variables & Types

Declaring data, value vs reference types, and the built-in numeric, text and boolean types.

Operators

Arithmetic, comparison, logic and more.

Conditionals

if / else, switch, and pattern matching.

Loops

while, for, foreach, break and continue.

Functions & Scope

Naming reusable blocks of logic, and the regions where names live.

Wrapping Up

What's Next

The next module, C# Intermediate, turns from writing instructions to structuring programs, centered on object-oriented programming.

  • Memory — the stack, the heap, and the garbage collector that manages them.
  • Classes, objects & structs — bundling data together with the code that operates on it.
  • Inheritance & polymorphism — deriving new types from existing ones.
  • Generics, collections & exceptions — reusable types, common data structures, and structured error handling.

Together, these complete the language, preparing the ground for Unity.

Programming in Unity · C# Fundamentals

That's a Wrap

The foundations are in place. From here, the focus shifts from writing instructions to structuring programs, shaping data and logic into types of your own.