Classes & Objects

C# · Lesson 15 of 31 7 min read Updated

Until now you have used types that .NET provides: int, string, List<T>. Real programs need their own types: a BankAccount, a Student, a TrainTicket. A class is a blueprint that describes what data such a thing holds and what it can do. An object is one actual thing built from that blueprint, such as Aarav's account or Priya's account.

This is the heart of object-oriented programming (OOP). In this lesson you will write classes with fields, constructors and methods, create objects, protect data with private, share data with static, and use C# 12's shorter primary constructors.

A first class

Here is a simple bank account. The class is declared after the top-level statements that use it.

C#
BankAccount aarav = new BankAccount("Aarav Mehta", 5000m);
BankAccount priya = new("Priya Nair", 1200m);

aarav.Deposit(2500m);
bool ok = priya.Withdraw(2000m);

Console.WriteLine(aarav.Describe());
Console.WriteLine(priya.Describe());
Console.WriteLine($"Priya's withdrawal worked: {ok}");

class BankAccount
{
    private readonly string _owner;
    private decimal _balance;

    public BankAccount(string owner, decimal openingBalance)
    {
        _owner = owner;
        _balance = openingBalance;
    }

    public void Deposit(decimal amount)
    {
        if (amount <= 0)
        {
            throw new ArgumentException("Deposit must be positive.");
        }
        _balance += amount;
    }

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

    public string Describe() => $"{_owner}: Rs {_balance}";
}
Output
Aarav Mehta: Rs 7500
Priya Nair: Rs 1200
Priya's withdrawal worked: False

The parts of the class:

  • Fields (_owner, _balance) store each object's data. Every object has its own copy: Aarav's balance and Priya's balance are separate.
  • The constructor BankAccount(...) has the same name as the class and no return type. It runs once when an object is created and sets up the fields.
  • Methods (Deposit, Withdraw, Describe) are the things an object can do. Inside them, _balance means "this object's balance".
  • new BankAccount(...) creates an object. When the type is already written on the left, the shorter target-typed new(...) works too.
  • readonly means the field can only be set in the constructor; an owner's name never changes after opening.

Encapsulation: public and private

Access modifiers decide who can use a member. public members can be used from anywhere. private members can only be used inside the class. Fields are almost always private, and the class offers public methods that keep the data valid. This idea is called encapsulation.

If code outside the class tries to change the balance directly, it does not compile:

C#
aarav._balance = 1_000_000m;
Output
error CS0122: 'BankAccount._balance' is inaccessible due to its protection level

That error is your friend. The only way to change a balance is through Deposit and Withdraw, which refuse negative deposits and overdrafts. If a bug ever creates a wrong balance, you know exactly which two methods to check. When you leave out the modifier, class members are private by default.

Static members and ToString

Most members belong to an object, so each object has its own copy. A static member belongs to the class itself and is shared by all objects. Use it for things like a counter of how many objects exist.

Every class also inherits a ToString method, which Console.WriteLine and interpolation call automatically. By default it returns just the type name. Overriding it gives your objects a readable text form.

C#
Student a = new("Ananya");
Student b = new("Farhan");

Console.WriteLine(a);
Console.WriteLine(b);
Console.WriteLine($"Students admitted: {Student.Count}");

class Student
{
    private static int s_count;

    private readonly string _name;
    private readonly int _rollNo;

    public Student(string name)
    {
        s_count++;
        _name = name;
        _rollNo = 100 + s_count;
    }

    public static int Count => s_count;

    public override string ToString() => $"Roll {_rollNo}: {_name}";
}
Output
Roll 101: Ananya
Roll 102: Farhan
Students admitted: 2

The s_ prefix is a common convention for private static fields, just as _ marks private instance fields. You call a static member on the class name, Student.Count, not on an object. Count here is a read-only property; properties are the subject of the next lesson. Calling an instance member from a static context without an object gives error CS0120: An object reference is required for the non-static field, method, or property.

Objects are reference types

A class is a reference type. A variable of a class type holds a reference to an object, not the object itself. Copying the variable copies the reference, so two variables can point to the same object.

C#
BankAccount kabir = new("Kabir Singh", 3000m);
BankAccount sameAccount = kabir;

sameAccount.Deposit(500m);
Console.WriteLine(kabir.Describe());

BankAccount? notOpened = null;
Console.WriteLine(notOpened is null);
Output
Kabir Singh: Rs 3500
True

The deposit made through sameAccount shows up in kabir, because there is only one account object. A reference can also be null, meaning it points to no object at all. Calling a method on a null reference throws NullReferenceException, the most common crash in C# programs; Nullable Types & Null Safety shows how the compiler helps you avoid it.

Primary constructors

C#
Wallet wallet = new("Rohan");
wallet.Add(150m);
wallet.Add(75.50m);
Console.WriteLine(wallet);

class Wallet(string owner)
{
    private decimal _balance;

    public void Add(decimal amount) => _balance += amount;

    public override string ToString() => $"{owner}'s wallet: Rs {_balance}";
}
Output
Rohan's wallet: Rs 225.50

owner is captured by the class and used in ToString. Primary constructor parameters are not fields with a fixed readonly guarantee, so for important data that must never change, assign them to a readonly field or property, as in the BankAccount example.

Common mistakes

  • Using an object that was never created. BankAccount acc = null; acc.Deposit(100m); throws NullReferenceException. Create it with new.
  • Making fields public. Anyone can then put the object into an invalid state. Keep fields private and expose methods or properties.
  • Confusing static and instance members. A static method has no this object, so it cannot read instance fields directly.
  • Writing a return type on a constructor. public void BankAccount(...) is a normal method, not a constructor.
  • Expecting b = a to copy an object. It copies the reference; both names point to the same object.

Key points

  • A class is a blueprint; new creates objects (instances) from it.
  • Fields hold each object's data; methods define behaviour; a constructor sets up a new object.
  • Keep fields private and expose safe public methods: this is encapsulation.
  • static members belong to the class and are shared by every object.
  • Override ToString to give objects a readable text form.
  • Classes are reference types; C# 12 primary constructors shorten simple classes.

Exercise

Create a class TrainTicket with private fields for passenger name, train number and fare (decimal). Add a constructor, a method ApplyConcession(int percent) that reduces the fare (reject percentages outside 0 to 75 by returning false), and override ToString. Also keep a static count of tickets issued. Create tickets for Isha (train 12015, Rs 1240) and Rohan (train 12951, Rs 3150), give Isha a 25% concession, try a 90% concession for Rohan, and print both tickets and the count.

Show solution
C#
TrainTicket isha = new("Isha", 12015, 1240m);
TrainTicket rohan = new("Rohan", 12951, 3150m);

Console.WriteLine(isha.ApplyConcession(25));
Console.WriteLine(rohan.ApplyConcession(90));
Console.WriteLine(isha);
Console.WriteLine(rohan);
Console.WriteLine($"Tickets issued: {TrainTicket.Issued}");

class TrainTicket
{
    private static int s_issued;

    private readonly string _passenger;
    private readonly int _trainNo;
    private decimal _fare;

    public TrainTicket(string passenger, int trainNo, decimal fare)
    {
        _passenger = passenger;
        _trainNo = trainNo;
        _fare = fare;
        s_issued++;
    }

    public static int Issued => s_issued;

    public bool ApplyConcession(int percent)
    {
        if (percent < 0 || percent > 75)
        {
            return false;
        }
        _fare -= _fare * percent / 100;
        return true;
    }

    public override string ToString() => $"{_passenger}, train {_trainNo}, fare Rs {_fare:F2}";
}
Output
True
False
Isha, train 12015, fare Rs 930.00
Rohan, train 12951, fare Rs 3150.00
Tickets issued: 2

The concession is calculated with decimal arithmetic: 1240 x 25 / 100 = 310, so Isha pays 930. Rohan's 90% request is rejected, so his fare is unchanged.

Quick Quiz

1. What is the relationship between a class and an object?

2. What is the default access level of a class member with no modifier?

3. How do you call a static method Count on the class Student?

4. After var a = new BankAccount("A", 100m); var b = a; b.Deposit(50m);, what is a's balance?

Learn C# with a mentor

Free lessons get you started. Our Programming & Software Development training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call