The filter-then-transform pattern
A very common task is to keep the elements that pass a test and then transform the ones that remain. Pick the even numbers and square them. Take the finished orders and read their totals. With a loop, the pattern has three parts: a result list, an if that decides what to keep, and an expression that builds the new value.
var evenSquares = new List<int>();
foreach (var x in nums) {
if (x % 2 == 0) {
evenSquares.Add(x * x);
}
}
LINQ, short for Language Integrated Query, is a set of methods that say the same thing in one expression. Two of them match this pattern exactly:
Where(test)keeps the elements for which the test returnstrue. It plays the role of theif.Select(transform)builds a new value from each element. It plays the role of the expression insideAdd.
Lambdas and chaining
The test and the transform are written as lambdas, small inline functions. The arrow => separates the parameter from the result:
var evenSquares = nums.Where(x => x % 2 == 0).Select(x => x * x).ToList();
Read it left to right: from nums, keep every x where x % 2 == 0, turn each remaining x into x * x, and collect the results into a list. The lambda for Where must produce a bool. The lambda for Select produces the new value, and its type can differ from the input type. For example, names.Select(n => n.Length) turns strings into integers.
Order matters
Each call receives the output of the one before it, so Where runs first here and Select sees only the even numbers. Swapping them would square every number first and then test the squares. That would still work for evenness, since a square is even only when its root is, but with other tests the order changes the result. Filtering first also means the transformation runs on fewer elements.
Lazy evaluation and ToList
Where and Select are lazy. On their own they only describe a query, and nothing runs until something enumerates it, such as a foreach loop, string.Join or ToList(). Without ToList() the variable holds an IEnumerable<int> that re-runs the query each time it is enumerated:
var nums = new List<int> { 1, 2, 3, 4, 5, 6 };
var query = nums.Where(x => x % 2 == 0).Select(x => x * x);
nums.Add(8);
Console.WriteLine(string.Join(", ", query)); // 4, 16, 36, 64
The 8 was added after the query was written, and it still shows up because the query only ran when string.Join enumerated it. Calling ToList() runs the query once and stores the results in a real List<int>, which will not change when nums does.
The using directive
Where and Select are not methods of List<int>. They are extension methods that come into scope only when using System.Linq; is at the top of the file. Without it, the compiler reports error CS1061: List<int> does not contain a definition for Where, and it asks whether you are missing a using directive. That message tells you the fix. In this exercise the directive is already in place, but you will meet the error when you start a file from scratch.
The exercise, step by step
The lesson builds both versions side by side, so you can compare them.
- Create the data.
var nums = new List<int> { 1, 2, 3, 4, 5, 6 };uses a collection initializer.varlets the compiler inferList<int>. - Make an empty result list named
loopSquares. It grows with eachAdd, so it needs no size. - Open a
foreachloop overnumswith the variablex, and use braces. - Test for evens. Inside the loop,
if (x % 2 == 0). The remainder after dividing by 2 is 0 for even numbers. - Add the square. Inside the
if, callloopSquares.Add(x * x), so only values that passed the test are squared and stored. - Print the loop result with
string.Join(", ", loopSquares)after the textloop:.string.Joinputs the separator between elements only, so there is no trailing comma. - Write the LINQ version in one line:
Wherefor the test,Selectfor the square andToList()to materialize the result inlinqSquares. - Print it the same way, after the text
linq:.
var nums = new List<int> { 1, 2, 3, 4, 5, 6 };
var loopSquares = new List<int>();
foreach (var x in nums) {
if (x % 2 == 0) {
loopSquares.Add(x * x);
}
}
Console.WriteLine("loop: " + string.Join(", ", loopSquares));
var linqSquares = nums.Where(x => x % 2 == 0).Select(x => x * x).ToList();
Console.WriteLine("linq: " + string.Join(", ", linqSquares));
The program prints:
loop: 4, 16, 36
linq: 4, 16, 36
Common mistakes
- Missing
using System.Linq;. This causes the CS1061 error described above. - A
Wherelambda that doesn't return abool. Writingx => x % 2is a compile error, because the result is anint. Compare with== 0. - Forgetting
ToList(). Printing a lazy query withConsole.WriteLinedirectly shows the type name, not the items. Usestring.Joinor addToList(). - Expecting
Whereto change the original. LINQ never modifiesnums. Each query produces new data.
Key takeaways
Wherekeeps elements that pass a test;Selecttransforms each element.- Both take lambdas, and they chain from left to right.
- They are extension methods from
System.Linq. - They are lazy:
ToList()runs the query and stores the result.