Object-Oriented Programming
Definition
Object-Oriented Programming (OOP) is a programming paradigm that organizes software around objects — data structures that contain both state (fields, properties) and behavior (methods). C# is a fundamentally object-oriented language where every type derives from System.Object.
Core Concepts
The Four Pillars
| Pillar | Purpose | C# Mechanism |
|---|---|---|
| Encapsulation | Hide internal state, expose a controlled API | Access modifiers, properties |
| Inheritance | Reuse and extend existing types | class Derived : Base, interfaces |
| Polymorphism | Treat derived types as their base type | virtual/override, interfaces |
| Abstraction | Model real-world concepts, hide complexity | abstract classes, interfaces |
Encapsulation
Bundling data with the methods that operate on it, while restricting direct access to internal state.
public class BankAccount
{
private decimal _balance; // private field — hidden state
public decimal Balance => _balance; // read-only exposure
public void Deposit(decimal amount)
{
if (amount <= 0) throw new ArgumentException("Amount must be positive");
_balance += amount;
}
}
Inheritance
A derived class inherits members from a base class, promoting code reuse.
public class Animal
{
public virtual string Speak() => "...";
}
public class Dog : Animal
{
public override string Speak() => "Woof!";
}
Polymorphism
The same operation behaves differently based on the runtime type.
Animal[] animals = { new Dog(), new Cat() };
foreach (var animal in animals)
Console.WriteLine(animal.Speak()); // "Woof!", "Meow!"
Abstraction
Hiding complex implementation details and exposing only what consumers need.
public abstract class Shape
{
public abstract double Area(); // no implementation — forced override
}
Variable Scope
Variables have different lifetimes and visibility depending on where they are declared:
| Scope Level | Declared In | Accessible In | Lifetime |
|---|---|---|---|
| Block | { } (if, loop, etc.) | Only within that block | Until block exits |
| Method | Method body | Only within that method | Until method returns |
| Instance | Class/struct (no static) | Any instance method in the class | Lifetime of the object |
| Static | Class/struct (static) | All instances of the class | Lifetime of the application |
public class ScopeDemo
{
private int _instanceField = 10; // Instance scope
private static int _staticField = 20; // Static (class) scope
public void Method()
{
int localVar = 30; // Method scope
if (localVar > 0)
{
int blockVar = 40; // Block scope
Console.WriteLine(blockVar); // Accessible
}
// Console.WriteLine(blockVar); // Error — out of scope
for (int i = 0; i < 5; i++)
{
// i is block-scoped to the for loop
}
// Console.WriteLine(i); // Error — out of scope
}
}
C# does not allow declaring a local variable with the same name as another local variable in an enclosing scope. However, a local variable can have the same name as a field — in that case, use this to disambiguate:
private string name = "field";
public void SetName(string name)
{
this.name = name; // 'this.name' = field, 'name' = parameter
}
The this Keyword
this refers to the current instance of the class. It is used to:
public class Employee
{
private string name;
private decimal salary;
// 1. Disambiguate field from parameter
public Employee(string name, decimal salary)
{
this.name = name;
this.salary = salary;
}
// 2. Pass the current instance to another method
public void Register()
{
EmployeeDatabase.Add(this);
}
// 3. Chain constructor calls (constructor chaining)
public Employee() : this("Unknown", 0m) { }
// 4. Indexer declaration
public object this[int index] => properties[index];
// 5. Extension method target — 'this' inside extension methods
}
thisUse this when parameter names shadow field names (common convention), for constructor chaining, and when passing the current instance. Avoid redundant this. for unambiguous field access — it adds noise.
The static Keyword
static means a member belongs to the type itself, not to any instance:
public class Configuration
{
// Static field — shared across all instances
public static int InstanceCount { get; private set; }
// Static constructor — runs once, before any instance is created or static member is accessed
static Configuration()
{
InstanceCount = 0;
}
// Instance constructor — runs per instantiation
public Configuration()
{
InstanceCount++;
}
// Static method — called on the type
public static void Reset() => InstanceCount = 0;
// Static class — cannot be instantiated, all members must be static
public static class Defaults
{
public const int MaxRetries = 3;
public static readonly TimeSpan Timeout = TimeSpan.FromSeconds(30);
}
}
// Usage
var config1 = new Configuration();
var config2 = new Configuration();
Console.WriteLine(Configuration.InstanceCount); // 2
Configuration.Reset();
Console.WriteLine(Configuration.InstanceCount); // 0
Static constructor rules:
- No access modifiers, no parameters
- Runs only once per AppDomain
- Called lazily — before first instance creation or any static member access
- Cannot be called directly
Static class rules:
- Cannot be instantiated (
newis a compile error) - All members must be static
- Cannot be used as a field, parameter, or return type
- Commonly used for utility/helper methods (e.g.,
Math,Console,Enumerable)
const vs readonly
Both create immutable values, but they differ in when the value is set and what types they accept:
public class Settings
{
// const — value set at compile time, implicitly static
public const int MaxRetries = 3;
public const string AppName = "CommitToMemory";
// readonly — value set at runtime (constructor or declaration)
public readonly DateTime CreatedAt;
public readonly string ConnectionString;
public Settings(string connectionString)
{
ConnectionString = connectionString;
CreatedAt = DateTime.UtcNow;
}
}
| Feature | const | readonly |
|---|---|---|
| When assigned | Compile time | Runtime (constructor or declaration) |
| Implicitly static | Yes | No (can be instance or static) |
| Allowed types | Primitives, strings, enums, null | Any type |
Can use new | No | Yes |
| Changing value | Binary-breaking change (inlined) | Safe across assemblies |
| Fields can be modified | Never | No (after constructor) |
readonly for public valuesconst values are inlined at compile time — every assembly that references the constant gets the literal value baked in. If you change a const, all referencing assemblies must be recompiled. Use public static readonly for values that might change between versions (e.g., configuration defaults, version strings).
Destructors and IDisposable
A destructor (finalizer) is called by the garbage collector before an object is reclaimed. In C#, it uses the ~ prefix syntax:
public class FileHandle
{
private readonly IntPtr _handle;
public FileHandle(string path)
{
_handle = NativeMethods.OpenFile(path);
}
// Destructor — called by GC before object is collected
~FileHandle()
{
NativeMethods.CloseFile(_handle);
}
}
Destructors have significant drawbacks: you don't control when they run (non-deterministic), they keep the object alive for an extra GC generation, and they run on a finalizer thread. Prefer the IDisposable pattern with using statements:
public class FileHandle : IDisposable
{
private IntPtr _handle;
private bool _disposed;
public FileHandle(string path)
{
_handle = NativeMethods.OpenFile(path);
}
public void Dispose()
{
if (_disposed) return;
NativeMethods.CloseFile(_handle);
_handle = IntPtr.Zero;
_disposed = true;
GC.SuppressFinalize(this); // Prevent destructor from running
}
// Destructor as safety net — only if Dispose was not called
~FileHandle() => Dispose();
}
// Deterministic cleanup with using
using var handle = new FileHandle("data.txt");
// Dispose called automatically at end of scope
Constructors (Detailed)
public class Product
{
public string Name { get; set; }
public decimal Price { get; set; }
public string Category { get; set; }
// Default constructor
public Product() : this("Unknown", 0m, "General") { }
// Parameterized constructor
public Product(string name, decimal price) : this(name, price, "General") { }
// Master constructor — all parameters
public Product(string name, decimal price, string category)
{
Name = name;
Price = price;
Category = category;
}
// Copy constructor
public Product(Product other)
{
Name = other.Name;
Price = other.Price;
Category = other.Category;
}
// Static factory method (alternative to constructor overloading)
public static Product FreeTrial() => new Product("Trial", 0m, "Subscription");
}
Constructor chaining — use this(...) to call another constructor in the same class, and base(...) to call a base class constructor:
public class BaseEntity
{
public int Id { get; }
public DateTime CreatedAt { get; }
public BaseEntity(int id)
{
Id = id;
CreatedAt = DateTime.UtcNow;
}
}
public class Order : BaseEntity
{
public string Customer { get; }
// Chaining to base constructor
public Order(int id, string customer) : base(id)
{
Customer = customer;
}
}
Constructor overloading — multiple constructors with different parameter signatures:
public class Rectangle
{
public double Width { get; }
public double Height { get; }
public Rectangle() : this(1, 1) { } // Square 1x1
public Rectangle(double side) : this(side, side) { } // Square
public Rectangle(double width, double height) // Full constructor
{
Width = width;
Height = height;
}
}
Sealed Classes and Methods
sealed prevents further inheritance or overriding:
// Sealed class — cannot be inherited
public sealed class ConnectionString
{
public string Value { get; }
public ConnectionString(string value) => Value = value;
}
// public class CustomConnectionString : ConnectionString { } // Compile error
// Sealed method — cannot be overridden further
public class BaseRenderer
{
protected virtual void RenderHeader() { }
protected virtual void RenderBody() { }
}
public class HtmlRenderer : BaseRenderer
{
protected sealed override void RenderHeader()
{
// This implementation is final — no further override allowed
}
protected override void RenderBody() { } // Can still be overridden
}
public class CustomHtmlRenderer : HtmlRenderer
{
// protected override void RenderHeader() { } // Compile error — sealed
protected override void RenderBody() { } // OK
}
Mark classes sealed unless you intentionally design for inheritance. The JIT compiler can optimize virtual calls on sealed types (devirtualization). Common sealed types: string, Exception, most value types.
Nested Classes
A class declared inside another class. Useful for implementation details, builder patterns, and tightly-coupled helper types:
public class LinkedList<T>
{
private Node? _head;
// Nested class — has access to private members of enclosing class
private class Node
{
public T Data { get; }
public Node? Next { get; set; }
public Node(T data) => Data = data;
}
public void Add(T item)
{
var node = new Node(item); // Can access private nested class
if (_head is null) _head = node;
else { /* append */ }
}
}
// Builder pattern with nested class
public class Query
{
public string Table { get; }
public string WhereClause { get; }
public int? Limit { get; }
private Query(string table, string where, int? limit)
{
Table = table;
WhereClause = where;
Limit = limit;
}
// Nested builder
public class Builder
{
private string _table = "";
private string _where = "";
private int? _limit;
public Builder From(string table) { _table = table; return this; }
public Builder Where(string clause) { _where = clause; return this; }
public Builder Take(int count) { _limit = count; return this; }
public Query Build() => new Query(_table, _where, _limit);
}
}
var query = new Query.Builder()
.From("Users")
.Where("Active = 1")
.Take(50)
.Build();
Nested class access rules:
| Access Level | Enclosing Class Can Access | Nested Class Can Access Enclosing |
|---|---|---|
private nested | Yes | All members of enclosing (including private) |
protected nested | Yes | All members of enclosing |
public nested | Yes | Only public/internal members of enclosing |
Partial Classes and Partial Methods
partial splits a class, struct, interface, or method across multiple files. Commonly used for code generation (designers, source generators):
// File: Order.cs
public partial class Order
{
public int Id { get; set; }
public decimal Total { get; set; }
public decimal CalculateTax() => Total * 0.1m;
// Partial method declaration — no body
partial void OnOrderCreated();
}
// File: Order.Generated.cs (auto-generated)
public partial class Order
{
public void Initialize()
{
OnOrderCreated(); // Safe to call — if no implementation exists, call is removed
}
// Partial method implementation
partial void OnOrderCreated()
{
Console.WriteLine($"Order {Id} created");
}
}
Partial method rules (C# 9+):
- Can have access modifiers, return types, and
outparameters - Can be
static,virtual, orasync - If no implementation exists, the compiler removes all calls to it
- Without an access modifier (legacy), must return
voidand cannot haveoutparameters
Partial properties (C# 13):
// Declaration
public partial string ConnectionString { get; }
// Implementation (often in a generated file)
public partial string ConnectionString => _config.GetConnectionString();
See Modern C# for partial properties and source generator patterns.
Code Examples
Classes and Objects
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
public Person(string name, int age)
{
Name = name;
Age = age;
}
public void Introduce() => Console.WriteLine($"Hi, I'm {Name}, age {Age}.");
}
// Instantiation
var person = new Person("Alice", 30);
person.Introduce();
Constructors
public class Product
{
public string Name { get; set; }
public decimal Price { get; set; }
// Default constructor
public Product() : this("Unknown", 0m) { }
// Parameterized constructor
public Product(string name, decimal price)
{
Name = name;
Price = price;
}
}
Properties
Properties are members that provide flexible access to private fields. They look like public data members but are actually special methods called accessors (get / set).
Auto-Implemented Properties
When no additional logic is needed, the compiler generates a hidden backing field:
public class Employee
{
public string Name { get; set; }
public string Department { get; set; }
}
Full Property with Backing Field
Use when you need validation or computed logic:
public class Employee
{
private int _age;
public int Age
{
get => _age;
set => _age = value >= 0 ? value : throw new ArgumentOutOfRangeException();
}
}
Read-Only and Write-Only Properties
A property with only a get accessor is read-only — common for values set via the constructor and never changed externally:
public class Employee
{
private readonly string _id;
public string Id => _id; // Read-only
public Employee(string id) => _id = id;
}
A property with only a set accessor is write-only (rare, but valid):
public string Password
{
set => _passwordHash = Hash(value); // Write-only — no get
}
Asymmetric Accessor Accessibility
You can restrict individual accessors with a more restrictive modifier:
public class Employee
{
// Public get, private set — readable everywhere, writable only inside the class
public string Name { get; private set; }
// Public get, protected set — writable in derived classes too
public decimal Salary { get; protected set; }
}
- Only one accessor can have a modified access level (the other inherits the property's level).
- The accessor modifier must be more restrictive than the property's modifier (e.g., a
publicproperty can haveprivate set, but aprivateproperty cannot havepublic get).
Modern C# Property Features
public class Employee
{
// Init-only setter (C# 9+) — assignable only during object initialization
public string Department { get; init; }
// Required property (C# 11) — callers must set it
public required string Email { get; set; }
}
Access Modifiers
| Modifier | Same Class | Derived Class | Same Assembly | External Assembly |
|---|---|---|---|---|
public | Yes | Yes | Yes | Yes |
private | Yes | No | No | No |
protected | Yes | Yes | No | No |
internal | Yes | No | Yes | No |
protected internal | Yes | Yes | Yes | No |
private protected | Yes | Yes (same assembly) | No | No |
Inheritance in Depth
A derived class inherits all non-private members from its base class. C# supports single class inheritance — a class can have only one direct base class, but the inheritance chain can be arbitrarily deep.
public class Writer
{
public string FileName { get; set; }
public Writer(string fileName) => FileName = fileName;
public void Write() => Console.WriteLine("Writing to a file");
}
public class XMLWriter : Writer
{
// Call base constructor with :base()
public XMLWriter(string fileName) : base(fileName) { }
public void FormatXMLFile() => Console.WriteLine("Formatting XML file");
}
Base Constructor Chaining
Use :base(...) to pass arguments from the derived constructor to the base constructor. This ensures shared initialization logic runs in the base class:
public class Writer
{
public string FileName { get; set; }
public Writer(string fileName)
{
FileName = fileName;
}
}
public class XMLWriter : Writer
{
public XMLWriter(string fileName) : base(fileName) { }
}
public class JSONWriter : Writer
{
public JSONWriter(string fileName) : base(fileName) { }
}
Upcasting and Downcasting
An inheritance hierarchy lets you reference derived objects through their base type, but with limitations:
XMLWriter xml = new XMLWriter("file.xml");
Writer writer = xml; // Upcasting — implicit, always safe
writer.Write(); // OK — part of Writer class
// writer.FormatXMLFile(); // Error — not part of Writer class
You cannot assign a base object to a derived variable directly, but you can use the as keyword for safe downcasting:
Writer writer = new XMLWriter("file.xml");
// Safe downcasting with 'as' — returns null if cast fails
XMLWriter xml = writer as XMLWriter; // OK — writer was XMLWriter
xml.FormatXMLFile();
// Another safe option: pattern matching
if (writer is XMLWriter xmlWriter)
xmlWriter.FormatXMLFile();
Method Hiding with new
If a derived class declares a method with the same signature as a non-virtual base method, the compiler issues a warning. The new keyword explicitly acknowledges method hiding — the base implementation is concealed, not overridden:
public class Writer
{
public void SetName() => Console.WriteLine("Setting name in the base Writer class");
}
public class XMLWriter : Writer
{
public new void SetName() => Console.WriteLine("Setting name in the XMLWriter class");
}
Writer w = new XMLWriter();
w.SetName(); // "Setting name in the base Writer class" — resolved at compile time
XMLWriter x = new XMLWriter();
x.SetName(); // "Setting name in the XMLWriter class"
new vs overridenew hides the base member and resolves based on the compile-time type. override provides true polymorphic behavior resolved at runtime. Always prefer override for polymorphism — use new only when you intentionally need to hide a member you cannot change.
Virtual and Override (Polymorphism)
A virtual method in a base class can be overridden in a derived class to provide different behavior. The runtime type determines which implementation is called:
public class Writer
{
public virtual void CalculateFileSize()
=> Console.WriteLine("Calculating file size in a Writer class");
}
public class XMLWriter : Writer
{
public override void CalculateFileSize()
=> Console.WriteLine("Calculating file size in the XMLWriter class");
}
Writer w = new XMLWriter();
w.CalculateFileSize(); // "Calculating file size in the XMLWriter class" — runtime dispatch
Call the base implementation from an override using base.:
public override void CalculateFileSize()
{
base.CalculateFileSize(); // Call base implementation first
Console.WriteLine("Additional XML-specific logic");
}
Polymorphic Method Rules
| Rule | Details |
|---|---|
No private virtual/override | Virtual methods are meant to be exposed to derived classes; overridden methods cannot be more restrictive |
| Signatures must match | Return type, name, and parameters must be identical |
Only override virtual/abstract | Attempting to override a non-virtual method is a compile error |
Without override, you are hiding | If you forget override, the method hides the base — use new to acknowledge |
| Overridden methods are virtual | An override can itself be overridden in further derived classes |
Inheritance Keywords Summary
public class Base
{
public virtual void DoWork() => Console.WriteLine("Base work");
}
public class Derived : Base
{
public override void DoWork() => Console.WriteLine("Derived work"); // overrides
public new void DoWorkLegacy() { } // hides base member — avoid if possible
}
public sealed class Final : Derived { } // cannot be inherited
Abstract Classes
An abstract class cannot be instantiated — it exists solely to be inherited from. Use it when multiple derived classes share common logic that should not be duplicated.
- If a class has even one abstract member, the entire class must be marked
abstract(compiler enforces this). - An abstract class does not need to have any abstract members — you can mark a class
abstractpurely to prevent direct instantiation.
public abstract class Vehicle
{
public string Model { get; set; }
// Abstract — no body, must be overridden in derived class
public abstract double CalculateFuelEfficiency();
// Concrete — derived class inherits this as-is
public void Start() => Console.WriteLine("Engine started");
}
Interfaces
An interface defines a contract — a set of members that any implementing class must provide. Unlike class inheritance, a class can implement multiple interfaces.
public interface IWriter
{
void WriteFile();
}
public class XmlWriter : IWriter
{
public void WriteFile() => Console.WriteLine("Writing file in the XmlWriter class.");
}
public class JsonWriter : IWriter
{
public void WriteFile() => Console.WriteLine("Writing file in the JsonWriter class.");
}
Implementing an Interface
When implementing an interface, the implementing class must:
- Use exactly matching method names, return types, and parameters
- Make all implemented methods
public(unless using explicit implementation) - Implement every member defined in the interface
A class can inherit from a base class and implement interfaces at the same time — base class comes first:
public class FileBase
{
public virtual void SetName() => Console.WriteLine("Setting name in base class.");
}
public class XmlWriter : FileBase, IWriter
{
public void WriteFile() => Console.WriteLine("Writing file in the XmlWriter class.");
public override void SetName() => Console.WriteLine("Setting name in the XmlWriter class.");
}
Referencing Classes Through Interfaces
An interface variable can hold a reference to any class that implements it, but you can only access members defined by that interface:
XmlWriter writer = new XmlWriter();
writer.SetName(); // OK — from base class
writer.WriteFile(); // OK — from interface
IWriter iWriter = new XmlWriter();
iWriter.WriteFile(); // OK — part of IWriter
// iWriter.SetName(); // Error — not part of IWriter
Decoupling Classes with Interfaces
Interfaces enable loose coupling — a class depends on an abstraction, not a concrete type. This is the foundation of Dependency Injection:
// Tightly coupled — depends on a specific class
public class XmlFileWriter
{
private readonly XmlWriter _xmlWriter;
public XmlFileWriter(XmlWriter xmlWriter) => _xmlWriter = xmlWriter;
public void Write() => _xmlWriter.WriteFile();
}
// Problem: cannot reuse for JsonWriter, must duplicate the class
// Loosely coupled — depends on an interface
public class FileWriter
{
private readonly IWriter _writer;
public FileWriter(IWriter writer) => _writer = writer;
public void Write() => _writer.WriteFile();
}
// Works with ANY class that implements IWriter
FileWriter fileWriter = new FileWriter(new XmlWriter());
fileWriter.Write(); // "Writing file in the XmlWriter class."
fileWriter = new FileWriter(new JsonWriter());
fileWriter.Write(); // "Writing file in the JsonWriter class."
Multiple Interface Implementation
A class can implement any number of interfaces. It must implement all members from every interface:
public interface IFormatter
{
void FormatFile();
}
public class XmlWriter : FileBase, IWriter, IFormatter
{
public void WriteFile() => Console.WriteLine("Writing file in the XmlWriter class.");
public override void SetName() => Console.WriteLine("Setting name in the XmlWriter class.");
public void FormatFile() => Console.WriteLine("Formatting file in XmlWriter class.");
}
Explicit Interface Implementation
When two interfaces define a method with the same signature, use explicit implementation to provide separate implementations for each. Explicit members are accessed only through the interface, not through the class:
public interface IReader
{
void Process();
}
public interface IWriter
{
void Process();
}
public class DocumentService : IReader, IWriter
{
// Explicit implementation — no access modifier
void IReader.Process() => Console.WriteLine("Reading document");
void IWriter.Process() => Console.WriteLine("Writing document");
// Regular (implicit) implementation — accessible on the class directly
public void Process() => Console.WriteLine("Default processing");
}
// Usage
var service = new DocumentService();
service.Process(); // "Default processing"
IReader reader = service;
reader.Process(); // "Reading document"
IWriter writer = service;
writer.Process(); // "Writing document"
Generic Interface with Default Implementation (C# 8+)
public interface IRepository<T>
{
T GetById(int id);
void Add(T entity);
// Default interface method — provides implementation without requiring override
void Remove(T entity) => Console.WriteLine($"Removing {entity}");
}
public class InMemoryRepository<T> : IRepository<T>
{
public T GetById(int id) => default;
public void Add(T entity) { }
// Remove() is inherited from the interface — no override needed
}
Abstract Class vs Interface
| Feature | Abstract Class | Interface |
|---|---|---|
| Constructors | Yes | No |
| Fields | Yes | No (static only) |
| Default implementation | Yes | Yes (C# 8+, DIM) |
| Multiple inheritance | No (single) | Yes |
| Access modifiers | Any | public (implicitly) |
| When to use | "Is-a" with shared logic | "Can-do" capability contract |
Prefer composing behavior through interfaces and injected services over deep inheritance hierarchies. Inheritance should model true "is-a" relationships. If the relationship is "has-a", use composition.
When to Use
- Use classes for most types — they support inheritance, references, and nullability.
- Use abstract classes when multiple derived types share common state or logic.
- Use interfaces to define capabilities that cross inheritance boundaries.
- Use sealed on classes you do not intend to be inherited — it enables runtime optimizations.
Common Pitfalls
Calling a virtual method from a constructor invokes the override on a derived class before the derived constructor has run. The derived object is only partially initialized, which leads to subtle bugs.
public abstract class Base
{
protected Base() => Initialize(); // DANGEROUS
protected abstract void Initialize();
}
public class Derived : Base
{
private readonly string _name;
public Derived(string name) { _name = name; }
protected override void Initialize() => Console.WriteLine(_name.Length); // NullReferenceException!
}
- Deep inheritance hierarchies — more than 2-3 levels become fragile and hard to reason about. Prefer composition.
- Sealing too early —
sealedprevents test doubles (mocking). Seal only when you have a clear reason. - Using
newfor method hiding — it breaks polymorphism. Useoverrideinstead whenever possible.
Key Takeaways
- OOP in C# centers on classes, objects, and the four pillars: encapsulation, inheritance, polymorphism, and abstraction.
- Use properties over public fields — they give you encapsulation without sacrificing syntax.
- Prefer interfaces for defining contracts; use abstract classes for sharing implementation.
- Composition over inheritance is a guiding principle for maintainable design.
- Mark classes
sealedby default unless you intentionally design for inheritance.
Interview Questions
Q: What is the difference between an abstract class and an interface? A: An abstract class can have constructors, fields, and implementation; supports single inheritance. An interface defines a contract with no state; supports multiple inheritance. Since C# 8, interfaces can have default implementations, but they still cannot hold instance state.
Q: What are the four pillars of OOP? A: Encapsulation (hiding internal state), Inheritance (reusing and extending types), Polymorphism (same operation, different behavior), and Abstraction (modeling essentials, hiding complexity).
Q: What is the difference between override and new?
A: override provides true polymorphic behavior — the derived method is called based on runtime type. new hides the base member and resolves based on compile-time type, breaking polymorphism.
Q: Does C# support multiple inheritance? A: C# supports single class inheritance but multiple interface implementation. A class can inherit from one base class while implementing any number of interfaces.
Q: What is encapsulation? A: Encapsulation is the bundling of data and behavior within a class, restricting direct access to internal state through access modifiers and exposing a controlled public API via methods and properties.
Q: What is the difference between virtual and abstract?
A: A virtual method has a default implementation that derived classes may override. An abstract method has no implementation and derived classes must override it. Only abstract classes can declare abstract members.