span.sup { vertical-align:text-top; }

Advanced Basics

The LINQ Enumerable Class, Part 1.

Ken Getz

Code download available at:AdvancedBasics2008_07.exe(184 KB)

Contents

Inside Enumerable
Creating Sequences
Selecting Sequences
Retrieving a Single Element
Filtering Sequences
Ordering Sequences
Verifying Sequences

As part of a recent programming project, I needed to create a randomly ordered list of integers between 1 and 100. Yes, this is obviously a basic Computer Science 101-type problem. And I originally used the same solution I had written back in the classroom, in 1985, using lots of looping and comparisons and generating random numbers and several arrays. But in these days of declarative programming, the solution really bothered me.

After some research, I managed to whittle the problem down to just two lines of code:

Dim rnd As New System.Random()
Dim numbers = Enumerable.Range(1, 100). _
  OrderBy(Function() rnd.Next)

After running these two lines, the numbers variable contains the integers between 1 and 100, in random order.

The behavior of this simple yet elegant solution counts on the System.Linq.Enumerable class, which provides a few dozen shared methods called extension methods that allow you to manipulate data in any class that implements the IEnumerable(Of T) interface. In addition, as you see in this example, these methods also can generate new collections of data.

In this and my next column, I will provide examples of the Enumerable class's extension methods, showing off multiple overloaded versions of the members and using various data structures to demonstrate the capabilities. Because you can apply these methods in so many places, and because they're so incredibly useful, it makes sense to take some time and investigate each—that way, when you need to operate on a collection, you'll know what tools you have available to you.

I've created a simple Windows®-based application as a sample. In the project, I've added simple classes representing the Customers, Categories, and Products data, and I've added a class named SimpleDataContext that fills instances of the these classes with data from the SQL Server® Northwind sample database. In addition, this class exposes public properties named Customers, Categories, and Products; each containing a generic List of Customer, Category, and Product instances, respectively. (Although I could have used the LINQ to SQL designer to create a "real" DataContext class, doing so wouldn't allow me to demonstrate the Enumerable class's extension methods: when you call the described methods in context of LINQ to SQL, you're actually calling extension methods on the LINQ to SQL provider's Queryable class that have the same signatures as the methods shown here. Although such examples would illustrate the same behavior, they wouldn't be using the Enumerable class.) I strongly suggest that you download the sample application and follow along using this application—the code will make much more sense if you can experiment with it directly, rather than just reading it here.

To make the long list of methods more tractable, I've broken them down into logical groupings. You'll investigate methods that deal with creating, selecting, retrieving a single element, filtering, converting, positioning within sequences, ordering, verifying, calculating, and performing set operations of sequences of data. In each of the following sections, you'll try out various examples that demonstrate one or more methods of the Enumerable class. By the time you're done, you will have worked through all the methods of the Enumerable class, and you will have investigated many of the overloads for the more complicated methods.

Note that the examples generally allow Visual Basic® to infer data types, rather than having the code specify the type of the return value for calls to the extension methods on the Enumerable class. Also, the sample relies on the Northwind sample database, installed on SQL Express. If you want to run the sample, you'll either need to replicate this environment or modify the sample application.

Inside Enumerable

The Enumerable class plays an important role in every LINQ query you create. For example, imagine that your project includes a Product class and a generic List of Product instances (as in the sample project). You've created an instance of the SimpleData­Context class from the sample that fills data in the Products list, and you write a query like this:

Dim db As New SimpleDataContext()
Dim query = _
  From prod In db.Products _
  Where prod.CategoryID = 1 _
  Order By prod.ProductName _
  Select prod.ProductName

If you've thought much about this, you've probably realized that, behind the scenes, the Visual Basic compiler must convert the SQL-like syntax into some set of method calls on some instance of a type. Indeed, in this example, the Products property of the SimpleDataContext class is of the List(Of Product) type and, as you might have guessed, the List(Of T) type implements the IEnumerable(Of T) interface. The compiler converts the LINQ keywords into corresponding calls to extension methods on the Enumerable type. Specifically, you could create the same exact set of instructions by rewriting the previous LINQ query like this:

Dim query = _
  db.Products _
  .Where(Function(prod) CBool(prod.CategoryID = 1)) _
  .OrderBy(Function(prod) prod.ProductName) _
  .Select(Function(prod) prod.ProductName)

If you examine the intermediate language (IL) created by the compiler, using the LINQ query that employs Visual Basic keywords, you will see that the compiler converts the Visual Basic keywords into calls to the corresponding methods of the Enumerable class. Doing this results in the extension of the System.Collec­tions.Generic.List(Of T) class behavior. (As a matter of fact, it's easy to determine this—look in the documentation for this particular class's members, and you'll see a list of extension methods, most of which are defined by the Enumerable class.)

What does this all mean to you? Because the Enumerable class's extension methods can process many other classes—including Array and List—you can use methods of the Enumerable class not only to create LINQ queries but also to manipulate the behavior of arrays and other data structures.

If you examine the documentation for the extension methods on the Enumerable class, you'll find that each member is Shared. As a result, you have two options for calling a method. First, you can call it as a method of the declaring type, like this, passing in the instance of the extended class (assuming that the results variable is of a type can be processed by the Count method):

Dim itemCount = _
  Enumerable.Count(results, _
  Function(prod) prod.CategoryID = 3)

You can also call Count as if it were an instance method on the collection itself. When you call the extension method this way, you don't need to supply the first parameter:

Dim itemCount = _
  results.Count(Function(prod) _
  prod.CategoryID = 3)

Visual Basic and C# handle the conversion from keywords into extension method calls differently. Visual Basic provides more keywords that you can use within your LINQ queries than does C#. For example, you can create a LINQ query in Visual Basic using the Take keyword, like this:

Dim db As New SimpleDataContext()
Dim query = _
  From prod In db.Products _
  Where prod.CategoryID = 1 _
  Order By prod.ProductName _
  Take 10 _
  Select prod.ProductName

You could also use the Take extension method, provided by the Enumerable class, like this:

Dim query = _
  (From prod In db.Products _
  Where prod.CategoryID = 1 _
  Order By prod.ProductName _
  Select prod.ProductName).Take(10)

Because C# doesn't supply the Take keyword, you would have no other choice than to call the Take method explicitly. Figure 1 lists the mappings between Visual Basic LINQ query keywords and the corresponding methods of the Enumerable class.

Visual Basic LINQ Query Keyword Enumerable Method
Aggregate … Into … All All
Aggregate … Into … Any Any
Aggregate … Into … Average Average
Aggregate … Into … Count Count
Aggregate … Into … LongCount LongCount
Aggregate … Into … Max Max
Aggregate … Into … Min Min
Aggregate … Into … Sum Sum
Distinct Distinct
Group By GroupBy
Group Join GroupJoin
Order By OrderBy
Order By … Descending OrderByDescending
Order By (with multiple fields) ThenBy
Order By … Descending (with multiple fields) ThenByDescending
Select Select
Skip Skip
Take Take
Take While TakeWhile
Where Where

Creating Sequences

The Enumerable class provides several Shared methods that are not extension methods but exist for you to create new sequences. I will start by examining these simple methods.

The Enumerable.Range method creates a new sequential list of integers. You specify the starting value and the number of items in the list. The method returns an IEnumerable(Of Integer) sequence. You can use this method to solve the original problem in this article (that is, retrieving a list of numbers between two endpoints, in random order).

The following code fills the list using the Range method, then calls the Enumerable.OrderBy method with a simple lambda expression to arrange the sequence in random order:

' From RangeDemo in the sample:
Dim rnd As New System.Random
Dim items = Enumerable.Range(1, 10)
Dim randomList = _
  items.OrderBy(Function() rnd.Next())

Running the sample procedure multiple times returns different results each time, but you should see a list like the following each time you execute the code:

6, 8, 7, 1, 2, 9, 10, 4, 3, 5

The Enumerable.Reverse method returns its input sequence in reverse order. The following code calls the Reverse method both as a Shared method of the Enumerable class and as an instance method on the list resulting from calling the Enumerable.Range method:

'From ReverseDemo in the sample:
Dim items = Enumerable.Range(1, 10)
Dim reversed = items.Reverse()
reversed = Enumerable.Reverse(items)

However you call the Reverse method, you'll end up with the following list:

10, 9, 8, 7, 6, 5, 4, 3, 2, 1

The Enumerable.Repeat method creates a list containing the value you supply, repeated as many times as you specify:

'From RepeatDemo in the sample:
Dim repeated = Enumerable.Repeat("Hello", 5)

After calling the previous code, the repeated variable contains the following items:

Hello, Hello, Hello, Hello, Hello

Use the Enumerable.Empty method to create an empty IEnumerable(Of T) that's set up to contain the specified data type. Using this method, you could create the empty instance of the collection and add items to it as necessary. By initializing the collection to an empty collection, you never have to worry about whether the reference is Nothing:

'From EmptyDemo in the sample:
Dim emptyList = _
 Enumerable.Empty(Of Customer)()

Selecting Sequences

The Enumerable class provides two different extension methods that allow you to select a sequence of elements, projecting the elements from the input sequence into an output sequence. The Enumerable.Select method simply projects each element into a new form. The following example takes a sequence containing customers in the U.S. from the Northwind Customers table, then projects the results into a sequence that contains a numbered list of contact names:

' From SelectDemo in the sample:
Dim db As New SimpleDataContext
Dim results = _
  From cust In db.Customers _
  Where cust.Country = "USA"
Dim selectResults = results.Select( _
  Function(cust, index) _
  String.Format("{0}. {1}", _
  index + 1, cust.ContactName))

Note that the Select method has multiple overloaded versions. After running this code, selectResults contains a list of values resembling the following:

1. Howard Snyder
2. Yoshi Latimer
3. John Steel
4. Jaime Yorres
5. Fran Wilson
6. Rene Phillips
7. Paula Wilson
8. Jose Pavarotti
9. Art Braunschweiger
10. Liz Nixon
11. Liu Wong
12. Helvetius Nagy
13. Karl Jablonski

The Enumerable.SelectMany method takes a collection of items, each of which can be a collection of items, and collapses the two-dimensional input into a single-dimensional output sequence. This method can be useful if you need to take, for example, a list of categories, each of which contains products, and end up with a list of products with corresponding category information. The following example retrieves a list of categories for which the category contains less than seven products, and projects the result into a single list containing information about each product, along with category information:

' From SelectManyDemo in the sample:
Dim db As New SimpleDataContext
Dim categories = _
  From cat In db.Categories Where cat.Products.Count < 7
Dim manyResults As IEnumerable(Of String) = _
  categories.SelectMany(Function(cat, index) _
  cat.Products.Select( _
  Function(prod As Product) String.Format("{0}. Category {1}: {2}", _
    index, prod.CategoryID, prod.ProductName)))

In this case, the lambda expression passed to the SelectMany method accepts a reference to a specific category, working through each category in turn. This particular overload of the SelectMany method passes the index of the category along with the category to the lambda expression. Given the category and its index, the lambda expression calls the Select method on the Products property of the category, allowing it to work on each of the products within the category. The sample returns a sequence containing the following list of strings. The call to SelectMany flattened the original category/product hierarchy into a single list of values:

0. Category 6: Mishi Kobe Niku
0. Category 6: Alice Mutton
0. Category 6: Thüringer Rostbratwurst
0. Category 6: Perth Pasties
0. Category 6: Tourtière
0. Category 6: Pâté chinois
1. Category 7: Uncle Bob's Organic Dried Pears
1. Category 7: Tofu
1. Category 7: Rössle Sauerkraut
1. Category 7: Manjimup Dried Apples
1. Category 7: Longlife Tofu

Retrieving a Single Element

Often, you will need to retrieve and work with a single element from a sequence. The Enumerable class provides several methods that allow you to filter the contents of a sequence to a single element. The Enumerable.First and Enumerable.Last methods return the first or last elements in the sequence, respectively. The Enumerable.Single method returns an element given a function or lambda expression that specifies a single element. The Enumerable.ElementAt method returns a single element at a particular index within the sequence.

Each of these methods throws an exception if the requested element doesn't exist in the sequence—or, for the Single method, if the restriction results in either zero elements or multiple elements. Each of these methods also allows you to specify a lambda expression or function to restrict the input data. In any case, the resulting sequence must contain a single element.

The Enumerable class also provides similar methods that return a single element if possible and a default member of the sequence's type if not. These methods are Enumerable.FirstOrDefault, Enumerable.LastOrDefault, Enumerable.SingleOrDefault, and Enumerable.ElementAtOrDefault. Figure 2 demonstrates the behavior of these methods. Running the sample places the following data into the output stream:

ALFKI: Maria Anders
ALFKI: Maria Anders
Last in USA: Karl Jablonski
Sequence contains no elements
Specified argument was out of the range of valid values.
Parameter name: index
CustomerID = XXXXX is Nothing: True
Customer 2 in USA is Nothing: False
Customer 200 in USA is Nothing: True

Figure 2 Retrieving Elements with Enumerable Methods

' From SingleElementDemo in the sample:

Dim oneCustomer As Customer
Dim sw As New StringWriter

Dim db As New SimpleDataContext

Dim query0 = From cust In db.Customers

Dim query1 = _
  From cust In db.Customers _
  Where cust.CustomerID = "ALFKI"

Dim query2 = _
  From cust In db.Customers _
  Where cust.Country = "USA"

Dim query3 = _
  From cust In db.Customers _
  Where cust.CustomerID = "XXXXX"

' This works fine, finds the customer whose CustomerID is ALFKI:
oneCustomer = query1.Single
sw.WriteLine("ALFKI: {0}", oneCustomer.ContactName)

' You can supply an expression to filter, when calling 
' the First, Last, Single, and so on, methods:
oneCustomer = query0.Single( _
  Function(cust) cust.CustomerID = "ALFKI")
sw.WriteLine("ALFKI: {0}", oneCustomer.ContactName)

' This works fine, finds the last customer in the USA:
oneCustomer = query2.Last
sw.WriteLine("Last in USA: {0}", oneCustomer.ContactName)

' These two raise exceptions:
Try
  ' There's no customer use customer ID is "XXXXX":
  oneCustomer = query3.Single
Catch ex As Exception
  sw.WriteLine(ex.Message)
End Try

Try
  ' There aren't 200 customers in the USA:
  oneCustomer = query2.ElementAt(200)
Catch ex As Exception
  sw.WriteLine(ex.Message)
End Try

' These don't raise exceptions, but return default values
' if they fail:
oneCustomer = query3.SingleOrDefault
sw.WriteLine("CustomerID = XXXXX is Nothing: {0}", _
             oneCustomer Is Nothing)

' Because there are at least two customers in the USA,
' method returns a valid, single customer:
oneCustomer = query2.ElementAtOrDefault(2)
sw.WriteLine("Customer 2 in USA is Nothing: {0}", _
             oneCustomer Is Nothing)

oneCustomer = query2.ElementAtOrDefault(200)
sw.WriteLine("Customer 200 in USA is Nothing: {0}", _
             oneCustomer Is Nothing)

Filtering Sequences

Use the Enumerable.Where, Enumerable.Distinct, or Enumerable.OfType method to filter the contents of an existing sequence, returning a subset of the original data in an output sequence. The OfType method filters the input sequence based on a specified type. Imagine that you want to perform an operation on only a particular type of control on a form. Using the OfType method, you can restrict the collection of controls exposed by the form and iterate through only the relevant subset of controls.

The following example fills a generic List with data and retrieves an output list of integers, and then retrieves a list of strings:

' From OfTypeDemo in the sample:
Dim items As New List(Of Object)
items.Add("January")
items.Add(0)
items.Add("Monday")
items.Add(3)
items.Add(5)
items.Add("September")

Dim numbers = items.OfType(Of Integer)()
Dim strings = items.OfType(Of String)()

After running the code, the two output lists contain the following data:

0, 3, 5
January, Monday, September

The Enumerable.Where method allows you to specify a condition that filters the input sequence. A second overloaded version provides access to the index of each item in the collection so you can filter based on the index as well. The following example uses both the Where and Select methods, first filtering and then projecting the results, retrieving a sequence of files in the C:\Windows folder smaller than 100 bytes:

' From WhereDemo in the sample:
Dim files As IEnumerable(Of FileInfo) = _
  New DirectoryInfo("C:\Windows").GetFiles()
Dim fileResults = _
  files _
  .Where(Function(file) file.Length < 100) _
  .Select(Function(file) _
    String.Format("{0} ({1})", file.Name, file.Length))

Because arrays implement the IEnumerable interface, you can use the Where method to filter array contents just as with any other collection. In this case, the code filters the array of FileInfo objects returned by the GetFiles method. Also note that the example cascades calls to the Where method and then the Select method. On my computer, the sample returned the following results:

Addrfixr.ini (62)
bthservsdp.dat (12)
iltwain.ini (36)
S86D5A060.tmp (48)
setuperr.log (0)

You can use the second overload of the Where method to access the index of each input item that is processed. For example, the following Where method call returns files less than 100 bytes in size that also appear within the first 20 input files:

' From WhereDemo in the sample:
fileResults = _
  files _
  .Where(Function(file, index) _
    (file.Length < 100) And (index < 20)) _
  .Select(Function(file) _
    String.Format("{0} ({1})", file.Name, file.Length))

In this case, on my computer, the results looked like this (the missing files fell outside the first 20 files processed):

Addrfixr.ini (62)
bthservsdp.dat (12)
iltwain.ini (36)

The Enumerable.Distinct method allows you to filter a list so that the output sequence contains only the unique items from the input list. For simple values (strings, numbers, and so on), this process is easy.

For lists of complex objects, how can the runtime engine perform the comparisons to determine whether or not two objects are duplicates? It can't simply compare the instance variables, as this would, in practice, simply compare the memory addresses of the objects. Instead, for complex objects, you must supply a comparer—an instance of a class that implements IEqualityComparer(Of T) to perform the comparison. (This same issue applies to several methods of the Enumerable class, and it will show up again later in this column.)

Suppose you had a sequence containing customer information and you wanted a unique list of countries from where those customers came. If you had a simple list of countries, you could use the default comparer to compare the strings. In this case, you have a list of customers instead. (Yes, you could use the Select method to project the list so that you only had a list of countries, but then you'd lose all the remaining data for other calculations using the list.)

The sample project includes the CustomerCountryComparer class, which implements the IEqualityComparer(Of Customer) interface, as shown in Figure 3. The sample code demonstrates two different ways to use the Distinct method.

Figure 3 IEqualityComparer

Public Class CustomerCountryComparer
  Implements IEqualityComparer(Of Customer)

  Public Function Equals1( _
    ByVal x As Customer, ByVal y As Customer) As Boolean _
    Implements IEqualityComparer(Of Customer).Equals

    Return x.Country.Equals(y.Country)

  End Function

  Public Function GetHashCode1( _
    ByVal obj As Customer) As Integer _
    Implements IEqualityComparer(Of Customer).GetHashCode

    Return obj.Country.GetHashCode

  End Function
End Class

The first projects a list of customers to a list of country names first, and then uses the default string comparer to reduce the list so that it's unique. The second uses the specific CustomerCountry­Comparer class to perform the comparisons and create the unique list, and then projects the resulting distinct list of customers into a list of strings:

' From DistinctDemo in the sample:
Dim db As New SimpleDataContext

Dim countries As IEnumerable(Of String) = _
  db.Customers _
  .Select(Function(cust) cust.Country) _
  .Distinct()

countries = customers _
  .Distinct(New CustomerCountryComparer) _
  .Select(Function(cust) cust.Country)

You may need to handle empty sequences gracefully, and the Enumerable.DefaultIfEmpty method returns either the entire input sequence or a default-valued instance if the sequence is empty. Using this method, you're guaranteed that you always have at least a single item of the sequence's type.

The procedure in Figure 4 demonstrates one use for the Default­IfEmpty method. This code fills the noCustomers variable with an empty list (there are no customers whose country is "XXX"), then creates the noCustomers1 list calling the DefaultIfEmpty method. Running the sample code produces the following output:

noCustomers contains 0 element(s). noCustomers1 contains 1 element(s).
XXXXX

Figure 4 Using DefaultIfEmpty

' From DefaultIfEmptyDemo in the sample:
Dim db As New SimpleDataContext
Dim sw As New StringWriter

Dim noCustomers = _
  From cust In db.Customers Where cust.Country = "XXX"
Dim noCustomers1 = _
  noCustomers.DefaultIfEmpty()

Dim results = _
  String.Format( _
  "noCustomers contains {0} element(s). " & _
  "noCustomers1 contains {1} element(s).", _
  noCustomers.Count, noCustomers1.Count)
sw.WriteLine(results)

' You can specify the exact value to use if the 
' collection is empty:
noCustomers1 = noCustomers.DefaultIfEmpty( _
  New Customer With {.CustomerID = "XXXXX"})
For Each cust In noCustomers1
  sw.WriteLine(cust.CustomerID)
Next

noCustomers1 contains one element (a Customer with no information filled in). You can call the DefaultIfEmpty method supplying a default value for the singleton item, as in the second method call.

Ordering Sequences

You can use Enumerable.OrderBy, Enumerable.ThenBy, Enumerable.OrderByDescending, and Enumerable.ThenBy­Descending to supply an initial ordering and then one or more secondary orderings (in ascending or descending order). This sample starts with a sequence of customers and then applies ordering using several of these methods:

' From OrderByDemo in the sample:
Dim db As New SimpleDataContext
Dim customers = _
  From cust In db.Customers _
  Where cust.ContactTitle = "Owner"

' Sort first by country, then by city descending, then by name:
Dim results = _
  customers _
  .OrderBy(Function(cust) cust.Country) _
  .ThenByDescending(Function(cust As Customer) cust.City) _
  .ThenBy(Function(cust As Customer) cust.ContactName) _
  .Select(Function(cust) String.Format("({0}, {1}) {2}", _
  cust.Country, cust.City, cust.ContactName))

Note that Visual Basic won't complain if you don't specify the data types for the parameter to the lambda expression in the ThenBy and ThenByDescending method calls, but if you don't, you won't get any IntelliSense® to help you. Only the first ordering method can infer the data type of the lambda expression parameter. Oddly enough, the Select method correctly infers the type, even at the end of the long chained list of methods. The sample sorts the customers whose title is "Owner" first by country, then descending by city, and, finally, by contact name within each city:

(Denmark, Kobenhavn) Jytte Petersen
(France, Paris) Marie Bertrand
(France, Nantes) Janine Labrune
(France, Marseille) Laurence Lebihan
(Germany, Köln) Henriette Pfalzheim
(Mexico, México D.F.) Ana Trujillo
(Mexico, México D.F.) Antonio Moreno
(Mexico, México D.F.) Miguel Angel Paolino
(Norway, Stavern) Jonas Bergulfsen
(Poland, Warszawa) Zbyszek Piestrzeniewicz
...

Verifying Sequences

Some applications require you to verify that specific conditions are met within a sequence. Do any of the elements meet a criterion? Do all the elements meet a criterion? Does a particular item appear within the sequence? Are two sequences equal? The Enumerable class provides methods that provide all this information.

To determine whether a sequence contains any elements at all, call the Enumerable.Any method without any parameters. To supply a condition and to determine whether any elements in the sequence match the condition, pass a function to the method. The method returns True if any elements of the input sequence exist or match the supplied condition (see Figure 5).

Figure 5 Finding a Sequence

' From AnyDemo in sample:
Dim db As New SimpleDataContext
Dim results = _
  From product In db.Products _
  Where product.CategoryID = 1

' Determine if a list has any elements:
Dim anyElements = results.Any()

' Call as extension method or shared method
' of IEnumerable:
Dim matchingElements = _
  results.Any(Function(prod) prod.ProductName.StartsWith("M"))
matchingElements = _
  Enumerable.Any(results, _
  Function(prod As Product) prod.ProductName.StartsWith("M"))

To determine whether all members of a sequence meet a criterion, call the Enumerable.All method, supplying a function that specifies the condition. This example determines if all the elements in the results collection have a UnitsInStock value greater than 5:

' From AllDemo in sample:
' Is it true that all products have more than 5 units in stock?
Dim db As New SimpleDataContext
Dim results = _
  From product In db.Products _
  Where product.CategoryID = 1
Dim allElements = _
  results.All(Function(prod) CBool(prod.UnitsInStock > 5))

To determine whether a sequence contains a particular element, call Enumerable.Contains. If you're searching for a simple value, like a string or an integer, you can use the default comparer, and you only need to supply the value you're seeking. If you're attempting to determine whether a more complex object exists within a sequence, you must supply an instance of a class that implements IEqualityComparer(Of T).

The sample code in Figure 6 demonstrates both calls to Enumerable.Contains. The first example simply determines whether the number 5 appears within a list; the second determines whether an element corresponding to Chai appears within the list of products (this example uses the same ProductComparer class you saw earlier in the column).

Figure 6 Finding Items in a Sequence

' From ContainsDemo in the sample:
' Simple check to see if list contains a value:
Dim numbers = Enumerable.Range(1, 10)
Dim contains5 = numbers.Contains(5)

' More complex objects require more complex comparison:
Dim db As New SimpleDataContext
Dim results = _
  From product In db.Products _
  Where product.CategoryID = 1

Dim item As New Product _
  With {.ProductID = 1, .ProductName = "Chai"}
Dim containsChai = results.Contains(item, New ProductComparer())

To determine whether two sequences are equal, call the Enumerable.SequenceEqual method. Just like the Enumerable.Contains method, you can either compare two sequences of simple values using the default comparer or two sequences of more complex objects using a custom comparer.

If the two sequences don't contain the same types or are of different lengths, the comparison fails immediately. If they contain the same type and are of the same length, the Enumerable.SequenceEqual method compares each item using the specified comparer. Figure 7 demonstrates both ways of calling the Enumerable.SequenceEqual method. In both cases, the return value is False.

Figure 7 Comparing Sequences

' From SequenceEqualDemo in sample:
Dim rnd As New Random
Dim start = rnd.Next

Dim count = 10
Dim s1 = Enumerable.Range(start, count)

' Choose a different random starting point:
start = rnd.Next
Dim s2 = Enumerable.Range(start, count)
Dim sequencesEqual = s1.SequenceEqual(s2)
sw.WriteLine("sequencesEqual = {0}", sequencesEqual)

' What if there's no default comparer? Must use your own:
Dim products1 = _
  From prod In db.Products _
  Where prod.CategoryID = 1
Dim products2 = _
  From prod In products1 _
  Where prod.UnitPrice > 5

sequencesEqual = _
  products1.SequenceEqual(products2, New ProductComparer())
sw.WriteLine("sequencesEqual = {0}", sequencesEqual)

In my next column, I'm going to focus on the remaining methods of the Enumerable class, including some fun methods that perform useful set-based operations. Until then, you can download the sample application and give the Enumerable class's methods a try. They'll certainly enrich your work with data structures in Visual Studio® 2008.

Send your questions and comments for Ken to basics@microsoft.com.

Ken Getz is a Senior Consultant with MCW Technologies and a courseware author for AppDev (www.appdev.com). He is coauthor of ASP .NET Developers Jumpstart, Access Developer's Handbook, and VBA Developer's Handbook, 2nd Edition. Reach him at keng@mcwtech.com.