C# OOP Introduction
Object-oriented programming, often shortened to OOP, is a way to organize programs around objects: values that contain data and behavior together. In C#, OOP matters because almost every useful .NET program uses classes, objects, methods, properties, and interfaces to model real problems. Instead of writing one long list of instructions, you design small types that collaborate.
Overview: How OOP Works in C#
C# is a strongly typed, object-oriented language running on the Common Language Runtime, or CLR. A class is a blueprint that describes what data an object stores and what operations it can perform. An object is an instance of that class created at runtime, usually with new. If BankAccount is the blueprint, then one customer account object can have a balance of 150, while another account object can have a balance of 900.
OOP is built on four main ideas. Encapsulation keeps an object’s internal state protected and exposes controlled operations through methods and properties. Abstraction means code can use a type through the useful concepts it exposes, without depending on every internal detail. Inheritance lets one class derive from another and reuse or extend behavior. Polymorphism lets code treat different derived objects through a common base type or interface while still running the correct overridden behavior at runtime.
Internally, reference type objects such as class instances live on the managed heap. A variable of a class type usually stores a reference to an object, not the entire object data inline. The CLR tracks those objects and frees unreachable ones with garbage collection. Instance methods receive a hidden reference to the current object, available in code as this. That is why two objects created from the same class can run the same method but read and update different fields.
Syntax
class ClassName
{
private string fieldName;
public ClassName(string value)
{
fieldName = value;
}
public string PropertyName => fieldName;
public void MethodName()
{
Console.WriteLine(fieldName);
}
}
| Part | Purpose |
|---|---|
class ClassName |
Declares a new reference type blueprint. |
private string fieldName |
Stores internal object state that other code cannot access directly. |
public ClassName(...) |
Constructor code that runs when an object is created. |
PropertyName |
Exposes data safely, often with validation or read-only access. |
MethodName |
Defines behavior that belongs to each object. |
Examples
Example 1: A Simple Class and Object
using System;
class Dog
{
private string name;
public Dog(string name)
{
this.name = name;
}
public void Bark()
{
Console.WriteLine($"{name} says woof!");
}
}
class Program
{
static void Main()
{
Dog dog = new Dog("Milo");
dog.Bark();
}
}
Output:
Milo says woof!
The Dog class stores a private field called name. The constructor receives the name when the object is created. The Bark method uses the field belonging to that specific object.
Example 2: Encapsulation With Properties and Methods
using System;
class BankAccount
{
private decimal balance;
public BankAccount(string owner, decimal openingBalance)
{
Owner = owner;
Deposit(openingBalance);
}
public string Owner { get; }
public decimal Balance => balance;
public void Deposit(decimal amount)
{
if (amount <= 0)
{
throw new ArgumentOutOfRangeException(nameof(amount), "Deposit must be positive.");
}
balance += amount;
}
public bool TryWithdraw(decimal amount)
{
if (amount <= 0 || amount > balance)
{
return false;
}
balance -= amount;
return true;
}
}
class Program
{
static void Main()
{
BankAccount account = new BankAccount("Ada", 100m);
account.Deposit(50m);
bool withdrew = account.TryWithdraw(30m);
Console.WriteLine(account.Owner);
Console.WriteLine(account.Balance);
Console.WriteLine(withdrew);
}
}
Output:
Ada
120
True
This class does not let outside code assign the balance directly. All balance changes go through Deposit or TryWithdraw, so the class can reject invalid operations. This is encapsulation: the object protects its invariants, such as “balance cannot be changed by arbitrary assignment.”
Example 3: Inheritance and Polymorphism
using System;
abstract class Notification
{
public Notification(string recipient)
{
Recipient = recipient;
}
public string Recipient { get; }
public abstract string FormatMessage(string message);
}
class EmailNotification : Notification
{
public EmailNotification(string recipient) : base(recipient)
{
}
public override string FormatMessage(string message)
{
return $"Email to {Recipient}: {message}";
}
}
class SmsNotification : Notification
{
public SmsNotification(string recipient) : base(recipient)
{
}
public override string FormatMessage(string message)
{
return $"SMS to {Recipient}: {message}";
}
}
class Program
{
static void Main()
{
Notification[] notifications =
{
new EmailNotification("dev@example.com"),
new SmsNotification("555-0100")
};
foreach (Notification notification in notifications)
{
Console.WriteLine(notification.FormatMessage("Build finished"));
}
}
}
Output:
Email to dev@example.com: Build finished
SMS to 555-0100: Build finished
The array is declared as Notification[], but it contains two different derived types. When FormatMessage is called, the CLR dispatches to the override belonging to the actual runtime object. That is polymorphism.
How It Works Step by Step
- The compiler reads each class declaration and verifies that fields, properties, constructors, and methods are valid for that type.
- When code calls
new BankAccount(...), the CLR allocates memory for the object on the managed heap and initializes its fields to default values. - The constructor runs, assigning properties and setting the starting state. Constructors should leave the object valid and ready to use.
- When an instance method runs, C# passes the current object as the hidden
thisreference. Field access such asbalancemeansthis.balance. - For virtual or abstract members, the CLR uses the runtime type of the object to choose the correct override. A base variable can point at a derived object without losing that object’s real type.
- Later, if no reachable references point to the object, the garbage collector may reclaim its memory.
Common Mistakes
Making State Public
class BadAccount
{
public decimal Balance;
}
This compiles, but it is usually a design mistake. Any code can set Balance to a negative number or skip required business rules. Prefer private fields and controlled methods or properties.
using System;
class GoodAccount
{
private decimal balance;
public decimal Balance => balance;
public void Deposit(decimal amount)
{
if (amount <= 0)
{
throw new ArgumentOutOfRangeException(nameof(amount));
}
balance += amount;
}
}
class Program
{
static void Main()
{
GoodAccount account = new GoodAccount();
account.Deposit(25m);
Console.WriteLine(account.Balance);
}
}
Output:
25
Forgetting to Override Virtual Behavior
class Shape
{
public virtual double Area()
{
return 0;
}
}
class Circle : Shape
{
public double Area(double radius)
{
return Math.PI * radius * radius;
}
}
The Circle method overloads Area instead of overriding it. If code uses a Shape reference, it still calls the base Area(). The corrected version keeps the same signature and uses override.
using System;
abstract class Shape
{
public abstract double Area();
}
class Circle : Shape
{
private readonly double radius;
public Circle(double radius)
{
this.radius = radius;
}
public override double Area()
{
return Math.PI * radius * radius;
}
}
class Program
{
static void Main()
{
Shape shape = new Circle(2);
Console.WriteLine(Math.Round(shape.Area(), 2));
}
}
Output:
12.57
Best Practices
- Keep fields private unless there is a strong reason to expose them.
- Use properties for simple access and methods for actions that change state or perform work.
- Make constructors establish a valid object immediately.
- Prefer composition over inheritance when one object merely uses another instead of being a specialized version of it.
- Use
abstractclasses or interfaces when callers need a common contract for multiple implementations. - Name classes as nouns, methods as verbs, and properties as readable facts about the object.
- Keep classes focused. A class that validates input, talks to a database, formats reports, and sends email probably has too many responsibilities.
Practice Exercises
- Create a
Bookclass withTitle,Author, and a method that returns a display string. - Create a
Temperatureclass that stores Celsius privately and exposes Fahrenheit as a read-only calculated property. - Create an abstract
PaymentMethodclass with aPaymethod, then implement two derived classes with different output messages.
Summary
- OOP organizes C# programs around types that combine state and behavior.
- A class is a blueprint; an object is a runtime instance of that blueprint.
- Encapsulation protects object state by forcing changes through controlled members.
- Inheritance creates specialized classes, while polymorphism lets base-type code call derived behavior.
- The CLR allocates class instances on the managed heap, dispatches virtual calls by runtime type, and later reclaims unreachable objects with garbage collection.
