Just added: Algorithms you can run and practice
Updated Jun 22, 2026 Edit on GitHub

Classes & Objects - The Spine of C#

You've been writing methods inside a class since Phase 1, even if nobody made a fuss about it. Every C# program lives inside a class - there's no "outside." Classes and objects aren't a corner of C#; they are C#. Almost everything you'll touch - a list, a file, an HTTP client, a button - is an object built from a class.

The word "OOP" arrives wrapped in scary vocabulary: encapsulation, polymorphism, abstraction. Set that aside - there's one idea underneath, and once it lands the keywords stop being spells: a class bundles data together with the behavior that acts on that data, and new stamps out individual copies you can use.

The mental model: blueprint vs. instance

📝 Class - a blueprint describing what kind of thing exists: what data it holds and what it can do. Object (also called an instance) - one concrete thing built from that blueprint with new. The class is the cookie cutter; the objects are the cookies.

A class Account doesn't hold any money - it's the idea of an account. Write new Account(...) and you get a real account, with its own balance, separate from every other account you make.

💡 Key point. The class is written once, at design time. Objects are created over and over, at run time. One blueprint, many independent instances - each remembering its own state.

Fields, constructors, and this

A class needs somewhere to keep its data (fields), a way to set that data up when an object is born (a constructor), and a word for "the specific object I'm working on" (this). Let's build Account for real.

📝 Field - a variable that lives on the object, holding its data. Constructor - a special method that runs automatically when you write new, whose job is to set up the new object's starting data. It has no return type and shares the class's name. this - a reference to the current object, the one a method was called on.

class Account
{
    private decimal balance;   // a field: data stored on each Account
    public string Owner;       // another field

    // Constructor: runs when you write new Account(...)
    public Account(string owner, decimal opening)
    {
        this.Owner = owner;        // this.Owner = the field; owner = the parameter
        this.balance = opening;
    }

    public void Deposit(decimal amount)
    {
        this.balance += amount;    // change THIS account's balance
    }

    public decimal Balance()
    {
        return this.balance;
    }
}

class Program
{
    static void Main()
    {
        Account ada = new Account("Ada", 100m);   // build one instance
        ada.Deposit(50m);
        Console.WriteLine($"{ada.Owner}: {ada.Balance()}");
    }
}
Ada: 150

What just happened: new Account("Ada", 100m) ran the constructor, copying the two arguments into the new object's fields. ada is now one instance carrying its own Owner and balance. ada.Deposit(50m) changed Ada's balance specifically - this.balance means "the balance of the account Deposit was called on." A second account's balance would be untouched - that separateness is the whole point of instances.

Notice this.Owner = owner: the field is Owner, the parameter is owner - same word, different case. this. makes it unambiguous: left side is the object's field, right the parameter. You'll see this constantly in constructors.

💡 Object initializer syntax. C# gives a shortcut for setting public members right after construction - instead of (or alongside) constructor arguments, write the assignments in braces:

Account ada = new Account("Ada", 0m)
{
    Owner = "Ada Lovelace"   // set a public member inline, after the constructor runs
};

What just happened: The constructor ran first (with "Ada" and 0m), then the braces reassigned Owner to "Ada Lovelace". Object initializers are pure convenience - they run after the constructor and can only touch members the caller is allowed to set. They read nicely for an object with several settable fields.

Properties - C#'s signature feature

Here's where C# parts ways with most languages you've seen. You might make a field public and let callers read and write it directly. That works, but it's a trap: the day you need to validate a value, compute it, or log when it changes, a raw public field gives you no hook - you'd have to change every caller.

Java solved this with getName() / setName(...) methods everywhere - verbose, and the caller has to know whether it's reading a field or calling a getter. C# solved it differently with one of its defining features: the property.

📝 Property - a member that looks like a field from the outside (account.Owner) but runs code underneath. It has a get accessor (runs on read) and/or a set accessor (runs on assignment, with the value arriving as the keyword value). Callers can't tell a property from a field - that's the point.

The simplest form is an auto-property, where the compiler generates the hidden backing field:

class Account
{
    public string Owner { get; set; }      // auto-property: read and write
    public string Bank  { get; init; }     // init-only: settable once, at creation
    public decimal Balance { get; private set; }  // public read, private write

    public Account(string owner)
    {
        Owner = owner;
        Balance = 0m;
    }
}

What just happened: Owner { get; set; } is a full read/write property in one line - the compiler wired both accessors to an invisible backing field. Bank { get; init; } is init-only: settable in the constructor or an object initializer, then frozen. Balance { get; private set; } lets anyone read the balance but only code inside this class change it - callers can't set it to a million.

When you need logic, spell the accessors out - here's a guarded setter that refuses bad data:

class Account
{
    private decimal balance;   // the backing field, hidden

    public decimal Balance
    {
        get { return balance; }
        set
        {
            if (value < 0)
                throw new ArgumentException("Balance cannot be negative.");
            balance = value;    // 'value' is the incoming assigned amount
        }
    }
}

class Program
{
    static void Main()
    {
        var acc = new Account();
        acc.Balance = 200m;            // calls the set accessor; value = 200
        Console.WriteLine(acc.Balance); // calls the get accessor
        acc.Balance = -5m;             // set accessor throws
    }
}
200
Unhandled exception. System.ArgumentException: Balance cannot be negative.

What just happened: acc.Balance = 200m looked like a plain assignment but ran the set block, value holding 200. Since 200 passed the check, it landed in the hidden balance field. Reading acc.Balance ran the get block. The illegal acc.Balance = -5m failed the value < 0 check and threw - the property guarded its own data, with no change to the caller's code.

You can also make a computed, read-only property - only a get calculating its value from other state:

public string Summary => $"{Owner} has {Balance:C}";   // expression-bodied, get-only

What just happened: Summary has no backing field. Every read runs the => expression and builds a fresh string from Owner and Balance. It's read-only because there's nothing to assign to - this is how you expose derived information without storing it.

💡 Why this matters. Properties are the idiomatic C# way to expose an object's state - not raw public fields, not Java-style getX/setX. They give a field's clean syntax and a method's power to validate, compute, and control access. Your reflex should be public string Name { get; set; }, not public string Name;. Reach for the full accessor form only when you need logic.

Encapsulation & access modifiers

The guarded setter above hinted at the bigger idea: encapsulation - keeping an object's internal data private and exposing only a controlled surface. The reason isn't tidiness; uncontrolled state is where bugs breed. If any code anywhere can set balance to anything, any code anywhere can corrupt it. Make balance private with a guarded property, and there's exactly one place a bad balance can come from.

C# controls visibility with access modifiers - the four you'll use constantly:

📝 public - visible everywhere. private - visible only inside the same class (the default for class members if you write nothing). protected - visible inside this class and its subclasses (matters once you hit inheritance, Phase 6). internal - visible anywhere in the same project/assembly, but not to outside code that references your library.

The discipline: make state private, expose it through public properties and methods.

class Thermostat
{
    private double celsius;   // private state - nobody touches this directly

    public double Celsius
    {
        get => celsius;
        set
        {
            if (value < -273.15)
                throw new ArgumentException("Below absolute zero.");
            celsius = value;
        }
    }

    // A public, read-only view derived from the private state
    public double Fahrenheit => celsius * 9 / 5 + 32;
}

class Program
{
    static void Main()
    {
        var t = new Thermostat();
        t.Celsius = 20;
        Console.WriteLine($"{t.Celsius}C = {t.Fahrenheit}F");
    }
}
20C = 68F

What just happened: celsius is private, so no outside code can poke an impossible temperature into it. The only way in is Celsius's setter, which rejects anything below absolute zero. Fahrenheit is a read-only computed view of that private value. The class decides exactly what the world can do to it - set a valid Celsius, read either scale, nothing else. That controlled surface is encapsulation, keeping an object trustworthy as your program grows.

⚠️ Don't reflexively make everything public. A class with all-public fields is a bag of variables with no defenses - it can't stop bad data or change its internals later without breaking callers. Start private, open up only what callers genuinely need.

static members, and overriding ToString

So far every field and method has belonged to an instance - each Account has its own balance. But sometimes a member belongs to the class itself. That's what static means.

📝 Instance member - belongs to each object; you need one to use it (ada.Deposit(...)). static member - belongs to the class as a whole, used through the class name (Account.Count), existing even with zero objects made.

This is why your entry point is static void Main(): when the program starts, no objects exist yet, so Main can't belong to an instance - it has to belong to the class itself.

class Account
{
    public static int Count = 0;   // shared across ALL accounts
    public string Owner { get; }

    public Account(string owner)
    {
        Owner = owner;
        Count++;                   // bump the shared counter on every new account
    }
}

class Program
{
    static void Main()
    {
        new Account("Ada");
        new Account("Grace");
        Console.WriteLine(Account.Count);   // through the class, not an instance
    }
}
2

What just happened: Count is static, so there's exactly one shared by every Account. Each constructor incremented that shared counter, so after two accounts Account.Count is 2. You read it through the class name, never through an object - static members don't belong to any instance.

The other thing nearly every class should do is teach itself how to print. By default, printing an object gives its type name - useless. Overriding ToString fixes that. Every C# object inherits a ToString() method (from the universal base type object, Phase 6), which you can replace with something meaningful:

class Account
{
    public string Owner { get; }
    public decimal Balance { get; }

    public Account(string owner, decimal balance)
    {
        Owner = owner;
        Balance = balance;
    }

    public override string ToString()      // replace the default, useless version
    {
        return $"Account({Owner}: {Balance:C})";
    }
}

class Program
{
    static void Main()
    {
        var ada = new Account("Ada", 150m);
        Console.WriteLine(ada);            // Console.WriteLine calls ToString() for you
    }
}
Account(Ada: $150.00)

What just happened: Console.WriteLine(ada) automatically called ada.ToString(). Because we overrode it (override tells the compiler we're deliberately replacing the inherited version), it returned our friendly string instead of the default "Account". Overriding ToString makes objects readable in logs, debuggers, and quick Console.WriteLines.

⚠️ One gotcha to bank for later: == doesn't mean what you'd expect for classes. For a class (a reference type), == and .Equals() compare whether two variables point at the same object in memory - not whether they hold the same data. Two separate Account objects with identical owner and balance are == only if literally the same object:

var a = new Account("Ada", 150m);
var b = new Account("Ada", 150m);
Console.WriteLine(a == b);   // False - two different objects, despite identical data
Console.WriteLine(a == a);   // True  - same object
False
True

What just happened: a and b describe the same account on paper, but they're two distinct objects at different spots in memory, so == says False. This is reference equality, the default for all classes (a few built-in types like string override it to compare contents, which is why string == behaved the way you'd expect back in Phase 2, but your own classes get reference equality by default) - it trips people constantly. Records flip this to compare by value automatically (Phase 13); the full gotcha, including how to override equality yourself, is in Phase 9. For now: == on class objects asks "same object?", not "same contents?"

Recap

  1. A class is a blueprint written once; an object/instance is one concrete thing built from it with new, carrying its own data. One blueprint, many independent instances.
  2. Fields hold an object's data; the constructor sets that data up when new runs; this means "the current object," and disambiguates a field from a same-named parameter.
  3. Properties are C#'s signature feature: they look like fields but run code (get/set, with value as the incoming assignment). Auto-properties ({ get; set; }), init-only setters, private set, and computed get-only properties are the idiomatic way to expose state - not raw public fields.
  4. Encapsulation means keeping state private and exposing a controlled surface via properties/methods; access modifiers (public, private, protected, internal) set visibility. Private-by-default prevents whole classes of bugs.
  5. static members belong to the class, not any instance (which is why Main is static); override ToString so your objects print meaningfully.
  6. ⚠️ For classes, == compares object identity, not contents - two objects with identical data are not equal unless they're the same object. (Records fix this; full story in Phase 9.)

You can now design your own types - the building blocks of every C# program. Next, we connect classes together: how one class can build on another, and how interfaces let unrelated classes promise the same behavior.

Quick check

Test yourself on the ideas that define C# classes - properties and reference equality:

[
  {
    "q": "What makes a C# property different from a plain public field?",
    "choices": [
      "A property looks like a field to callers but can run code (validation, computation) in its get/set accessors",
      "A property is always faster than a field because the compiler optimizes it",
      "A property can only be read, never written",
      "There is no real difference; 'property' is just another word for 'field'"
    ],
    "answer": 0,
    "explain": "A property exposes a field-like syntax (account.Owner) while running code underneath. Its get and set accessors let you validate, compute, log, or restrict access - without callers ever knowing it isn't a raw field. That's why C# uses properties instead of public fields or Java-style getX/setX."
  },
  {
    "q": "You write two separate accounts with identical data and compare them with ==. What do you get, and why?",
    "choices": [
      "False - for a class, == compares object identity (same object in memory), not contents",
      "True - == always compares the data inside two objects",
      "A compile error, because you can't use == on classes",
      "True, but only if both objects were created in the same method"
    ],
    "answer": 0,
    "explain": "Classes are reference types, so == (and the default .Equals) ask 'are these the same object?', not 'do they hold the same data?'. Two distinct objects with identical fields are not ==. Records change this to value comparison (Phase 13); the full gotcha is in Phase 9."
  },
  {
    "q": "Why is a program's entry point declared `static void Main()`?",
    "choices": [
      "Because when the program starts no objects exist yet, so Main must belong to the class itself rather than an instance",
      "Because static methods run faster than instance methods",
      "Because Main is not allowed to use the 'this' keyword for security reasons",
      "Because static is required on every method that returns void"
    ],
    "answer": 0,
    "explain": "A static member belongs to the class as a whole, not to any object. At startup there are no instances yet, so Main can't be an instance method - there'd be no object to call it on. It has to belong to the class itself, which is exactly what static means."
  }
]

Before the quiz: without looking back, say (or jot down) the core idea of this phase in your own words.

Check your understanding 3 questions

1. What makes a C# property different from a plain public field?

2. You write two separate accounts with identical data and compare them with ==. What do you get, and why?

3. Why is a program's entry point declared `static void Main()`?