(Print this page)

LINQ-to-Objects Part 3 (of 3)
Published date: Sunday, September 23, 2012
On: Moer and Éric Moreau's web site

Here we are in September. The summer is over, very important season in Canada because of the nice temperature and because it means holidays for most people.

It is also the time for me to write the last part of a series about LINQ-to-Objects. The first 2 articles of the series have been well received. This last article on that subject will cover new categories of LINQ operators: Partitioning, Element, and other hard to classify operators.

Downloadable demo

The downloadable this month is a solution containing 2 projects (1 in VB, 1 in C#). It was created using Visual Studio 2010 but you can surely reuse this code if you Visual Studio 2008 or better.

You will find all the code of last month article as well in the solution for you convenience if you want to retry/review some topics of the previous demo.

Figure 1: The demo application in action

Test Data

We will continue the demo using the same test data as in the previous article.

Partitioning Operators

Sometimes, you are not interested in working with the full set of data. You want to work on a subset of the full source. For example you might want to take the first x items as a sample. Partitioning operators are also well suited to create paging features.

The first operator falling in this category is Take and it is really simple. It is applied on the result of a query and takes an integer argument saying how many elements to return from the query. For example, the following query will take the first 5 rows of the set of products:

'VB
Dim result = Enumerable.Range(1000, 100).
   Select(Function(x) (New Product With {
                       .IdProduct = x,
                       .Description = "Product " + x.ToString(),
                       .Price = x * 2
                   })).
       Take(5)
//C#
var result = Enumerable.Range(1000, 100)
    .Select(x => (new Product
    {
        IdProduct = x,
        Description = "Product " + x,
        Price = x * 2
    }))
    .Take(5);

Sometimes, you don’t know exactly how many you want but you have a formula/condition to determine which items to extract. You can then use the TakeWhile operator like this:

'VB
Dim result = Enumerable.Range(1000, 100).
   Select(Function(x) (New Product With {
                       .IdProduct = x,
                       .Description = "Product " + x.ToString(),
                       .Price = x * 2
                   })).
       TakeWhile(Function(y) y.Price < 2020)
//C#
var result = Enumerable.Range(1000, 100)
    .Select(x => (new Product
    {
        IdProduct = x,
        Description = "Product " + x,
        Price = x * 2
    }))
    .TakeWhile(y => { return y.Price < 2020; });

The Skip and SkipWhile operators are often used in conjunction with the Take and TakeWhile operators. As I said before, it is easy to build a pagination feature with these operators.

For example, this query will skip the 23 first items and take the 5 following:

'VB
Dim result = Enumerable.Range(1000, 100).
   Select(Function(x) (New Product With {
                       .IdProduct = x,
                       .Description = "Product " + x.ToString(),
                       .Price = x * 2
                   })).
       Skip(23).
       Take(5)
//C#
var result = Enumerable.Range(1000, 100)
    .Select(x => (new Product
    {
        IdProduct = x,
        Description = "Product " + x,
        Price = x * 2
    }))
    .Skip(23)
    .Take(5);

This other example will skip all the items while the price is less than 2010 and take all the following ones while the price is less than 2020:

'VB
Dim result = Enumerable.Range(1000, 100).
   Select(Function(x) (New Product With {
                       .IdProduct = x,
                       .Description = "Product " + x.ToString(),
                       .Price = x * 2
                   })).
       SkipWhile(Function(z) z.Price < 2010).
       TakeWhile(Function(y) y.Price < 2020)
//C#
var result = Enumerable.Range(1000, 100)
    .Select(x => (new Product
    {
        IdProduct = x,
        Description = "Product " + x,
        Price = x * 2
    }))
    .SkipWhile(z => { return z.Price < 2010; })
    .TakeWhile(y => { return y.Price < 2020; });

Notice that you can also mix Take and SkipWhile or TakeWhile and Skip if it better fits your requirements.

Element Operators

As the name says, element operators are not returning sets, they are returning a single item. Much like Take(1) but you have an element returned instead of a set containing a single item.

For example, this query will return the first customer having the city set to Montréal:

'VB
Dim result = _customers.First(Function(c) c.City = "Montréal")
//C#
var result = _customers.First(c => c.City == "Montréal");

But what if your criteria is not returning any element at all? You would expect the returned element to be a null value but instead you will received an InvalidOperationException (Sequence contains no matching element). You have to 2 options to prevent your application from crashing. The first one is to put your query into a Try … Catch block. Your second option, my preferred one, is to use another operator (FirstOrDefault) which will return a null element instead of raising an exception:

'VB
Dim result = _customers.FirstOrDefault(Function(c) c.City = "Sherbrooke")
//C#
var result = _customers.FirstOrDefault(c => c.City == "Sherbrooke");

The value returned when an element is not found is not always null. It will be null for all reference types and nullable types. In other cases, you will get the default value of your source.

Also notice that the predicate is not mandatory. If you don’t write any, the first element of the set will be returned.

Just like First and FirstOrDefault, you can use Last or LastOrDefault exactly the same way.

Another operator falls in this category. It is named Single. You can use this operator when you are expecting one and only item to be returned. An InvalidOperationException will be thrown if no item or more than one item is returned by the predicate.

'VB
Dim result = _customers.Single(Function(c) c.City = "Vancouver")
//C#
var result = _customers.Single(c => c.City == "Vancouver");

The SingleOrDefault operator also exist but behaves a bit differently. If the predicate returns a single item, the item is returned as expected. If no item is returned, the default value is returned. If more than one item is returned an exception is thrown.

Still in the same category, the ElementAt and the ElementAtOrDefault operators can be used if you have the index (0 based) of the item you want. Notice that you will receive an ArgumentOutOfRangeException if you try to reach item at a position that does not exist.

'VB
Dim result = _customers.ElementAt(3)
//C#
var result = _customers.ElementAt(3);

Miscellaneous Operators

Not much left is left on what I wanted to cover.

One of these is the Concat operator which is used to take 2 sets of the same type and concatenate them together. This is an example of concatenating 2 sets of products:

'VB
Dim result = _products.Concat(_products2)
//C#
var result = _products.Concat(_products2);

I can hear you say “wait a minute, haven’t we saw the Union operator doing the exact same thing?”. You are right but there is an important difference between both. The Concat operator will return all the items of both sets while the Union operator will return the distinct result (eliminating duplicates). If you are sure that you don’t have duplicates, the Concat operator will perfom better because it requires less checking.

One last, the SequenceEqual operator is useful to compare if 2 sets are identical. It returns a Boolean value:

'VB
If _products.SequenceEqual(_products2) Then
    lblResults.Text = "Both sets are identical"
Else
    lblResults.Text = "Sets are different"
End If
//C#
if (_products.SequenceEqual(_products2))
    lblResults.Text = "Both sets are identical";
else
    lblResults.Text = "Sets are different";

Conclusion

This is the last article in a series of 3 on the LINQ-to-Objects topic. I hope you found something useful in them.

Of course this article (even all the 3 parts together) are not exhaustive on the LINQ subjects. Complete books (I should say tick books) have been written on this subject and even those are not exhaustive. But I wish you found in here a good basement on which to build up your knowledge of LINQ.


(Print this page)