Lesson 7 • Intermediate Track
Object-Oriented Programming
By the end of this lesson you'll be able to design your own types — bundling data and behaviour into classes, building objects from them, initialising them with constructors, and protecting their data with encapsulation. This is the way real C# programs are organised.
What You'll Learn
- Tell the difference between a class (blueprint) and an object (instance)
- Create classes with fields and methods, and build objects with new
- Write constructors that initialise objects in one line — no invalid state
- Use this to refer to the current object inside its own methods
- Apply encapsulation: private fields, public methods, and access modifiers
- Use properties (auto, validated, computed) and tell static from instance members
💡 Real-World Analogy
A class is like an architectural blueprint for a house. It defines what rooms exist, where the doors go, and how the plumbing works — but it's not a house you can live in. An object is an actual house built from that blueprint. You can build hundreds of houses from one blueprint, each with its own paint colour (field values), yet they all share the same structure (methods and properties). A constructor is the building crew that sets each house up the day it's finished. And encapsulation is the walls and the wiring behind them: you flick the light switches (public methods), but you can't grab the live wires directly (private fields).
Understanding OOP in C#
Object-Oriented Programming (OOP) is a way of organising code around objects — units that bundle data (what something is) and behaviour (what something does). Instead of one long script, you model your program as a set of cooperating objects, much like the real world.
C# is built around OOP — nearly everything lives inside a class. There are four classic pillars:
- 🔒 Encapsulation — hide internal data, expose controlled access (this lesson)
- 🧬 Inheritance — build specialised classes from general ones (next lesson)
- 🎭 Polymorphism — the same method name behaving differently per object
- 🎨 Abstraction — describe "what" without the "how" (interfaces & abstract classes)
This lesson covers the foundations and the first pillar — encapsulation — alongside classes, objects, constructors, properties, and static members. These are the building blocks of every C# project from here on.
📊 Access Modifiers Quick Reference
| Modifier | Who can access it | Use when |
|---|---|---|
| public | Anywhere | Methods and properties other code needs to call |
| private | Only inside the same class | Internal data and helpers (the default for fields) |
| protected | Same class + derived classes | Data subclasses need but outside code shouldn't touch |
| internal | Same assembly (project) | Shared within a project but hidden from outside consumers |
If you don't write a modifier on a class member, it defaults to private. Get into the habit of being explicit — it makes your intent obvious to the next reader.
1. Classes and Objects
A class is the blueprint — it describes the fields (the data each object holds) and the methods (what each object can do). An object is a specific thing built from that blueprint with its own values; you create one with the new keyword. The class is written once; you can build as many objects from it as you like, and each keeps its own data. Read this worked example, run it, then you'll finish a class yourself.
Worked example: a Car class & two objects
Read every comment, run it, and notice each object keeps its own field values.
using System;
// A class is a BLUEPRINT — it describes what every Car will have and do.
// It is not a car you can drive; it's the design on paper.
class Car
{
// Fields = the data each Car object holds.
public string Brand;
public string Model;
public int Year;
// A method = something the object can DO.
public void StartEngine()
{
Console.WriteLine($"{Brand} {Model} ({Year}) — engine started! 🚗");
}
}
class Program
{
static void Main()
{
...Your turn. The Rectangle class below is almost complete — it just needs a second field and a one-line method body. Fill in the two ___ blanks, then run it.
🎯 Your turn: complete the Rectangle class
Add the Height field and finish Area(), then check the area is 12.
using System;
// 🎯 YOUR TURN — finish the Rectangle class, then run it.
class Rectangle
{
// 1) Width is done for you. Add a public int field called Height below it.
public int Width;
public int ___; // 👉 name this field Height
// 2) Area() should return Width times Height.
public int Area()
{
return ___; // 👉 multiply the two fields: Width * Height
}
}
class Program
{
static void Main()
{
Rectangle r = new Rectang
...2. Constructors & this
Setting every field by hand after new is tedious and easy to forget — leaving an object half-built. A constructor fixes that: it's a special method with the same name as the class that runs automatically when you write new ClassName(...), so you create and initialise the object in one line. Inside it, this means "the object being built right now" — handy when a parameter has the same name as a field (this.Brand = brand;).
Worked example: a constructor for Car
One line now builds the object and fills its data — no half-built state.
using System;
class Car
{
public string Brand;
public string Model;
public int Year;
// A CONSTRUCTOR runs automatically when you write 'new Car(...)'.
// It has the same name as the class and no return type.
// 'this.Brand' = THIS object's Brand; 'brand' = the value passed in.
public Car(string brand, string model, int year)
{
this.Brand = brand; // 'this' makes it clear which is which
this.Model = model;
this.Year = year;
}
...Now you try. Finish the Dog constructor so it stores the name, then create a dog and make it bark. Fill in the three ___ blanks:
🎯 Your turn: add a constructor, then use it
Complete the constructor, build a Dog, and call its method.
using System;
class Dog
{
public string Name;
public string Breed;
// 🎯 YOUR TURN — finish the constructor and the Main method.
// 1) Set this object's fields from the values passed in.
public Dog(string name, string breed)
{
this.Name = ___; // 👉 assign the 'name' parameter to this.Name
this.Breed = breed; // (this one is done for you)
}
public void Bark()
{
Console.WriteLine($"{Name} the {Breed} says Woof! 🐶");
}
}
...3. Encapsulation — Protecting Data
Encapsulation means hiding a class's data behind a controlled gate. You mark fields private so outside code can't touch them directly, and you offer public methods as the only way in. Those methods can enforce the rules — a bank account can refuse a negative deposit or an overdraft. The payoff is twofold: invalid data is impossible, and you can change how the class works inside without breaking any code that uses it.
Worked example: a BankAccount with private data
See how public methods guard the private balance and reject bad input.
using System;
class BankAccount
{
// 'private' means: only code INSIDE this class can touch balance.
// Outside code cannot read or change it directly — that's the point.
private double balance;
public string Owner { get; } // read-only from outside
public BankAccount(string owner, double initialBalance)
{
Owner = owner;
balance = initialBalance;
}
// Public methods are the ONLY way in — so they can enforce the rules.
public void Deposit(d
...4. Properties — Fields vs Properties
A field is raw stored data; a property looks like a field from outside but runs code when read or written, which lets you add validation without changing how callers use it. An auto-property — public string Name { get; set; } — is the everyday default and the compiler creates the hidden storage for you. For checks, use a full property with a private backing field; for values derived on the fly, use a computed property with =>; and leave out set to make a property read-only.
Worked example: auto, validated & computed properties
Compare a plain auto-property with a validating setter and a computed one.
using System;
class Student
{
// Auto-property — the compiler creates the hidden storage for you.
// This is the everyday default when no validation is needed.
public string Name { get; set; }
// Full property — a 'backing field' lets you validate the value.
private int age;
public int Age
{
get => age; // returning the stored value
set // 'value' is whatever was assigned
{
if (value < 0 |
...🔎 Deep Dive: why properties instead of public fields?
It's tempting to just write public string name; and be done. The trouble is the day you need a rule — "names can't be empty" — you'd have to add validation, which means turning the field into a property, which changes its type of member and can break code (and reflection/serialisation) that relied on it being a field.
A property future-proofs you: it already looks like obj.Name = "Al"; to callers, but you can slip validation into the setter later without changing a single line of calling code.
public string Name { get; set; } // start simple
public string Name // add a rule later — callers unchanged
{
get => _name;
set => _name = string.IsNullOrWhiteSpace(value)
? throw new ArgumentException("Name required")
: value;
}Convention: properties are PascalCase (FirstName), private backing fields are camelCase or underscore-prefixed (firstName or _firstName).
5. Static vs Instance Members
Instance members belong to each object — every car has its own brand. Static members belong to the class itself and are shared by every object — perfect for things like "how many cars exist in total." You access an instance member through an object (car1.Brand) and a static member through the class name (Counter.GetTotalCount()). If a value should be the same for every object, make it static; if each object needs its own copy, keep it instance.
Worked example: static (shared) vs instance (per-object)
Watch one shared counter rise as separate objects are created.
using System;
class Counter
{
// STATIC field — ONE copy shared by every Counter object that exists.
private static int totalCount = 0;
// INSTANCE field — every object gets its OWN Label.
public string Label { get; }
public Counter(string label)
{
Label = label;
totalCount++; // bumped each time ANY object is made
}
// STATIC method — called on the CLASS, not on an object.
public static int GetTotalCount() => totalCount;
...Putting It Together: a Playlist Class
Here's a small but real class that uses everything from this lesson at once — a constructor, a read-only property, a private field guarded by a public method, a computed property, and a static counter shared across every playlist. You understand every line now.
Worked example: a Playlist class
Add tracks, check if a playlist is empty, and watch the shared count grow.
using System;
// === A Playlist class — uses everything from this lesson at once ===
class Playlist
{
public string Name { get; } // read-only property, set in constructor
private int trackCount; // private field — controlled access only
public static int PlaylistsMade { get; private set; } // static, shared count
public Playlist(string name)
{
Name = name;
trackCount = 0;
PlaylistsMade++; // every new playlist bumps t
...Notice PlaylistsMade is static with a private set — outside code can read the total but only the class itself can change it. That's encapsulation applied to a static member.
Pro Tips
- 💡 Default to private fields, public properties: this is the standard C# convention. Don't expose a field as public unless you have a strong reason.
- 💡 Use auto-properties for simple data:
public string Name { get; set; }is cleaner than a full backing field when there's no validation to do. - 💡 Prefer init-only setters for immutable data: in C# 9+,
public string Id { get; init; }allows setting only during object creation. - 💡 Initialise objects in the constructor so they're never in a half-built, invalid state — that's the whole reason constructors exist.
- 💡 Reach for records for plain data carriers:
record Person(string Name, int Age);auto-generates equality,ToString, and deconstruction.
Common Errors (and the fix)
- "CS0246: The type or namespace name 'X' could not be found": the class you're using doesn't exist or isn't in scope — usually a misspelt class name or a missing
usingdirective. Check the spelling and add the rightusingat the top. - "System.NullReferenceException: Object reference not set to an instance of an object": you used an object that was never created with
new, so it'snull.Car c; c.StartEngine();crashes — writeCar c = new Car(...);first. - Forgetting
new: declaringCar c;only makes a variable that points at nothing. You must actually build the object:c = new Car(...);before you can use it. - "CS0122: 'BankAccount.balance' is inaccessible due to its protection level": you tried to read or set a
privatemember from outside the class. Go through a public method or property instead — that's encapsulation doing its job. - "CS1061: 'Car' does not contain a definition for 'StartEngine'" when calling on the class: you wrote
Car.StartEngine()on an instance method. Call it on an object (car1.StartEngine()), or make the memberstaticif it truly belongs to the class.
📋 Quick Reference
| Task | Code | Notes |
|---|---|---|
| Define a class | class Car { ... } | The blueprint |
| Create an object | var c = new Car(...); | new builds an instance |
| Constructor | public Car(string b) { ... } | Same name as class |
| This object | this.Brand = brand; | Current instance |
| Auto-property | public string Name { get; set; } | Storage created for you |
| Read-only property | public int Id { get; } | Set in constructor only |
| Computed property | public bool IsAdult => Age >= 18; | No backing field |
| Static member | Counter.GetTotalCount() | On the class, shared |
Frequently Asked Questions
Q: What's the actual difference between a class and an object?
A class is the design — written once, it lists the fields and methods. An object is a real thing built from that design with new, holding its own values. Blueprint vs house: one blueprint, many houses.
Q: When should I use a field versus a property?
Use a private field for internal storage, and expose data through a property rather than a public field. Properties look like fields to callers but let you add validation or change the implementation later without breaking anyone.
Q: Why did I get a NullReferenceException?
You used an object variable that was never assigned with new, so it points at null. Create the object first (var c = new Car(...);) before calling its methods or reading its data.
Q: When should something be static instead of instance?
Make it static when the value or behaviour belongs to the class as a whole and is shared by every object — like a running count of how many objects exist, or a utility method that doesn't need any object's data. If each object needs its own copy, keep it instance.
Mini-Challenge: Build a BankAccount
No blanks this time — just a brief and an outline to keep you on track. Build a BankAccount class with a private balance, a constructor, Deposit and Withdraw methods that enforce the rules, and a way to read the balance — then exercise it in Main. Run it and check your output against the expected line in the comments.
🎯 Mini-Challenge: a BankAccount class
Write the class and use it; the final balance should be £120.00.
using System;
// 🎯 MINI-CHALLENGE: Build a BankAccount class
// 1. Give it a private double field called 'balance'.
// 2. Add a constructor that takes a starting balance and stores it.
// 3. Add Deposit(double amount): add amount to balance (ignore amounts <= 0).
// 4. Add Withdraw(double amount): subtract it ONLY if amount <= balance,
// otherwise print "Insufficient funds".
// 5. Add a Balance property (or GetBalance() method) to read the balance.
//
// In Main: make an account with 100,
...🎉 Lesson Complete
- ✅ A class is a blueprint; an object is an instance built with
new - ✅ Fields hold data, methods do work — a class bundles both together
- ✅ Constructors initialise an object in one line;
thismeans the current object - ✅ Encapsulation = private fields + public methods/properties for controlled access
- ✅ Properties (auto, validated, computed) are the C# way to expose data safely
- ✅ Static members belong to the class and are shared; instance members are per-object
- ✅ Access modifiers:
public,private,protected,internal - ✅ Next lesson: Inheritance & Polymorphism — extending classes and overriding behaviour
Sign up for free to track which lessons you've completed and get learning reminders.