LINQ In Depth
csharp1-8 previewed LINQ's two syntaxes. This chapter goes underneath both — into what a LINQ query actually is before it runs, and where its behavior genuinely diverges from java2-4's own Streams API.
Method Syntax vs. Query Syntax — When to Reach for Which
Method syntax composes naturally with the rest of C# — custom extension methods, conditionals, anything else in scope. Query syntax tends to read more clearly once a query involves a join or spans multiple sources, where its SQL-like shape genuinely earns its keep.
Deferred Execution
Genuinely comparable to java2-4's own lazy Streams — a LINQ query isn't run when it's defined, only when it's actually enumerated. But here's a real, concrete difference: a LINQ query can be re-enumerated — each foreach, each .ToList(), re-runs the whole query fresh against the current state of the source. java2-4's own Streams throw IllegalStateException on a second terminal operation; C#'s query above has no such restriction at all.
IEnumerable<T> vs. IQueryable<T>
IEnumerable<T> queries run as ordinary in-memory delegates, the same territory java2-4's Streams live in entirely. IQueryable<T> — used by Entity Framework and similar tools — is a genuinely different mechanism: the same .Where()/.OrderBy() calls build an expression tree instead of executing anything, which gets translated into real SQL and run on a database server, only once the query is enumerated. java2-4's Streams have no equivalent capability at all — they only ever operate over already-materialized in-memory Java objects.
A Deferred Execution Gotcha — Variable Capture
A real, opposite design choice from java2-3's own Java lambdas: Java requires a captured local variable to be effectively final — reassigning it anywhere makes the capture illegal, a compile error. C# places no such restriction on ordinary local variables — the lambda captures the variable itself, not a snapshot of its value, so the query genuinely reads whatever threshold holds at the moment it's actually enumerated, not the moment it was written.
| Aspect | Java Streams (java2-4 / java2-3) | C# LINQ |
|---|---|---|
| Execution timing | deferred until a terminal op | deferred until enumeration |
| Re-use after first execution | throws IllegalStateException | fully re-enumerable, re-runs fresh each time |
| Remote/database translation | not available — in-memory only | IQueryable<T> — expression trees translated to SQL |
| Captured local variable | must be effectively final | captured live, by reference — freely reassignable |
.ToList() or .ToArray() runs the query once, right there, and hands back a real, independent collection.
foreach loops over the same query variable can genuinely produce different output if the source collection or a captured variable changed in between — a real, easy-to-miss source of confusing bugs, especially coming from Java's single-use Stream model where this scenario simply can't arise.
Coding Challenges
Define a LINQ query over a List<int> filtering for values greater than 5, add a new qualifying element to the list after defining the query, then enumerate the query and show the new element is included.
📄 View solutionWrite a query capturing a local int variable in a Where() lambda, enumerate it once, then change the variable's value and enumerate the exact same query variable a second time, showing the two results differ.
📄 View solutionRepeat Challenge 1's scenario, but call .ToList() immediately after defining the query. Add the same new element to the source list afterward, then print the materialized list, showing the new element is NOT included this time.
📄 View solutionChapter 2 Quick Reference
- Method syntax composes naturally; query syntax reads best for joins/multi-source queries
- LINQ queries are deferred, like java2-4's Streams — but genuinely re-enumerable, unlike Streams' single-use IllegalStateException restriction
- IEnumerable<T> runs in-memory like Java Streams; IQueryable<T> translates to expression trees and real SQL — no Java Streams equivalent exists
- C# lambdas capture local variables live, by reference — no effectively-final restriction, the opposite of java2-3's own Java rule
- .ToList()/.ToArray() force immediate evaluation for a stable, independent snapshot
- Next chapter: delegates, events, and lambda expressions — a different mechanism behind similar syntax to java2-3's own SAM interfaces