ToLookup or not ToLookup, that is the question!

ToLookup or not ToLookup, that is the question!

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 the ToLookup call is read-only. It is therefore not possible to append or otherwise modify the lookup table. This is in contrast with IDictionary<TKey, TValue> returned by ToDictionary, 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 is GroupBy. Really the only difference between these two methods is that GroupBy uses deferred execution, while ToLookup'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 this blog.