16 questions found
What is the difference between IEnumerable<T>, ICollection<T>, and IList<T> in the collection interface hierarchy?
Intermediate
IEnumerable<T> only guarantees the ability to ITERATE (foreach); ICollection<T> extends it with Count, Add, Remove, and Contains; IList<T> extends ICollection<T> further with INDEX-based access (this[int index]) and Insert/RemoveAt — each level adds more capability at the cost of requiring more from the underlying implementation.
void ProcessReadOnly(IEnumerable<int> items) { /* can only iterate */ }
void ProcessCountable(ICollection<int> items) { Console.WriteLine(items.Count); }
void ProcessIndexable(IList<int> items) { Console.WriteLine(items[0]); }
Real-world example
Accepting the loosest interface (IEnumerable<T>) a method's parameter genuinely needs, maximizing which concrete collection types can be passed in.
Common follow-ups: Why is it generally considered good practice to accept the LEAST specific interface your method actually requires?
Interfaces & Abstract Classes
How does Dictionary<TKey,TValue> handle hash collisions internally, and why does a poor GetHashCode() implementation degrade its performance?
Advanced
Dictionary<TKey,TValue> uses a hash table with SEPARATE CHAINING (or a similar bucket-based collision resolution) — when multiple keys hash to the same bucket, it falls back to a linear scan WITHIN that bucket using Equals(); a poorly-distributed GetHashCode() (e.g., one that always returns the same value) collapses the theoretical O(1) average lookup into effectively O(n), since every key lands in the same bucket.
public class BadKey {
public override int GetHashCode() => 1; // terrible: every instance collides into the same bucket
}
// A Dictionary<BadKey, T> degrades to O(n) linear-scan performance
Real-world example
Debugging a mysteriously slow Dictionary-based cache traced back to a custom key type with a poorly-implemented GetHashCode().
Common follow-ups: What's the contractual relationship that GetHashCode() and Equals() must maintain for a custom key type to work correctly?
Equality: Equals
GetHashCode & IEquatable
What is the performance and thread-safety difference between ConcurrentDictionary<TKey,TValue> and a regular Dictionary<TKey,TValue> guarded by a manual lock?
Advanced
ConcurrentDictionary uses fine-grained internal locking (per-bucket/segment) allowing MULTIPLE threads to read/write DIFFERENT parts of the dictionary concurrently with less contention, whereas wrapping a plain Dictionary in a single 'lock' serializes ALL access — one thread at a time, even for operations on completely unrelated keys — making ConcurrentDictionary generally faster under real concurrent load, though single-threaded access to a plain Dictionary is still slightly faster due to lower overhead.
var concurrentCache = new ConcurrentDictionary<string, int>();
concurrentCache.AddOrUpdate("visits", 1, (key, oldValue) => oldValue + 1); // thread-safe atomic update
// vs. manual locking around a plain Dictionary:
lock (lockObject) { dictionary["visits"] = dictionary.GetValueOrDefault("visits") + 1; }
Real-world example
Building a thread-safe, high-throughput in-memory cache accessed concurrently by many request-handling threads in a web server.
Common follow-ups: Why might a single 'lock' around a plain Dictionary actually outperform ConcurrentDictionary under LOW contention (few concurrent threads)?
Multithreading & Task Parallel Library
How would you choose between List<T>, LinkedList<T>, and ImmutableList<T> based on their different Big-O characteristics for common operations?
Advanced
List<T> offers O(1) index access and O(1) amortized append, but O(n) insertion/removal in the middle (due to shifting elements); LinkedList<T> offers O(1) insertion/removal ANYWHERE (given a node reference), but O(n) index access (no random access at all); ImmutableList<T> uses a persistent tree structure giving O(log n) for most operations while preserving full immutability (every 'mutation' returns a new list sharing most of the old structure).
List<int> list = new(); list.Insert(0, 1); // O(n): shifts every existing element
LinkedList<int> linked = new(); linked.AddFirst(1); // O(1): no shifting needed
ImmutableList<int> immutable = ImmutableList<int>.Empty.Add(1); // O(log n), returns a NEW list
Real-world example
Choosing LinkedList<T> specifically for a scenario with frequent insertions/removals at arbitrary positions and no need for index access, like an LRU cache's internal ordering.
Common follow-ups: In practice, why does List<T> often outperform LinkedList<T> even for insertion-heavy workloads, despite the Big-O theory favoring LinkedList<T>?
Generics
How does the 'yield return'-based lazy evaluation of collections (like LINQ or a custom iterator) differ from eagerly materializing a full collection, in terms of memory usage?
Advanced
A lazily-evaluated sequence (built with yield return, or LINQ methods before .ToList()/.ToArray()) computes and holds only ONE element in memory at a time as it's consumed, whereas an eagerly-materialized collection allocates memory for the ENTIRE result set upfront — critical for processing very large or even infinite sequences without exhausting memory.
IEnumerable<int> LazyRange(int start, int count) {
for (int i = 0; i < count; i++) yield return start + i; // computed one at a time
}
var firstFew = LazyRange(1, 1_000_000_000).Take(5).ToList(); // only 5 values ever actually computed
Real-world example
Processing an enormous or effectively infinite sequence (like reading lines from a huge file) while only holding a small working set in memory.
Common follow-ups: What happens to this memory advantage if you accidentally call .ToList() too early in a LINQ chain over a lazy sequence?
Iterators & yield return
How would you implement a custom, strongly-typed, read-only wrapper around a mutable collection to safely expose it from a class's public API?
Advanced
Wrap the internal mutable collection with ReadOnlyCollection<T> (via .AsReadOnly()) or expose it typed as IReadOnlyList<T>/IReadOnlyCollection<T> — this prevents CONSUMERS from mutating your class's internal state directly through the exposed reference, while your class's own internal code retains full mutable access to the underlying List<T>.
public class Order {
private readonly List<string> _items = new();
public IReadOnlyList<string> Items => _items.AsReadOnly(); // consumers can read, but not mutate
public void AddItem(string item) => _items.Add(item); // only the class itself can mutate
}
Real-world example
Safely exposing an entity's internal collection (like an Order's line items) without letting external code bypass validation by mutating it directly.
Common follow-ups: Does wrapping with .AsReadOnly() create a deep copy, or does it still reflect live changes made through the class's own internal mutation methods?
Interfaces & Abstract Classes