Two LINQ shortcuts
Two jobs come up when you work with lists: numbering the items, and walking through two lists at the same time. You could write both with a for loop and an index. LINQ has shorter forms.
Select with an index
The lambda you give to Select can take a second parameter. The first is the item, the second is its position in the list:
List<string> fruits = new List<string>(new[] {"apple", "kiwi", "plum"});
List<string> numbered = fruits.Select((fruit, i) => (i + 1) + ". " + fruit).ToList();
Now numbered holds 1. apple, 2. kiwi and 3. plum. The item comes first and the index second. The index starts at 0, so add 1 for a list that people read. Two parameters mean the lambda needs parentheses: (fruit, i) => ....
Zip: two lists side by side
a.Zip(b) pairs the first item of a with the first item of b, then the second items, and so on. Each pair is a tuple with parts named First and Second, and a foreach can unpack it into two names, the way you unpack any tuple:
List<int> prices = new List<int>(new[] {3, 1, 2});
foreach (var (fruit, price) in fruits.Zip(prices)) {
Console.WriteLine(fruit + " " + price);
}
apple 3
kiwi 1
plum 2
Without unpacking, each pair prints like a tuple, for example (apple, 3), and its parts are pair.First and pair.Second.
Zip can also build the new values for you. Give it a second argument, a lambda with one parameter from each list:
List<string> tags = fruits.Zip(prices, (fruit, price) => fruit + " " + price).ToList();
That gives apple 3, kiwi 1 and plum 2 as a list of strings. As with Select, the result is a sequence, so ToList() turns it into a List.
The two methods work well together. Zip first to build pairs, then number the results with Select and its index. That is the pattern behind a numbered list of names and values, such as 1. apple: 3.
Both calls are lazy, like all LINQ chains: nothing is computed until a loop or ToList() reads the result. That is why each example ends with one of them.
Gotchas
Zipstops at the shorter list. Zipping the three fruits with{1, 2}gives only(apple, 1)and(kiwi, 2). The extra fruit is skipped and nothing is thrown.- Index zero. The first item has index 0. Numbering from 1 means
i + 1. - Parentheses. A foreach over pairs unpacks with
var (a, b)in parentheses, notvar a, b.
The lesson's task
A quiz show keeps its players in players and their points in scores, in the same order. First build numbered with Select and the index, like 1. Ana. The code already prints it. Then loop over players.Zip(scores), unpack each pair into player and points, and print Ana has 12 points. No index is needed in the second loop, because Zip keeps each player with their own points.
class Solution {
public void MainFunction() {
List<string> players = new List<string>(new[] {"Ana", "Ben", "Cleo"});
List<int> scores = new List<int>(new[] {12, 9, 15});
List<string> numbered = players.Select((player, i) => (i + 1) + ". " + player).ToList();
foreach (string line in numbered) {
Console.WriteLine(line);
}
foreach (var (player, points) in players.Zip(scores)) {
Console.WriteLine(player + " has " + points + " points");
}
}
}
Running it prints:
1. Ana
2. Ben
3. Cleo
Ana has 12 points
Ben has 9 points
Cleo has 15 points
Step by step
- Build
numberedwithplayers.Select((player, i) => ...), givingi + 1, a dot, a space and the player, thenToList(). - Write the second
foreachoverplayers.Zip(scores), withvar (player, points)to unpack each pair. - Print the player, the word
has, the points and the wordpoints.
Key takeaways
Select((item, i) => ...)gives each item and its index, starting at 0.a.Zip(b)pairs items by position, andvar (x, y)unpacks each pair.Zipends with the shorter list, and both methods need.ToList()when you want a list.
