Clarify the observable contract
A request to remove matching items needs an ownership decision: return a filtered copy or mutate an exclusively owned list. This lesson reviews a common bug where code removes elements while a foreach enumerator is traversing the same list. The executable solution demonstrates two deliberate alternatives without relying on an exception message or accidental enumerator behavior.
Returning a copy and mutating in place can both be correct, but they are not interchangeable for callers holding aliases. State which object owns the change. The program creates a separate owned list for RemoveAll so it can demonstrate compaction without altering the original source that the first result was derived from.
Walk through the verified program
Filtering odd values into an array produces one and three while leaving the original list one,two,three,four. A separate owned copy then calls RemoveAll and reports two removed entries. Both final selections agree, but their aliasing behavior differs. The harness keeps both lists available so the difference remains observable.
ToArray forces the filtered query before the original list is displayed, fixing that result to the observed values. RemoveAll evaluates the predicate over its owned list and returns the removal count. The final list and the count provide two independent aspects of the postcondition: what remains and how much was removed.
using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
var source=new List<int>{1,2,3,4};
int[] filtered=source.Where(x=>x%2!=0).ToArray();
Console.WriteLine("copy:"+string.Join(",",filtered)); Console.WriteLine("source:"+string.Join(",",source));
var owned=new List<int>(source); int removed=owned.RemoveAll(x=>x%2==0);
Console.WriteLine("in place:"+string.Join(",",owned)+";removed:"+removed);
Account for time, storage and allocations
Filtering and RemoveAll each take O(n) predicate work. The filtered array requires O(r) result storage, while RemoveAll compacts the owned list in place with constant auxiliary state. Constructing the separate demonstration copy itself costs O(n) storage; do not omit that cost when describing this exact program.
Space analysis must include demonstration setup when discussing the complete executable, even if the library operation itself compacts in place. A copy-returning API has required output storage; a mutating API may reduce retained capacity only under another explicit operation. Removing elements does not mean the backing array immediately shrinks to the kept count.
Review assumptions and likely defects
A deferred Where query is not yet a snapshot, so source mutation before enumeration changes its result. Removing by increasing index can skip shifted elements. Concurrent mutation adds another ownership problem that neither a filtered query nor an unsynchronized list solves automatically.
Replacing foreach with a forward index loop can avoid one enumerator issue while introducing skipped elements after each removal. Reverse-index deletion can be valid but may repeatedly shift list contents. A correct review weighs the appropriate collection operation and ownership contract rather than searching for a syntactic loop substitution.
Practice one: discriminate edge cases
Test all-matching, none-matching and alternating lists. Hint: count the removed items separately from the kept items and verify the original list remains unchanged only for the copy-returning path.
For alternating input, verify the entire result sequence rather than only its count. A skipped-removal bug can retain the wrong elements while still passing a weak size assertion. For the copy path, also assert the original sequence is unchanged; for the mutating path, assert the documented object itself changed.
Practice two: defend an extension
Review an API returning a mutable List that callers may retain. Propose either a documented transfer of ownership or an immutable snapshot boundary, and discuss allocation, aliasing and synchronization rather than merely rewriting foreach as a for loop.
An API can return an immutable snapshot or document exclusive mutable ownership, but a read-only interface alone does not necessarily freeze the backing collection. If callers may retain aliases, mutation needs synchronization or copying at a known boundary. Choose based on caller semantics before optimizing away allocations.
Evaluate correctness and interview reasoning
The interview review should identify the original mutation hazard, choose a deliberate ownership policy and show the observable difference between copying and compaction. It should count both required output and any demonstration copies when describing memory. A useful regression suite covers all matches, no matches and adjacent matches, with sequence assertions and alias checks. A senior explanation recognizes that read-only access and immutable state are different guarantees. This is a debugging and API-design exercise, so the most important improvement is making mutation responsibility clear rather than merely obtaining the expected two numbers once.
A working example
using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
var source=new List<int>{1,2,3,4};
int[] filtered=source.Where(x=>x%2!=0).ToArray();
Console.WriteLine("copy:"+string.Join(",",filtered)); Console.WriteLine("source:"+string.Join(",",source));
var owned=new List<int>(source); int removed=owned.RemoveAll(x=>x%2==0);
Console.WriteLine("in place:"+string.Join(",",owned)+";removed:"+removed);
Expected output
copy:1,3 source:1,2,3,4 in place:1,3;removed:2
Test all-matching, none-matching and alternating lists. Hint: count the removed items separately from the kept items and verify the original list remains unchanged only for the copy-returning path.
Further reading
Interview questions for this lesson
- Explain: How do a filtered copy and RemoveAll differ in ownership, aliasing and allocation despite returning the same kept values?
- Trace: Trace the complete program for debug mutation by choosing ownership. State the exact stdout, then justify the state transitions and relevant edge conditions.
- Design: Review an API returning a mutable List that callers may retain. Propose either a documented transfer of ownership or an immutable snapshot boundary, and discuss allocation, aliasing and synchronization rather than merely rewriting foreach as a for loop.