Skip to main content

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

PillarPurposeC# Mechanism
EncapsulationHide internal state, expose a controlled APIAccess modifiers, properties
InheritanceReuse and extend existing typesclass Derived : Base, interfaces
PolymorphismTreat derived types as their base typevirtual/override, interfaces
AbstractionModel real-world concepts, hide complexityabstract 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 LevelDeclared InAccessible InLifetime
Block{ } (if, loop, etc.)Only within that blockUntil block exits
MethodMethod bodyOnly within that methodUntil method returns
InstanceClass/struct (no static)Any instance method in the classLifetime of the object
StaticClass/struct (static)All instances of the classLifetime 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
}
}
Variable shadowing

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
}
When to use this

Use 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 (new is 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;
}
}
Featureconstreadonly
When assignedCompile timeRuntime (constructor or declaration)
Implicitly staticYesNo (can be instance or static)
Allowed typesPrimitives, strings, enums, nullAny type
Can use newNoYes
Changing valueBinary-breaking change (inlined)Safe across assemblies
Fields can be modifiedNeverNo (after constructor)
Prefer readonly for public values

const 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);
}
}
Prefer IDisposable over destructors

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
}
Seal by default

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 LevelEnclosing Class Can AccessNested Class Can Access Enclosing
private nestedYesAll members of enclosing (including private)
protected nestedYesAll members of enclosing
public nestedYesOnly 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 out parameters
  • Can be static, virtual, or async
  • If no implementation exists, the compiler removes all calls to it
  • Without an access modifier (legacy), must return void and cannot have out parameters

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; }
}
Rules for Accessor Modifiers
  • 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 public property can have private set, but a private property cannot have public 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

ModifierSame ClassDerived ClassSame AssemblyExternal Assembly
publicYesYesYesYes
privateYesNoNoNo
protectedYesYesNoNo
internalYesNoYesNo
protected internalYesYesYesNo
private protectedYesYes (same assembly)NoNo

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 override

new 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

RuleDetails
No private virtual/overrideVirtual methods are meant to be exposed to derived classes; overridden methods cannot be more restrictive
Signatures must matchReturn type, name, and parameters must be identical
Only override virtual/abstractAttempting to override a non-virtual method is a compile error
Without override, you are hidingIf you forget override, the method hides the base — use new to acknowledge
Overridden methods are virtualAn 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.

Abstract class rules
  • 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 abstract purely 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

FeatureAbstract ClassInterface
ConstructorsYesNo
FieldsYesNo (static only)
Default implementationYesYes (C# 8+, DIM)
Multiple inheritanceNo (single)Yes
Access modifiersAnypublic (implicitly)
When to use"Is-a" with shared logic"Can-do" capability contract
Composition Over Inheritance

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 virtual methods in constructors

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 earlysealed prevents test doubles (mocking). Seal only when you have a clear reason.
  • Using new for method hiding — it breaks polymorphism. Use override instead 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 sealed by 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.

References