OOP in C#
Classes, records for immutable data, interfaces, inheritance, and properties with get/set and auto-properties.
Classes and constructors
public class Car
{
private string model;
private int speed;
public Car(string model)
{
this.model = model;
this.speed = 0;
}
public void Accelerate(int amount)
{
speed += amount;
}
public string Describe()
{
return $"{model} is going {speed} km/h";
}
}
var car = new Car("Civic");
car.Accelerate(40);
Console.WriteLine(car.Describe()); // Civic is going 40 km/h
Properties: get/set and auto-properties
C# properties look like fields from the outside but let you control read/write access with get/set accessors — the idiomatic replacement for manually writing GetX()/SetX() methods:
public class BankAccount
{
private decimal balance;
public decimal Balance
{
get { return balance; }
set
{
if (value < 0) throw new ArgumentException("Balance cannot be negative");
balance = value;
}
}
}
When there's no extra logic, an auto-property generates the backing field for you:
public class Product
{
public string Name { get; set; }
public decimal Price { get; set; }
// init-only setter (C# 9+) — settable only during object initialization, then immutable
public string Sku { get; init; }
}
var product = new Product { Name = "Keyboard", Price = 49.99m, Sku = "KB-100" };
// product.Sku = "KB-200"; // Error: init-only property, can't be set after construction
Records — immutable data made easy
A record (C# 9+) is designed for immutable data and gives you value-based equality, a readable ToString(), and a concise with expression for creating modified copies — all generated automatically, none of which a regular class gives you for free:
public record Point(int X, int Y);
var p1 = new Point(3, 4);
var p2 = new Point(3, 4);
Console.WriteLine(p1 == p2); // True — records compare by value, not reference
Console.WriteLine(p1); // Point { X = 3, Y = 4 } — auto-generated ToString()
var p3 = p1 with { Y = 10 }; // creates a new record, copying p1 but overriding Y
Console.WriteLine(p3); // Point { X = 3, Y = 10 }
Console.WriteLine(p1); // Point { X = 3, Y = 4 } — p1 itself is unchanged
Compare this to a regular class, where == compares references by default, and you'd have to hand-write Equals, GetHashCode, and ToString() to get the same behavior:
public class ClassPoint
{
public int X { get; set; }
public int Y { get; set; }
}
var cp1 = new ClassPoint { X = 3, Y = 4 };
var cp2 = new ClassPoint { X = 3, Y = 4 };
Console.WriteLine(cp1 == cp2); // False — reference equality, different objects
Interfaces
public interface IPayable
{
decimal CalculatePay();
}
public class Employee : IPayable
{
public decimal HoursWorked { get; init; }
public decimal HourlyRate { get; init; }
public decimal CalculatePay() => HoursWorked * HourlyRate;
}
IPayable employee = new Employee { HoursWorked = 40, HourlyRate = 25m };
Console.WriteLine(employee.CalculatePay()); // 1000
By convention, interface names in C# start with I (IPayable, IDisposable, IEnumerable). A class can implement any number of interfaces, but inherit from only one base class.
Inheritance
public class Vehicle
{
public int Speed { get; protected set; }
public virtual void Accelerate()
{
Speed += 10;
}
}
public class SportsCar : Vehicle
{
public override void Accelerate()
{
Speed += 30; // sports cars accelerate faster
}
}
Vehicle v = new SportsCar();
v.Accelerate();
Console.WriteLine(v.Speed); // 30 — the overridden method ran (polymorphism)
A method must be marked virtual in the base class before a subclass can override it — unlike Java, where every non-final method is overridable by default. This is a deliberate C# design choice: overridability must be explicitly opted into.
Common mistakes
- Using a regular
classwith hand-rolledEquals/GetHashCodewhen arecordwould give you correct value equality for free — reach forrecordby default for immutable data models (DTOs, value objects). - Forgetting
virtualon a base class method, then being confused whyoverridein the subclass produces a compiler error. - Confusing
init(settable only at construction, then locked) withreadonly(settable in the constructor body, then locked) — both produce immutability, butinitalso works cleanly with object-initializer syntax (new Product { Sku = "..." }).
Interview questions
Q: What's the practical difference between a record and a class?
A record gets value-based equality (== and .Equals() compare property values, not references), an auto-generated readable ToString(), and support for non-destructive with copies — all generated by the compiler. A class defaults to reference equality and requires you to hand-write all of that yourself if you want the same behavior. Records are the idiomatic choice for immutable data models; classes remain the default for objects with identity and mutable behavior.
Q: Why does C# require the virtual keyword before a method can be overridden?
It's a deliberate design decision for clarity and performance — a method is non-overridable (and can be safely inlined/devirtualized by the JIT) unless the author explicitly opts in with virtual, forcing every override relationship in a codebase to be intentional and visible at the base class, rather than implicit like in some other object-oriented languages.