A quick-reference and hands-on guide to the fundamentals of C# programming.
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:
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.
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.
dotnet command-line tools..cs extensionnamespace and at least one classMain method;dotnet run or the csc compilerTell a friend, in two sentences and without the words “compile” or “runtime”, what happens between writing a C# file and seeing it run.
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.
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.
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!");
}
}
}
.cs file.// Top-level statement style (C# 9+)
Console.WriteLine("Hello, World!");
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#!
}
}
}
using System;
namespace MyApp
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello, C#!");
}
}
}
Hello, C#!
Explain to a friend why a C# program needs a Main method. What would go wrong without one?
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.
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.
var) and its name.? 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;
| Type | Size | Range / Use |
|---|---|---|
int | 4 bytes | –2,147,483,648 to 2,147,483,647 |
long | 8 bytes | Very large whole numbers |
double | 8 bytes | ~15–17 significant digits |
decimal | 16 bytes | 28–29 significant digits — use for money |
bool | 1 byte | true or false |
char | 2 bytes | Single Unicode character |
string | variable | Immutable sequence of characters |
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}");
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}");
Ada is 36 years old and 1.65 m tall. Admin: False Total: 59.97
Why would you store a price as decimal rather than double? Explain it without the word “precision”.
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.
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.
7 / 2 is 3.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
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-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
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 */}");
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"}");
Full boxes: 3 Left over: 2 Hello, friend
Explain what ?? does using a real-life example, like a spare key.
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.
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).
$"…" string using braces.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 ""
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)");
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)");
Ada Lovelace (12 chars)
If strings can't be changed, how does name.ToUpper() work? Explain it like you're describing a photocopier.
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.
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.
List<T>, that you fill in with a concrete type.int[] nums = { 10, 20, 30 };
nums[0]; // 10
nums.Length; // 3
int[] sized = new int[5]; // fixed size, all zeros
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
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);
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"]}");
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"]}");
Scores: 75, 88, 92, 95 the = 2, cat = 1
When would you pick a Dictionary over a List? Give an everyday example of each.
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.
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.
bool expression that decides which branch runs.switch, such as a constant, a type, or >= 90.cond ? a : b, an expression that chooses between two values.int score = 75;
if (score >= 90)
Console.WriteLine("A");
else if (score >= 75)
Console.WriteLine("B");
else
Console.WriteLine("C or below");
string grade = score switch
{
>= 90 => "A",
>= 75 => "B",
>= 60 => "C",
_ => "F", // default (_)
};
string status = age >= 18 ? "adult" : "minor";
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"
};
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",
};
A B D F 7 is odd
In a switch expression, why does the order of the arms matter? What would happen if >= 60 came first?
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.
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.
i that changes each iteration.< vs <=.for (int i = 0; i < 5; i++)
Console.WriteLine(i);
// 0 1 2 3 4
var fruits = new List<string> { "apple", "banana", "cherry" };
foreach (string fruit in fruits)
Console.WriteLine(fruit);
int i = 0;
while (i < 5)
Console.WriteLine(i++);
// do-while always runs at least once
do
{
Console.WriteLine(i);
i--;
} while (i > 0);
break to exit a loop, continue to skip to the next iteration.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));
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));
1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz
Explain the difference between while and do … while using a restaurant.
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.
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.
// 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
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
}
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;
}
}
Sum: 15 Min: 1, Max: 9
What is the difference between a parameter and an argument? Use a recipe as your example.
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.
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.
new.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 };
| Modifier | Access |
|---|---|
public | Accessible from anywhere |
private | Only within the same class |
protected | Class and derived classes |
internal | Same assembly only |
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}";
}
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}";
}
Ada: 100 Withdraw 250? False Withdraw 40? True Ada: 60
Why make Balance's setter private? Explain it as if the account were a real bank.
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.
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.
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!
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;
}
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
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;
}
Circle: 3.14 Rectangle: 12.00
What's the difference between “inherits from” and “implements”? Use a job analogy.
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.
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.
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.");
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
}
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;
}
Age: 42 Invalid: Age cannot be negative. Not a number: 'abc' Checked 3 inputs.
Explain throw and catch using a kitchen with a head chef.
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.