ToLookup or not ToLookup, that is the question!
Jan Doubek
Posted on May 30, 2021
Shakespeare will hopefully forgive me for abusing a line from his famous Hamlet. In my post, I won't be covering questions of life and death, but rather something much more prosaic - a lesser-known LINQ materialization method - ToLookup
.
We all know the famous LINQ materialization methods trio - ToArray
, ToList
, ToDictionary
. There's another such method, which, similarly to the other ones, can be quite useful. The other method is Enumerable.ToLookup().
Here's a description of ToLookup
from the Microsoft Docs page:
The ToLookup method returns a Lookup, a one-to-many dictionary that maps keys to collections of values.
Ok, so ToLookup
is similar to the ToDictionary
method. There's one subtle, but important difference though. With Dictionary, which is basically a typed hash table, you are allowed to associate only a single value to each key, i.e. there's always a one-to-one
mapping between keys and values. ToLookup
on the other hand lets the user store a collection of values for each key, i.e. effectively creating a one-to-many
mapping.
That's it for theory. Let's have a look at some examples.
ToLookup Examples
In the first example, we'll use ToLookup
to split up a collection of numbers into two collections containing even and odd numbers.
var numbers = new[] { 1, 2, 3, 4, 5, 6, 7, 8 };
// Using explicit types instead of 'var' for better explicability
ILookup<bool, int> evenNumbersLookup = numbers.ToLookup(num => (num % 2) == 0);
IEnumerable<int> evenNumbers = evenNumbersLookup[true];
IEnumerable<int> oddNumbers = evenNumbersLookup[false];
Console.WriteLine("EVEN: " + string.Join(',', evenNumbers));
Console.WriteLine("ODD: " + string.Join(',', oddNumbers));
Output:
EVEN: 2,4,6,8
ODD: 1,3,5,7
The key
of the lookup table here is a boolean determining whether the number is even or odd. The value
is a collection of integers satisfying the even/odd condition.
Simple, yet effective.
The lookup key
does not necessarily have to be a true/false value - it can be of any type. In the following example, we use ToLookup
together with an indexed select to split an input set of numbers into groups of 3 elements.
(Check out one of my previous LINQ Gems articles for more info about indexed select.)
var numbers = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 };
ILookup<int, int> groupedNumbersLookup = numbers
.Select((num, seq) => (num, seq))
.ToLookup(key => (key.seq / 3), val => val.num);
var i = 0;
while (groupedNumbersLookup.Contains(i))
{
Console.WriteLine($"GROUP {i+1}: " + string.Join(',', groupedNumbersLookup[i]));
i++;
}
The output will be:
GROUP 1: 1,2,3
GROUP 2: 4,5,6
GROUP 3: 7,8,9
GROUP 4: 10,11,12
And, naturally, the key
of the lookup table is not restricted to numbers only. Here is a nice example of a lookup table having a string
key:
var carsInStock = new[]
{
new { Make = "Ford", Price = 10000 },
new { Make = "Ford", Price = 15000 },
new { Make = "Ford", Price = 20000 },
new { Make = "Honda", Price = 15000 },
new { Make = "Honda", Price = 25000 },
new { Make = "Jeep", Price = 20000 },
};
var carValues = carsInStock.ToLookup(key => key.Make, val => val.Price);
int fordCarsValue = carValues["Ford"].Sum();
int hondaCarsValue = carValues["Honda"].Sum();
Console.WriteLine($"Total value of all Ford cars in stock: ${ fordCarsValue }");
Console.WriteLine($"Total value of all Honda cars in stock: ${ hondaCarsValue }");
Gives us the output that we'd expect:
Total value of all Ford cars in stock: $45000
Total value of all Honda cars in stock: $40000
Some Noteworthy Facts
- Similarly to all the other
ToXXX()
methods,ToLookup
as well uses immediate execution to populate the returned Lookup table. Any changes to the original sequence (after the method has returned) won’t change the elements stored in the lookup. - The
ILookup<TKey,TElement>
interface returned by theToLookup
call is read-only. It is therefore not possible to append or otherwise modify the lookup table. This is in contrast withIDictionary<TKey, TValue>
returned byToDictionary
, which constitutes a fully mutable collection of key/value pairs. - If you request a sequence for a key, which is not present inside of the lookup table, you'll get an empty sequence. Again, compare this behavior to Dictionary, which will throw a
KeyNotFoundException
for non-existing keys. - There is another extension method available, which is almost identical in behavior to what
ToLookup
does. That method isGroupBy
. Really the only difference between these two methods is thatGroupBy
uses deferred execution, whileToLookup's
execution is immediate.
Additional Resources
For a thorough deconstruction of the ToLookup
method, see John Skeet's great article Reimplementing LINQ to Objects Part 18 – ToLookup.
Conclusion
As you can see, the usage of ToLookup
is quite versatile. My favorite use case (by far) is the ability to split up a sequence of values into 2 sequences using a single line of code (described in the first example above).
Have you ever used ToLookup
in your code? If yes, what was the use case? Share your thoughts in the comments!
If you find this post interesting, be sure to check out other posts on my blog mydevtricks.com.
Posted on May 30, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.