Back to C# Examples

Basic C#

A quick-reference and hands-on guide to the fundamentals of C# programming.

How to use this page

The fastest way to keep what you learn is to teach it. The physicist Richard Feynman's trick was simple: if you can't explain something in plain words, you don't really understand it yet. For each section, work these four steps:

  1. Read the lesson once.
  2. Explain it out loud in plain words, as if teaching a friend — no jargon. If you stall, that's the gap.
  3. Do the exercise from a blank editor, without peeking. Where you get stuck is exactly what to reread.
  4. Reveal the solution, compare, and explain the idea again in your own words.

Every exercise is a complete program. Create a project once with dotnet new console -o practice, paste an exercise into Program.cs, and run it with dotnet run. Watch for the 💡 Explain it simply prompts and ✎ Exercise boxes in each section. Hover any code block and click Copy to grab it.

Contents

  1. What is C#?
  2. Program Structure
  3. Variables & Data Types
  4. Operators
  5. Strings
  6. Collections
  7. Control Structures
  8. Loops
  9. Methods
  10. Classes & OOP
  11. Interfaces & Inheritance
  12. Exception Handling

1. What is C#?

A programming language is a precise, written notation for telling a computer what to do. C# (pronounced “C sharp”) was designed at Microsoft by Anders Hejlsberg and released in 2002 as part of the .NET platform. Its goal was a language as productive as Java or Visual Basic but as familiar to C and C++ programmers as possible, which is why its curly-brace syntax looks so much like theirs. Today .NET is open source and runs on Windows, macOS, and Linux, and C# is used for web services, desktop applications, cloud software, and games built with the Unity engine.

C# is statically typed: every variable has a type that the compiler checks before the program runs, so many mistakes are caught early. It is also compiled, but in two stages. The C# compiler first translates your source code into Intermediate Language (IL), a portable, processor-neutral instruction set. When the program runs, the .NET runtime, called the Common Language Runtime (CLR), converts that IL into native machine code on the fly with a just-in-time (JIT) compiler. The runtime also provides garbage collection: it automatically frees memory your program no longer uses, so you never release memory by hand.

Finally, C# is object-oriented. Programs are organised around classes, blueprints that bundle data together with the code that operates on it. You will meet classes properly in section 10, but you will see the word class from the very first program.

.NET
The platform C# runs on: the runtime, the standard library, and the dotnet command-line tools.
IL and JIT
C# compiles to portable Intermediate Language, which the runtime compiles to machine code just in time.
Garbage collection
Automatic reclaiming of memory that is no longer reachable by the program.
Explain it simply

Tell a friend, in two sentences and without the words “compile” or “runtime”, what happens between writing a C# file and seeing it run.

Reveal a plain-language answer

First a tool checks your whole program for type mistakes and translates it into a general-purpose instruction format that isn't tied to any one kind of computer. Then, when you start it, .NET translates those instructions into the exact language your particular processor speaks, runs them, and tidies away memory you've finished with, so you don't have to.

2. Program Structure

Every C# program follows the same basic skeleton. Once you recognise that skeleton, every later example becomes easier to read, because only the code inside it changes.

A program usually begins with using directives, such as using System;, which let you refer to types in another namespace without typing their full names. A namespace is a named container that groups related types and prevents name clashes: two libraries can each have a class called Parser if they live in different namespaces. Inside the namespace is a class, and inside the class is a special method called Main. Main is the program's entry point: when you run the program, execution starts at its first line, and when Main finishes, the program ends.

C# groups statements into blocks with curly braces { }, and every statement ends with a semicolon ;. C# is case-sensitive, so Main and main are different names. Comments are notes for human readers that the compiler ignores; they start with // for one line or are wrapped in /* … */ for several lines. Since C# 9 you may also write top-level statements, code placed directly in the file, and the compiler generates the class and Main for you. The remaining exercises on this page use that shorter style.

Namespace
A named grouping of types, used to organise code and avoid name clashes.
using directive
Imports a namespace so its types can be used by their short names.
Entry point
The method where execution begins: Main, or the top-level statements.
using System;           // import the System namespace

namespace MyApp
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello, World!");
        }
    }
}
C# 9+ supports top-level statements — you can omit the class and Main boilerplate and write code directly in a .cs file.
// Top-level statement style (C# 9+)
Console.WriteLine("Hello, World!");
Exercise — your turn

Complete the classic program structure so that dotnet run prints exactly Hello, C#! Fill in the one missing line.

using System;

namespace MyApp
{
    class Program
    {
        static void Main(string[] args)
        {
            // TODO: print exactly: Hello, C#!
        }
    }
}
Show solution
using System;

namespace MyApp
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello, C#!");
        }
    }
}
Output
Hello, C#!
Explain it simply

Explain to a friend why a C# program needs a Main method. What would go wrong without one?

Reveal a plain-language answer

A program is a pile of instructions, and the computer needs to know which one to do first. Main is the front door: .NET always starts there. Without it, the compiler has no idea where to begin, so it refuses to build a runnable program. With top-level statements, the compiler simply writes that front door for you.

3. Variables & Data Types

A variable is a named storage location that holds a value while a program runs. Think of it as a labelled box: the label is the variable's name, and the contents are its value. The contents can change during the program, but the kind of thing the box holds cannot.

That kind is the variable's data type. The type decides which values are allowed and which operations make sense on them. C#'s common built-in types are int for whole numbers, double for numbers with a fractional part, decimal for exact base-10 values such as money, bool for true or false, char for a single character, and string for text. You declare a variable by writing its type and then its name, as in int age = 30;, or you can write var and let the compiler infer the type from the value. Either way the variable is still strongly typed; var only saves typing.

Types in C# fall into two families. Value types (int, double, bool, decimal, and structs) store their data directly, so copying one copies the value. Reference types (string, arrays, and classes) store a reference to data kept elsewhere in memory, so two variables can refer to the same object. Ordinary value types can never be null; adding a question mark, as in int?, makes a nullable version that can. A const is a value fixed at compile time that can never change.

Declaration
Introducing a variable by giving its type (or var) and its name.
Value vs. reference type
Stores the data itself / stores a reference to data held elsewhere.
Nullable
A type marked with ? that may also hold null, meaning “no value”.
int     age     = 30;
double  price   = 9.99;
float   temp    = 36.6f;
bool    active  = true;
char    grade   = 'A';
string  name    = "Alice";
decimal money   = 99.95m;   // high-precision — use for currency
object  anything = 42;      // base type of all types

// var — type inferred by the compiler
var city  = "NYC";           // inferred as string
var count = 10;              // inferred as int

// Constants
const int MAX = 100;

// Nullable types — can hold null
int? maybe = null;
string? optName = null;
TypeSizeRange / Use
int4 bytes–2,147,483,648 to 2,147,483,647
long8 bytesVery large whole numbers
double8 bytes~15–17 significant digits
decimal16 bytes28–29 significant digits — use for money
bool1 bytetrue or false
char2 bytesSingle Unicode character
stringvariableImmutable sequence of characters
Exercise — your turn

Declare the four variables so the program prints the three lines shown in the solution. Notice what a bool looks like when printed, and why price is a decimal.

using System;

// TODO: declare name ("Ada"), age (36), height (1.65) and isAdmin (false)
decimal price = 19.99m;
int quantity = 3;

Console.WriteLine($"{name} is {age} years old and {height} m tall.");
Console.WriteLine($"Admin: {isAdmin}");
Console.WriteLine($"Total: {price * quantity}");
Show solution
using System;

string name = "Ada";
int age = 36;
double height = 1.65;
bool isAdmin = false;
decimal price = 19.99m;
int quantity = 3;

Console.WriteLine($"{name} is {age} years old and {height} m tall.");
Console.WriteLine($"Admin: {isAdmin}");
Console.WriteLine($"Total: {price * quantity}");
Output
Ada is 36 years old and 1.65 m tall.
Admin: False
Total: 59.97
Explain it simply

Why would you store a price as decimal rather than double? Explain it without the word “precision”.

Reveal a plain-language answer

A double stores numbers in binary, and many everyday decimal amounts such as 0.10 can't be written exactly in binary, just as 1/3 can't be written exactly in decimal. The tiny leftovers add up, so sums of money can come out a fraction of a cent wrong. A decimal stores the digits in base 10, the way we write money, so 19.99 is exactly 19.99.

4. Operators

An operator is a symbol that tells the computer to perform an operation on one or more values, called operands. In a + b, the + is the operator and a and b are its operands. Any combination of values and operators that produces a result is an expression.

Arithmetic operators (+ - * / %) compute numbers. Watch integer division: when both operands are integers, / discards the remainder, so 10 / 3 is 3, and % (modulus) gives that remainder, 1. If either operand is a double, the division keeps the fraction. Comparison operators (== != < > <= >=) produce a bool. Logical operators combine booleans: && (AND), || (OR), and ! (NOT). Assignment operators (= += -= *= and so on) store a result in a variable, and ++ / -- add or subtract one.

C# also has operators designed for null, the value meaning “nothing here”. The null-coalescing operator a ?? b gives a unless it is null, in which case it gives b. The null-conditional operator a?.Member reads a member only if a is not null, and produces null otherwise instead of crashing. Operators follow precedence rules, so multiplication happens before addition, as in ordinary arithmetic. Use parentheses whenever the order might not be obvious to a reader.

Operand
A value an operator acts on.
Integer division
Dividing two integers, which drops any remainder: 7 / 2 is 3.
Precedence
The rules deciding which operator in an expression is applied first.

Arithmetic

int a = 10, b = 3;
a + b    // 13
a - b    // 7
a * b    // 30
a / b    // 3  — integer division
a % b    // 1  — modulus
a++      // post-increment

Comparison & Logical

x == y   // equal
x != y   // not equal
x <  y   // less than
x >  y   // greater than

a && b   // logical AND
a || b   // logical OR
!a        // logical NOT

Null-related Operators

// Null-coalescing — use right side if left is null
string display = name ?? "Guest";

// Null-coalescing assignment
name ??= "Guest";

// Null-conditional — safe member access
int? len = name?.Length;   // null if name is null
Exercise — your turn

A carton holds 5 eggs. Using only /, % and ??, finish the three TODOs so the program reports full cartons, leftovers, and a greeting that falls back to “friend”.

using System;

int eggs = 17;
int perBox = 5;
string? nickname = null;

Console.WriteLine($"Full boxes: {0 /* TODO */}");
Console.WriteLine($"Left over: {0 /* TODO */}");
Console.WriteLine($"Hello, {"" /* TODO: nickname, or "friend" if null */}");
Show solution
using System;

int eggs = 17;
int perBox = 5;
string? nickname = null;

Console.WriteLine($"Full boxes: {eggs / perBox}");
Console.WriteLine($"Left over: {eggs % perBox}");
Console.WriteLine($"Hello, {nickname ?? "friend"}");
Output
Full boxes: 3
Left over: 2
Hello, friend
Explain it simply

Explain what ?? does using a real-life example, like a spare key.

Reveal a plain-language answer

It's a backup plan. “Use the key in my pocket, but if my pocket is empty, use the spare under the mat.” nickname ?? "friend" says: use the nickname if there is one; if it's missing (null), use "friend" instead.

5. Strings

A string is a sequence of characters used to represent text: a name, a sentence, a line read from a file. In C# you write a string between double quotes, as in "hello". A single character is a separate type, char, written between single quotes, as in 'h'. You can read an individual character by its position with square brackets: name[0] is the first character.

C# strings are immutable: once a string exists, its contents never change. Methods that appear to modify a string, such as ToUpper(), Trim(), or Replace(), actually return a new string and leave the original untouched. That is why you must use the result, for example name = name.Trim();. When you build a long string piece by piece in a loop, the StringBuilder class avoids creating many throwaway strings.

The clearest way to build text from values is string interpolation. Put a $ before the opening quote and write any expression inside braces: $"Hello, {name}!". A verbatim string, marked with @, treats backslashes literally, which is handy for Windows file paths. The string type also has a large set of built-in methods for searching (Contains, IndexOf), slicing (Substring), splitting (Split), and joining (string.Join).

Immutable
Cannot be changed after creation; “changes” produce a new string.
Interpolation
Embedding expressions in a $"…" string using braces.
Index
A character's position in the string, counting from 0.
string first = "Alice";
string last  = "Smith";

// Concatenation
first + " " + last               // "Alice Smith"

// String interpolation (preferred)
$"Hello, {first}!"                // "Hello, Alice!"
$"Age: {age + 1}"                 // expressions work

// Verbatim string — backslashes are literal
@"C:\Users\Alice\Documents"

// Common string methods
first.Length                        // 5
first.ToUpper()                     // "ALICE"
first.ToLower()                     // "alice"
first.Trim()                        // strip whitespace
first.Contains("lic")              // true
first.StartsWith("Al")             // true
first.Replace("l", "r")            // "Arice"
first.Substring(1, 3)              // "lic"
first.Split(',')                   // string array
string.Join(", ", arr)             // join array to string
string.IsNullOrEmpty(name)         // true if null or ""
Exercise — your turn

Clean up the messy name: trim the spaces, capitalise the first letter of each word, and print it with its length. Finish the loop body and the line that builds full.

using System;

string raw = "  ada lovelace  ";

string[] words = raw.Trim().Split(' ');
for (int i = 0; i < words.Length; i++)
{
    // TODO: replace words[i] with its first letter upper-cased + the rest
}
string full = ""; // TODO: join the words back together with a space

Console.WriteLine($"{full} ({full.Length} chars)");
Show solution
using System;

string raw = "  ada lovelace  ";

string[] words = raw.Trim().Split(' ');
for (int i = 0; i < words.Length; i++)
{
    words[i] = char.ToUpper(words[i][0]) + words[i].Substring(1);
}
string full = string.Join(" ", words);

Console.WriteLine($"{full} ({full.Length} chars)");
Output
Ada Lovelace (12 chars)
Explain it simply

If strings can't be changed, how does name.ToUpper() work? Explain it like you're describing a photocopier.

Reveal a plain-language answer

It doesn't touch the original page at all. It makes a photocopy with every letter in capitals and hands you the copy. The original is still sitting there unchanged, so if you want to keep the capitalised version, you have to hold on to the copy, for example by assigning it back to name.

6. Collections

Programs rarely work with just one value. A collection is an object that holds many values under a single name. Choosing the right collection for the job is one of the most common design decisions you will make.

An array is the simplest collection: a fixed number of elements of the same type, reached by an index that counts from zero. Its length is set when it is created and cannot change. A List<T> is a resizable list: you can Add, Insert, and Remove items, and it grows automatically. The <T> means List is a generic type: you fill in the element type, so a List<string> can hold only strings, and the compiler enforces it.

A Dictionary<TKey, TValue> stores key–value pairs. Instead of finding a value by its numeric position, you find it by a key, such as a word to find how many times it appears. Keys must be unique, and lookups are very fast, even with millions of entries. Reading a key that doesn't exist throws an error, so use ContainsKey, TryGetValue, or GetValueOrDefault when a key might be missing. All of these types live in the System.Collections.Generic namespace.

Generic type
A type with a placeholder, such as List<T>, that you fill in with a concrete type.
Index
An element's position in an array or list, starting at 0.
Key–value pair
An entry in a dictionary: a unique key associated with one value.

Arrays

int[] nums = { 10, 20, 30 };
nums[0];                   // 10
nums.Length;               // 3

int[] sized = new int[5];  // fixed size, all zeros

List<T>

using System.Collections.Generic;

var fruits = new List<string> { "apple", "banana", "cherry" };

fruits.Add("date");          // append
fruits.Remove("banana");     // remove by value
fruits.Count;                // number of items
fruits[0];                   // "apple"
fruits.Contains("apple");   // true
fruits.Sort();               // sort in place

Dictionary<K, V>

var ages = new Dictionary<string, int>
{
    { "Alice", 30 },
    { "Bob",   25 },
};

ages["Alice"];              // 30
ages["Carol"] = 28;         // add or update
ages.ContainsKey("Bob");   // true
ages.Remove("Bob");

// Safe lookup
if (ages.TryGetValue("Alice", out int val))
    Console.WriteLine(val);
Exercise — your turn

Add 95 to the list, sort it, and print it. Then count the words in a sentence with a dictionary. Fill in the three TODOs.

using System;
using System.Collections.Generic;

var scores = new List<int> { 88, 92, 75 };
// TODO: add 95, then sort the list
Console.WriteLine($"Scores: {string.Join(", ", scores)}");

var counts = new Dictionary<string, int>();
foreach (string word in "the cat and the hat".Split(' '))
{
    // TODO: add one to this word's count (it starts at 0)
}
Console.WriteLine($"the = {counts["the"]}, cat = {counts["cat"]}");
Show solution
using System;
using System.Collections.Generic;

var scores = new List<int> { 88, 92, 75 };
scores.Add(95);
scores.Sort();
Console.WriteLine($"Scores: {string.Join(", ", scores)}");

var counts = new Dictionary<string, int>();
foreach (string word in "the cat and the hat".Split(' '))
{
    counts[word] = counts.GetValueOrDefault(word) + 1;
}
Console.WriteLine($"the = {counts["the"]}, cat = {counts["cat"]}");
Output
Scores: 75, 88, 92, 95
the = 2, cat = 1
Explain it simply

When would you pick a Dictionary over a List? Give an everyday example of each.

Reveal a plain-language answer

A List is like a queue of people: what matters is the order, and you find someone by their place in line. A Dictionary is like a phone book: you don't care about the order, you look someone up by their name, the key, and jump straight to their number, the value. If you find yourself searching a list for an item by name, you probably want a dictionary.

7. Control Structures

By default a program runs its statements in order, top to bottom. Control structures let a program make decisions, running some statements only when certain conditions are true. This is what allows software to respond differently to different inputs.

The if statement evaluates a condition, a bool expression wrapped in parentheses, and runs its block only if the condition is true. else if tests further conditions in order, and a final else catches every remaining case. Only the first matching branch runs. If a branch has a single statement the braces are optional, but many teams always write them, because adding a second line later without braces is a classic bug.

When one value must be compared against many possibilities, switch is clearer than a long chain of ifs. Modern C# (version 8 and later) also has the switch expression, which produces a value: each arm is a pattern followed by => and a result, and _ is the catch-all. Patterns can compare with relational operators, as in >= 90. For a simple two-way choice there is the ternary operator, condition ? a : b, which evaluates to a if the condition is true and b otherwise.

Condition
A bool expression that decides which branch runs.
Pattern
A test used in a switch, such as a constant, a type, or >= 90.
Ternary operator
cond ? a : b, an expression that chooses between two values.

if / else if / else

int score = 75;

if (score >= 90)
    Console.WriteLine("A");
else if (score >= 75)
    Console.WriteLine("B");
else
    Console.WriteLine("C or below");

switch expression (C# 8+)

string grade = score switch
{
    >= 90 => "A",
    >= 75 => "B",
    >= 60 => "C",
    _      => "F",    // default (_)
};

Ternary

string status = age >= 18 ? "adult" : "minor";
Exercise — your turn

Write the body of Grade as a switch expression (90+ A, 80+ B, 70+ C, 60+ D, otherwise F), and use the ternary operator to set parity.

using System;

Console.WriteLine($"{Grade(95)} {Grade(82)} {Grade(64)} {Grade(40)}");

int n = 7;
string parity = ""; // TODO: "even" or "odd" using ? :
Console.WriteLine($"{n} is {parity}");

static string Grade(int score) => score switch
{
    // TODO: the five arms, ending with _ => "F"
};
Show solution
using System;

Console.WriteLine($"{Grade(95)} {Grade(82)} {Grade(64)} {Grade(40)}");

int n = 7;
string parity = n % 2 == 0 ? "even" : "odd";
Console.WriteLine($"{n} is {parity}");

static string Grade(int score) => score switch
{
    >= 90 => "A",
    >= 80 => "B",
    >= 70 => "C",
    >= 60 => "D",
    _     => "F",
};
Output
A B D F
7 is odd
Explain it simply

In a switch expression, why does the order of the arms matter? What would happen if >= 60 came first?

Reveal a plain-language answer

C# checks the arms from top to bottom and stops at the first one that fits. A score of 95 is also at least 60, so if >= 60 came first, everyone who passed would get a D. Put the most specific (strictest) tests first and the catch-all _ last. The compiler will even warn you if an arm can never be reached.

8. Loops

A loop repeats a block of code, either a set number of times or until a condition changes. Repetition is at the heart of programming: processing every line in a file, every order in a list, or every frame of a game relies on loops. Each pass through the loop body is called an iteration.

C# has four loops. The for loop has three parts: an initialiser, a condition checked before each iteration, and an update run after each one, as in for (int i = 0; i < 5; i++). Use it when you need a counter. The foreach loop visits every element of a collection in turn, with no counter to get wrong, and is the usual choice for arrays, lists, and dictionaries. The while loop repeats as long as its condition stays true and checks that condition before each pass, so it may run zero times. The do … while loop checks after each pass, so its body always runs at least once.

Two keywords steer a loop from the inside: break leaves the loop immediately, and continue skips the rest of the current iteration and moves on to the next. A loop whose condition never becomes false is an infinite loop. That is occasionally intended, but usually it is a bug where the loop variable is never updated.

Iteration
One pass through the body of a loop.
Loop variable
A counter such as i that changes each iteration.
Off-by-one error
Looping one time too many or too few, often < vs <=.

for

for (int i = 0; i < 5; i++)
    Console.WriteLine(i);
// 0 1 2 3 4

foreach

var fruits = new List<string> { "apple", "banana", "cherry" };

foreach (string fruit in fruits)
    Console.WriteLine(fruit);

while & do-while

int i = 0;
while (i < 5)
    Console.WriteLine(i++);

// do-while always runs at least once
do
{
    Console.WriteLine(i);
    i--;
} while (i > 0);
Use break to exit a loop, continue to skip to the next iteration.
Exercise — your turn

The classic FizzBuzz. For 1 to 15, add “Fizz” for multiples of 3, “Buzz” for multiples of 5, “FizzBuzz” for multiples of both, and the number otherwise. Write the loop.

using System;
using System.Collections.Generic;

var parts = new List<string>();
// TODO: loop i from 1 to 15 and add the right word (or number) to parts

Console.WriteLine(string.Join(" ", parts));
Show solution
using System;
using System.Collections.Generic;

var parts = new List<string>();
for (int i = 1; i <= 15; i++)
{
    if (i % 15 == 0)
        parts.Add("FizzBuzz");
    else if (i % 3 == 0)
        parts.Add("Fizz");
    else if (i % 5 == 0)
        parts.Add("Buzz");
    else
        parts.Add(i.ToString());
}
Console.WriteLine(string.Join(" ", parts));
Output
1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz
Explain it simply

Explain the difference between while and do … while using a restaurant.

Reveal a plain-language answer

while is a host who checks for a free table before seating you. If there's none, you never sit down at all. do … while seats you first and checks afterwards whether you want to stay, so you always get at least one course. Use do … while when the body must happen once, such as showing a menu before asking for a choice.

9. Methods

A method is a named block of code that performs one task. (Other languages call these functions; in C# every method belongs to a class.) Methods are the main tool for breaking a large problem into small, understandable, reusable pieces: you write the steps once, name them, and call the name wherever the task is needed.

A method's signature declares its return type, its name, and its parameters, the inputs listed in parentheses, each with a type. The values supplied when you call it are the arguments. A method hands back a result with return; one that returns nothing has the return type void. When the whole body is a single expression, you can write it compactly with =>, as an expression-bodied method. Methods marked static belong to the class itself rather than to any one object, which is why the simple programs on this page can call them directly.

C# gives you flexible ways to pass data. Optional parameters have a default value that is used when the caller leaves them out. A params array lets a caller pass any number of arguments. By default, arguments are passed by value, so the method receives a copy. An out parameter lets a method send extra results back through a variable supplied by the caller, and ref lets it modify the caller's variable directly. Two methods may share a name if their parameter lists differ, which is called overloading.

Signature
A method's name together with its parameter types.
Parameter / argument
The variable in the definition / the actual value supplied in the call.
out parameter
A parameter the method must assign, used to return extra values.
// Basic method
static string Greet(string name)
{
    return $"Hello, {name}!";
}

// Expression-bodied method (single expression)
static int Add(int a, int b) => a + b;

// Optional / default parameters
static string Greet(string name = "World") => $"Hello, {name}!";

// out parameter — return multiple values
static void Divide(int a, int b, out int quotient, out int remainder)
{
    quotient  = a / b;
    remainder = a % b;
}

Divide(10, 3, out int q, out int r);  // q=3, r=1

// params — variable number of arguments
static int Sum(params int[] nums) => nums.Sum();
Sum(1, 2, 3, 4);   // 10
Exercise — your turn

Implement Sum, which takes any number of integers, and MinMax, which reports the smallest and largest values through two out parameters.

using System;

Console.WriteLine($"Sum: {Sum(1, 2, 3, 4, 5)}");

MinMax(new[] { 4, 1, 9, 3 }, out int lo, out int hi);
Console.WriteLine($"Min: {lo}, Max: {hi}");

static int Sum(params int[] nums)
{
    // TODO: add up nums and return the total
}

static void MinMax(int[] values, out int min, out int max)
{
    // TODO: assign min and max
}
Show solution
using System;

Console.WriteLine($"Sum: {Sum(1, 2, 3, 4, 5)}");

MinMax(new[] { 4, 1, 9, 3 }, out int lo, out int hi);
Console.WriteLine($"Min: {lo}, Max: {hi}");

static int Sum(params int[] nums)
{
    int total = 0;
    foreach (int n in nums)
        total += n;
    return total;
}

static void MinMax(int[] values, out int min, out int max)
{
    min = values[0];
    max = values[0];
    foreach (int v in values)
    {
        if (v < min) min = v;
        if (v > max) max = v;
    }
}
Output
Sum: 15
Min: 1, Max: 9
Explain it simply

What is the difference between a parameter and an argument? Use a recipe as your example.

Reveal a plain-language answer

A recipe says “add 2 cups of flour”. The word “flour” in the recipe is the parameter, a named slot for an ingredient. The actual bag of flour you pour in today is the argument. The recipe (the method) is written once with parameters, and each time you cook (call it), you supply real arguments.

10. Classes & OOP

Object-oriented programming (OOP) organises a program around objects: bundles of data together with the operations that make sense on that data. A class is the blueprint, and an object (or instance) is a thing built from it with new. One Person class can produce any number of person objects, each holding its own name and age.

A class contains members. Fields hold the object's data. Properties are the public face of that data: they look like fields to the caller (p.Name) but are backed by get and set accessors, so the class can control access, for example making a value readable by everyone but changeable only from inside. Methods define behaviour. A constructor is a special method with the same name as the class, run automatically by new to put the object into a valid starting state.

The key idea behind all of this is encapsulation: an object keeps its internal details private and exposes only a safe, deliberate set of operations. Access modifiers enforce it. public members can be used anywhere, while private members are visible only inside the class. A bank account shouldn't let outside code set its balance to any number; it should expose Deposit and Withdraw, which can check the rules first.

Class / object
The blueprint / a concrete instance created from it with new.
Property
A member that looks like a field but is read and written through accessors.
Encapsulation
Hiding an object's internals behind a controlled public interface.
public class Person
{
    // Properties (preferred over public fields)
    public string Name  { get; set; }
    public int    Age   { get; set; }
    private string _secret = "hidden";

    // Constructor
    public Person(string name, int age)
    {
        Name = name;
        Age  = age;
    }

    // Method
    public string Describe() =>
        $"{Name} is {Age} years old.";

    // Override ToString
    public override string ToString() => Name;
}

// Instantiate
var p = new Person("Alice", 30);
Console.WriteLine(p.Describe());   // Alice is 30 years old.

// Object initialiser syntax
var p2 = new Person("Bob", 25) { Age = 26 };
ModifierAccess
publicAccessible from anywhere
privateOnly within the same class
protectedClass and derived classes
internalSame assembly only
Exercise — your turn

Finish the BankAccount class. Balance can be read by anyone but set only inside the class, and Withdraw must refuse (return false) if the money isn't there.

using System;

var acct = new BankAccount("Ada");
acct.Deposit(100);
Console.WriteLine(acct);
Console.WriteLine($"Withdraw 250? {acct.Withdraw(250)}");
Console.WriteLine($"Withdraw 40? {acct.Withdraw(40)}");
Console.WriteLine(acct);

public class BankAccount
{
    public string Owner { get; }
    public decimal Balance { get; private set; }

    public BankAccount(string owner)
    {
        // TODO: store the owner
    }

    public void Deposit(decimal amount) => Balance += amount;

    public bool Withdraw(decimal amount)
    {
        // TODO: refuse if amount > Balance, otherwise subtract and return true
    }

    public override string ToString() => $"{Owner}: {Balance}";
}
Show solution
using System;

var acct = new BankAccount("Ada");
acct.Deposit(100);
Console.WriteLine(acct);
Console.WriteLine($"Withdraw 250? {acct.Withdraw(250)}");
Console.WriteLine($"Withdraw 40? {acct.Withdraw(40)}");
Console.WriteLine(acct);

public class BankAccount
{
    public string Owner { get; }
    public decimal Balance { get; private set; }

    public BankAccount(string owner)
    {
        Owner = owner;
    }

    public void Deposit(decimal amount) => Balance += amount;

    public bool Withdraw(decimal amount)
    {
        if (amount > Balance)
            return false;
        Balance -= amount;
        return true;
    }

    public override string ToString() => $"{Owner}: {Balance}";
}
Output
Ada: 100
Withdraw 250? False
Withdraw 40? True
Ada: 60
Explain it simply

Why make Balance's setter private? Explain it as if the account were a real bank.

Reveal a plain-language answer

At a real bank you can't walk behind the counter and write a new number in the ledger. You hand over cash or ask for a withdrawal, and the teller checks the rules. A private setter is the counter: outside code can see the balance, but the only way to change it is through Deposit and Withdraw, which enforce the rules, such as no overdrafts.

11. Interfaces & Inheritance

Inheritance lets a new class build on an existing one. The new derived class (written class Dog : Animal) automatically receives all the members of its base class and can add its own. This models an “is-a” relationship: a dog is an animal. A base-class method marked virtual may be replaced in a derived class with override, so each kind of animal can speak in its own way. A C# class can inherit from only one base class.

An interface is a contract. It lists members, such as methods and properties, that a class promises to provide, without saying how. An interface's name conventionally starts with I, as in IShape. A class declares that it implements an interface (class Circle : IShape) and must then supply every member, or it will not compile. Unlike base classes, a class may implement many interfaces, which describes a “can-do” relationship: a class can be comparable, disposable, and printable all at once.

Both features enable polymorphism, which means writing code once that works with many different types. A method that accepts an IShape can compute the area of a circle, a rectangle, or a shape invented next year, and at run time the correct version of Area() is chosen automatically for each object. Programming against interfaces rather than concrete classes keeps code flexible and easy to test.

Base / derived class
The class being inherited from / the class that inherits.
virtual / override
Marks a method that may be replaced / supplies the replacement.
Polymorphism
One piece of code working with objects of many types through a shared contract.

Inheritance

public class Animal
{
    public string Name { get; set; }

    public virtual string Speak() => "...";
}

public class Dog : Animal
{
    public override string Speak() => $"{Name} says: Woof!";
}

var dog = new Dog { Name = "Rex" };
Console.WriteLine(dog.Speak());    // Rex says: Woof!

Interfaces

public interface IShape
{
    double Area();
    double Perimeter();
}

public class Circle : IShape
{
    public double Radius { get; set; }
    public double Area()      => Math.PI * Radius * Radius;
    public double Perimeter() => 2 * Math.PI * Radius;
}
A class can inherit from only one base class, but it can implement multiple interfaces.
Exercise — your turn

Circle already implements IShape. Write Rectangle so it does too; the loop at the top then works for both without changes.

using System;
using System.Collections.Generic;

var shapes = new List<IShape> { new Circle(1), new Rectangle(3, 4) };
foreach (IShape s in shapes)
    Console.WriteLine($"{s.Name}: {s.Area():F2}");

public interface IShape
{
    string Name { get; }
    double Area();
}

public class Circle : IShape
{
    private readonly double _radius;
    public Circle(double radius) => _radius = radius;
    public string Name => "Circle";
    public double Area() => Math.PI * _radius * _radius;
}

// TODO: public class Rectangle : IShape, with a (width, height) constructor
Show solution
using System;
using System.Collections.Generic;

var shapes = new List<IShape> { new Circle(1), new Rectangle(3, 4) };
foreach (IShape s in shapes)
    Console.WriteLine($"{s.Name}: {s.Area():F2}");

public interface IShape
{
    string Name { get; }
    double Area();
}

public class Circle : IShape
{
    private readonly double _radius;
    public Circle(double radius) => _radius = radius;
    public string Name => "Circle";
    public double Area() => Math.PI * _radius * _radius;
}

public class Rectangle : IShape
{
    private readonly double _width, _height;
    public Rectangle(double width, double height)
    {
        _width = width;
        _height = height;
    }
    public string Name => "Rectangle";
    public double Area() => _width * _height;
}
Output
Circle: 3.14
Rectangle: 12.00
Explain it simply

What's the difference between “inherits from” and “implements”? Use a job analogy.

Reveal a plain-language answer

Inheriting is like joining the family business: you get everything your parent built (the tools, the customers, the habits) and you can change a few things. You can only have one parent business. Implementing an interface is like earning a certification: it's a promise that you can do certain things, such as drive a forklift, and you can hold as many certifications as you like. The employer (your code) only cares that you hold the certificate, not who your family is.

12. Exception Handling

Even correct programs meet situations they cannot handle in the normal way: a file that doesn't exist, text that isn't a number, a network that drops. In C#, such a problem is reported by throwing an exception, an object that describes what went wrong. Throwing an exception immediately stops the current code and passes control back up through the chain of method calls until something catches it. If nothing does, the program crashes with an error message.

You handle exceptions with a try block followed by one or more catch blocks. The code that might fail goes in try. Each catch names an exception type it can deal with, and C# runs the first one that matches, so list specific types before general ones such as Exception. An optional finally block runs whether or not an exception occurred, which makes it the place for cleanup such as closing files. (The using statement does that cleanup automatically for most resources.)

Your own code can signal problems with throw, as in throw new ArgumentException("Age cannot be negative."), choosing an exception type that describes the fault. Exceptions are for exceptional situations. When failure is expected and ordinary, such as a user typing a non-number, prefer a check such as int.TryParse, which returns false instead of throwing. Catch only what you can actually handle, and never silently swallow exceptions you don't understand.

Exception
An object representing an error, thrown to interrupt normal flow.
try / catch / finally
Risky code / handling for specific failures / cleanup that always runs.
Propagation
An uncaught exception travelling up the call chain to the next handler.
try
{
    int[] arr = { 1, 2, 3 };
    Console.WriteLine(arr[10]);   // throws IndexOutOfRangeException
}
catch (IndexOutOfRangeException ex)
{
    Console.WriteLine($"Index error: {ex.Message}");
}
catch (Exception ex)              // catch-all
{
    Console.WriteLine($"Error: {ex.Message}");
}
finally
{
    Console.WriteLine("Always runs — good for cleanup.");
}

// Throw your own exception
if (age < 0)
    throw new ArgumentException("Age cannot be negative.");
Only catch exceptions you can meaningfully handle. Let unexpected exceptions propagate rather than swallowing them silently.
Exercise — your turn

Write ParseAge: throw a FormatException if the text isn't a number, an ArgumentException if it's negative, and otherwise return the age. The try/catch/finally is already written.

using System;

string[] inputs = { "42", "-5", "abc" };
int checkedCount = 0;

foreach (string input in inputs)
{
    try
    {
        Console.WriteLine($"Age: {ParseAge(input)}");
    }
    catch (FormatException ex)
    {
        Console.WriteLine($"Not a number: {ex.Message}");
    }
    catch (ArgumentException ex)
    {
        Console.WriteLine($"Invalid: {ex.Message}");
    }
    finally
    {
        checkedCount++;
    }
}
Console.WriteLine($"Checked {checkedCount} inputs.");

static int ParseAge(string text)
{
    // TODO: use int.TryParse; throw FormatException($"'{text}'") if it fails,
    //       throw ArgumentException("Age cannot be negative.") if age < 0
}
Show solution
using System;

string[] inputs = { "42", "-5", "abc" };
int checkedCount = 0;

foreach (string input in inputs)
{
    try
    {
        Console.WriteLine($"Age: {ParseAge(input)}");
    }
    catch (FormatException ex)
    {
        Console.WriteLine($"Not a number: {ex.Message}");
    }
    catch (ArgumentException ex)
    {
        Console.WriteLine($"Invalid: {ex.Message}");
    }
    finally
    {
        checkedCount++;
    }
}
Console.WriteLine($"Checked {checkedCount} inputs.");

static int ParseAge(string text)
{
    if (!int.TryParse(text, out int age))
        throw new FormatException($"'{text}'");
    if (age < 0)
        throw new ArgumentException("Age cannot be negative.");
    return age;
}
Output
Age: 42
Invalid: Age cannot be negative.
Not a number: 'abc'
Checked 3 inputs.
Explain it simply

Explain throw and catch using a kitchen with a head chef.

Reveal a plain-language answer

A line cook finds the fish has gone bad. They can't fix it, so they don't quietly serve it; they shout the problem up the line (throw). Whoever is able to deal with it, maybe the head chef who can swap the dish, catches the problem and decides what to do. If nobody takes responsibility, service stops (the program crashes). And whatever happens, someone still wipes down the station at the end: that's finally.