Search
Close this search box.

C#/.NET Little Wonders: Select() and Where() with Indexes

Once again, in this series of posts I look at the parts of the .NET Framework that may seem trivial, but can help improve your code by making it easier to write and maintain. The index of all my past little wonders posts can be found here.

We’ve talked about the Select() and Where() LINQ extension methods before.  The Select() method lets you project from the source type to a new type, and the Where() method lets you filter the list of items to the ones you are interested in. 

Most people know of these methods in their simplest form, where they simply take a projection and predicate respectively that operates on just an element.  However, there are overloads for both of these methods that take a delegate that operates on both the element and the index of the element.

So let’s take a look at these and see what we can do with them.

Select() – Projects elements

The Select() method is responsible for projecting a sequence into a new sequence which may or may not be different types and/or values.

As an extension method, it’s most common form is:

  • Select(Func<T, TResult> projection)
    • Projects the source sequence into a new resulting sequence consisting of the results of each item passed through the projection delegate.

As you know, this gives us a lot of power, we can use it to change values (and keep types the same):

1 :  // an array of 1 to 10
     2 : var numbers = Enumerable.Range(1, 10).ToList();
3 : 4 :  // converts to an array of 2, 4, 6, ..., 20
         5 : var doubles = numbers.Select(i => i * 2).ToList();

So that’s an example of a projection that changes the values, you can also project to a different type:

1 :  // the numbers from 1 to 10
     2 : var numbers = Enumerable.Range(1, 10).ToList();
3 : 4 :  // project to list of strings "1: ", "2: ", ...
         5 : var lineNumbers =
                 numbers.Select(i => i.ToString() + ": ").ToList();

Now that we’ve reviewed the basic form, let’s look at a lesser-used overload of Select():

  • Select(Func<T, int, TResult> projection)
    • Projects the source sequence into a new resulting sequence consisting of the results of each item and its index in source passed through the projection delegate.

Note the difference here: in this overload the delegate takes not just an element, but an element and the index of that element in the source sequence this method is invoked upon.

Thus, if you had a list of items and wanted to take advantage of the item’s index as well in the projection, you can:

1 :  // say that racers consists of the names of racers in the order of finish:
     2 : IEnumerable<string> racers = ...;
3 : 4 :  // get a projection of new objects consisting of placing and name
         5
    : var finishers =
          racers.Select((r, index) => new { Place = index, Name = r }).ToList();

The code snippet above will take a sequence of string and convert it into a list of anonymous objects representing the racer’s name and their place.  So given this form of the Select(), you can use the index as part of the projection and either store it, or use it to build a new value.

One very important note, the index provided to the projection is the index from the sequence the Select() was immediately called from.  This is an important distinction because you can chain together multiple operations which may alter the number of items in the sequence (or their order) before the Select() is invoked.  This is very similar to the way that Skip() and Take() (click here for a discussion of these two methods) work with their index overloads.

For example:

1 :  // fibs under 100
     2 : var numbers = new[] { 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89 };
3 : 4 :  // even fibs and their indexes?
         5 : var evenFibs = numbers 6 :.Where(f => (f % 2) == 0) 7
    :.Select((f, index) => new { Number = f, Index = index }) 8 :.ToList();

So looking at this code, you might think you’d get a list that contains Number = 2 at Index = 2, Number = 8 at Index 5, and Number = 34 at Index 8.  But instead you’ll get Number = 2 at Index = 0, Number = 8 at Index = 1, and Number = 34 at Index = 2.

Why?  Because remember that the Where() filters the sequence from (1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89) down to just a sequence of the evens (2, 8, 34) and that is the sequence the Select() is operating on, thus that sequence is the one who’s indexes are used.  Make sense?  The Where() returns a new, shorter sequence, and that is the sequence that Select() uses for its elements and what it bases the indexes from.

So, how would you get the results with the original indexes?  Well, one way would be to perform the Select() first, and then filter the results with the Where() second:

1 :  // fibs under 100
     2 : var numbers = new[] { 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89 };
3 : 4 :  // yes, this works...
         5 : var evenFibs = numbers 6
    :.Select((f, index) => new { Number = f, Index = index }) 7
    :.Where(r => (r.Number % 2) == 0) 8 :.ToList();

So this one first projects the sequence into a sequence of an anonymous type holding both the number and the index, and then it filters down to only the those that have an even number.

Where() – filters elements

As you know, Where() filters based on a predicate.  It’s basic form is:

  • Where(Func<T, bool> predicate)
    • Filters the source sequence to a new sequence that contains only elements that return true from the predicateapplied to each element.

So, of course, as you’ve seen in the examples above, we can use Where() to filter a sequence of numbers to just the evens:

   1: // 1 through 10
   2: var numbers = Enumerable.Range(1, 10);
   3:  
   4: // 2, 4, 6, 8, 10
   5: var evens = numbers.Where(n => (n % 2) == 0).ToList();

Just like Select(), the Where() extension method also has an overload that takes passes an index to the predicate:

  • Where(Func<T, int, bool> predicate)
    • Filters the source sequence to a new sequence that contains only elements that return true from the predicate applied to each element and its index in source. F

For example, let’s say you wanted to pull every other item in a sequence, you could do this with the overload:

1 :  // fibs under 100
     2 : var numbers = new[] { 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89 };
3 : 4 :  // get every other fib number
         5 : var everyOtherFib =
                 numbers.Where((n, index) => (index % 2) == 0).ToList();

The resulting sequence from the above will yield (1, 2, 5, 13, 34, 89).  These may seem like trivial uses, but you could also use them to coordinate across sequences.  For example, let’s say you kick off multiple service requests in various threads, and have an array of bool representing whether the results came back successfully, we could filter the requests to get the list of failed requests:

1 : bool[] wasSuccessful = new...;
2 : 3 :  // get the list of requests that had bad return codes:
         4 : var failedRequests =
                 requests.Where((r, index) => !wasSuccessful[index]).ToList();

So the code above goes through the requests, takes the index of each request, and checks to see if wasSuccessful at that same index is false.

Finally, once again it is important to note that the index passed to the Where() clause is the index of the item in the sequence Where() is immediately called upon.  So, once again, if you need the index of the item in the original sequence, make sure that none of the clauses before your indexed Select(), Where(), Skip(), or Take() alter the sequence by re-ordering, adding, or removing items.

Sidebar: Naming style for index?

When you use the form of Select(), Where(), Skip(), or Take() where you provide a lambda expression that takes an index, it is often useful to name the lambda variables such that it’s clear which item is the index. 

1 :  // Ummm, is j the number and k the index?  or vice-versa...?
     2 : var evenFibs = numbers 3 :.Where((j, k) => (k % 2) == 0) 4 :.ToList();

In the code above, because j and k don’t really have any derivable meaning from the context, we have to know from experience (or Intellisense) that k is the index.  However, many folks reading this code may not know that.

This is why typically, and this is just my personal style, I like to name the index variable explicitly index, or at least x so it’s fairly clear that we’re talking about an index and not the value:

1 :  // Ah... it was the index!
     2 : var evenFibs =
             numbers 3 :.Where((j, index) => (index % 2) == 0) 4 :.ToList();

Summary

The Select() and Where() LINQ extension methods provide powerful ways to manipulate lists.  The Select() clause is great for projecting from one sequence to another sequence containing different types/values, and the Where() clause is useful for filtering a sequence down to only those that match a given predicate.  Both of these extension methods allow you to not only project and filter based on each element in the sequence, but also based on the element’s index in the sequence.  Care should be taken to realize that the index of the item may be altered by chained expressions.

Technorati Tags: C#,CSharp,.NET,Little Wonders,LINQ,Where,Select

This article is part of the GWB Archives. Original Author: James Michael Hare

Related Posts