C# Interview Questions
Language Fundamentals
Q1: What is the difference between readonly and const?
Both create immutable values, but they differ in when the value is set and what types they accept:
| 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 |
public class Settings
{
// const — value baked in at compile time, implicitly static
public const int MaxRetries = 3;
public const string AppName = "MyApp";
// readonly — value set at runtime
public readonly DateTime CreatedAt;
public readonly string ConnectionString;
public Settings(string connectionString)
{
ConnectionString = connectionString;
CreatedAt = DateTime.UtcNow;
}
}
readonly for public valuesconst values are inlined at compile time — every referencing assembly 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.
Q2: What is the sealed keyword used for?
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 CustomConnStr : ConnectionString { } // Compile error
// Sealed method — cannot be overridden further down the chain
public class BaseRenderer
{
protected virtual void RenderHeader() { }
}
public class HtmlRenderer : BaseRenderer
{
protected sealed override void RenderHeader()
{
// This override is final — no further override allowed
}
}
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.
Q3: Name all the access modifiers for types
| 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 |
Top-level types (not nested) can only be public or internal. The default is internal.
Q4: What is the difference between an interface and an abstract class?
| Feature | Abstract Class | Interface |
|---|---|---|
| Constructors | Yes | No |
| Fields / state | Yes | No (static fields only, C# 8+) |
| Default implementation | Yes | Yes (C# 8+, via DIM) |
| Multiple inheritance | No (single class inheritance) | Yes (multiple interfaces) |
| Access modifiers | Any | public (implicitly) |
| When to use | "Is-a" with shared logic | "Can-do" capability contract |
// Abstract class — shared state and logic
public abstract class Vehicle
{
public string Model { get; set; } // state
public void Start() => Console.WriteLine("Engine started"); // concrete method
public abstract double CalculateFuelEfficiency(); // must override
}
// Interface — pure contract
public interface IWriter
{
void WriteFile();
// C# 8+ default implementation
void Log() => Console.WriteLine("Writing...");
}
Since C# 8, interfaces can have default implementations, but they still cannot hold instance state (no instance fields).
Q5: When is a static constructor called?
A static constructor runs once per AppDomain, lazily — before the first instance is created or any static member is accessed. It is never called directly.
public class Configuration
{
public static int InstanceCount { get; private set; }
static Configuration()
{
InstanceCount = 0;
Console.WriteLine("Static ctor ran");
}
public Configuration() => InstanceCount++;
}
var c1 = new Configuration(); // "Static ctor ran" printed here
var c2 = new Configuration(); // No output — static ctor already ran
Rules:
- No access modifiers, no parameters
- Runs only once per AppDomain
- Called lazily (before first use)
- Thread-safe — the CLR guarantees only one thread executes it
Q6: How to create an extension method?
Extension methods add new methods to existing types without modifying them. They must be defined in a static class and the first parameter uses the this keyword:
public static class StringExtensions
{
public static bool IsNullOrEmpty(this string str)
=> string.IsNullOrEmpty(str);
public static string Truncate(this string str, int maxLength)
=> str.Length <= maxLength ? str : str[..maxLength];
}
// Usage — appears as instance method
string name = "CommitToMemory";
var shortName = name.Truncate(6); // "Commit"
bool empty = "".IsNullOrEmpty(); // true
Rules:
- Must be in a
staticclass at namespace level - First parameter must have
thismodifier - Cannot access private members of the extended type
- Instance methods always win over extension methods with the same signature
Q7: Does C# support multiple class inheritance?
No. C# supports only single class inheritance — a class can have exactly one direct base class. However, a class can implement multiple interfaces:
public class Base { }
public interface IReader { void Read(); }
public interface IWriter { void Write(); }
// Single class + multiple interfaces
public class Document : Base, IReader, IWriter
{
public void Read() { }
public void Write() { }
}
The reason: multiple class inheritance leads to the diamond problem — ambiguity when two base classes define the same member. Interfaces avoid this because they carry no implementation state (prior to C# 8 default methods, which are resolved differently).
Q8: Explain boxing and unboxing
Boxing converts a value type to a reference type (stored on the heap). Unboxing extracts the value type from the object.
int number = 42;
// Boxing — value type → reference type (heap allocation)
object boxed = number;
// Unboxing — reference type → value type (explicit cast)
int unboxed = (int)boxed;
// Hidden boxing in everyday code
ArrayList list = new(); // stores object — boxes value types
list.Add(42); // boxing!
int val = (int)list[0]; // unboxing
// Generic collections avoid boxing entirely
List<int> genericList = new();
genericList.Add(42); // no boxing — stored as int
Boxing allocates on the heap and causes GC pressure. Avoid it by using generic collections (List<T> over ArrayList) and generic interfaces (IEnumerable<T> over IEnumerable).