LINQ is syntactical sugar for extension methods and lambda expressions. To understand LINQ, it is important to first grapple with these concepts.
To begin, let's look at a simple and somewhat common scenario. We have an unsorted list of names. We want to go through each letter of the alphabet and print an alphabetized list of names for the current letter. Here's a typical approach.
Module Module1
Sub Main()
Dim alphabet As String() = New String() _
{"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", _
"K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", _
"U", "V", "W", "X", "Y", "Z"}
Dim names As String() = New String() _
{"Adam", "Dave", "John", "Alex", "Daryll", "Jacob", _
"Christopher", "Bill", "Ronald", "Jeff"}
For Each letter In alphabet
Dim selected_names As New List(Of String)
For Each name As String In names
If name.StartsWith(letter) Then selected_names.Add(name)
Next
If selected_names.Count > 0 Then
selected_names.Sort(AddressOf SortNamesMethod)
Console.WriteLine("Names beginning with '" & letter & "'")
For Each name As String In selected_names
Console.WriteLine(name)
Next
Console.WriteLine("")
End If
Next
Console.Read()
End Sub
Private Function SortNamesMethod(ByVal name1 As String, _
ByVal name2 As String) As Integer
Return name1.CompareTo(name2)
End Function
End Module
While this approach certainly accomplishes the job, you will see in a moment how LINQ can make the line count smaller, the program flow more logical and the code easier to maintain.
Extension Methods
Extension methods allow programmers to add useful methods to existing types. In our example, we will be adding an extension method to the IEnumerable(Of String)
type. The extension method will print the list of strings to the console, as well as the list title.
In order to create an extension method, we will need to import the System.Runtime.CompilerServices
namespace.
Imports System.Runtime.CompilerServices
Next we will need to create a new subroutine. The sub
will be named ToConsole
and will take two parameters. The first parameter will define the type to which this method is being added, and the second parameter will be the title that we want to give our list. Finally, we will need to add the Extension
attribute to the function to alert the compiler that this method is an extension method.
<Extension()> _
Private Sub ToConsole(ByVal items As IEnumerable(Of String), _
ByVal title As String)
End Sub
Now we need to add the functionality. We will borrow it from the Main
function above:
<Extension()> _
Private Sub ToConsole(ByVal items As IEnumerable(Of String), _
ByVal title As String)
If items IsNot Nothing Then
Console.WriteLine(title)
For Each item In items
Console.WriteLine(item)
Next
Console.WriteLine("")
End If
End Sub
We can use this method on any object that implements the IEnumerable(Of String)
interface. In our example, the selected_names
variable in the Main
method implements this interface because the List(Of String)
implements IEnumerable(Of String)
. We can modify our code to look like this:
Imports System.Runtime.CompilerServices
Module Module1
Sub Main()
Dim alphabet As String() = New String() _
{"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", _
"K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", _
"U", "V", "W", "X", "Y", "Z"}
Dim names As String() = New String() _
{"Adam", "Dave", "John", "Alex", "Daryll", "Jacob", _
"Christopher", "Bill", "Ronald", "Jeff"}
For Each letter In alphabet
Dim selected_names As New List(Of String)
For Each name As String In names
If name.StartsWith(letter) Then selected_names.Add(name)
Next
If selected_names.Count > 0 Then
selected_names.Sort(AddressOf SortNamesMethod)
selected_names.ToConsole("Names beginning with '" & letter & "'")
End If
Next
Console.Read()
End Sub
Private Function SortNamesMethod(ByVal name1 As String, _
ByVal name2 As String) As Integer
Return name1.CompareTo(name2)
End Function
<extension()> _
Private Sub ToConsole(ByVal items As IEnumerable(Of String), _
ByVal title As String)
If items IsNot Nothing Then
Console.WriteLine(title)
For Each item In items
Console.WriteLine(item)
Next
Console.WriteLine("")
End If
End Sub
End Module
Typically an extension method is most useful in cases where you'd want that method in more than one place. Our example is small and this is not really necessary, but the exercise will help when dealing with some of the built-in extension methods provided for LINQ.
Lambda Expression
Next, let's get rid of the SortNamesMethod
. In a small application like this one, defining a function that is used only once is not a problem, but in a very large application, these kinds of extra functions get to be annoying and confusing. We will use a simple lambda expression instead of the function.
The List(Of String).Sort()
function accepts a reference to a function as a parameter. The function doesn't care where the function exists or how it was declared as long as the function takes two string
s as parameters and returns an integer
indicating if the first string
is greater than, equal to or less than the second string
. We can replace the AddressOf SortNamesMethod
with the lambda expression.
Function(name1 As String, name2 As String) name1.CompareTo(name2)
The compiler reads this lambda expression and creates a function for us. The lambda expression explicitly declares its parameters and the compiler is able to detect that the return type is boolean (because name1.CompareTo(name2)
returns a boolean). Once the compiler has created the function, it replaces the lambda expression with the address of the compiler-created function. The resulting binary code is pretty much the same, but the benefit is that I no longer need to deal with that extra function floating around in my code.
Getting closer to LINQ
Now that we have seen how to create an extension method and use a lambda expression, let's look at some of the built-in extension methods available to us.
Let's begin with the Where
method. For any IEnumberable(Of T)
, the Where
method will return an IEnumerable(Of T)
where all items in the list match the predicate provided as a parameter. That's a little confusing, so let's looks at an example. We will be replacing the for
loop which filters the names list by the first letter with the Where
extension method.
Dim selected_names As New List(Of String)
For Each name As String In names
If name.StartsWith(letter) Then selected_names.Add(name)
Next
becomes:
Dim selected_names As New List(Of String)
selected_names = names.Where(Function(name As String) name.StartsWith(letter)).ToList()
We've replaced the for
-loop with an extension method. For the predicate parameter, we've used a lambda expression that accepts a String
parameter and returns a boolean. The lambda is transformed into a function at compile time, and the Where
extension method applies the function to each element in our name list at run time. The Where
extension method then returns an IEnumerable(Of String)
containing all items in our name list where the result of the predicate (StartsWith(letter)
) is true
. Then we use the ToList()
extension method to transform the IEnumerable(Of String)
into an IList(Of String)
so that we can assign the result back to the selected_names
variable.
We can take this a step further by sorting the list as we filter it. Instead of applying the ToList
extension method to the result of the Where
extension method, let's apply to OrderBy
extension method. The OrderBy
extension method takes a name from our list
as a parameter and returns some key by which to sort the list. In this case, we want to order the list by the name, so all we need to do is return the name that we passed in as a parameter.
selected_names = names.Where(Function(name As String) name.StartsWith(letter)).ToList()
becomes:
selected_names = names.Where(Function(name As String) _
name.StartsWith(letter)).OrderBy(Function(name As String) name).ToList()
The sort
function later in the code is no longer necessary. Our complete code now looks like:
Imports System.Runtime.CompilerServices
Module Module1
Sub Main()
Dim alphabet As String() = New String() _
{"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", _
"K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", _
"U", "V", "W", "X", "Y", "Z"}
Dim names As String() = New String() _
{"Adam", "Dave", "John", "Alex", "Daryll", "Jacob", _
"Christopher", "Bill", "Ronald", "Jeff"}
For Each letter In alphabet
Dim selected_names As New List(Of String)
selected_names = names.Where(Function(name As String) _
name.StartsWith(letter)).OrderBy(Function(name As String) name).ToList()
If selected_names.Count > 0 Then
selected_names.ToConsole("Names beginning with '" & letter & "'")
End If
Next
Console.Read()
End Sub
<extension()> _
Private Sub ToConsole(ByVal items As IEnumerable(Of String), _
ByVal title As String)
If items IsNot Nothing Then
Console.WriteLine(title)
For Each item In items
Console.WriteLine(item)
Next
Console.WriteLine("")
End If
End Sub
End Module
LINQ
Now that we've covered extension methods and lambda expressions, we can convert our selected_names
filter into a LINQ statement.
selected_names = names.Where(Function(name As String) _
name.StartsWith(letter)).OrderBy(Function(name As String) name).ToList()
becomes:
selected_names = (From name In names _
Where name.StartsWith(letter) _
Order By name).ToList()
This is the exact same statement written two ways. As you can see, LINQ is syntactic sugar to make our extension methods look prettier. Not all extension methods can be replaced with LINQ, which is why we must still call the ToList()
extension method as we did before. We must also wrap the LINQ in commas so that it is applied to the result of the entire LINQ statement instead of the name variable.
In addition to what we have seen, there is the Select
extension method. This method accepts as a parameter a name and returns whatever object you want to return. The result of applying this extension method to a list is a new list of whatever type you choose to return. LINQ even allows us to return an anonymous type (a new type inferred from the expression with whatever properties we specify). Using the Select
extension method in LINQ, we can transform our code to look like this:
Imports System.Runtime.CompilerServices
Module Module1
Sub Main()
Dim alphabet As String() = New String() _
{"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", _
"K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", _
"U", "V", "W", "X", "Y", "Z"}
Dim names As String() = New String() _
{"Adam", "Dave", "John", "Alex", "Daryll", "Jacob", _
"Christopher", "Bill", "Ronald", "Jeff"}
Dim rolodex = From list In (From letter In alphabet _
Select Letter = letter, Entries = _
(From name In names _
Where name.StartsWith(letter))) _
Where list.Entries.Count() > 0
For Each page In rolodex
page.Entries.ToConsole("Names beginning with '" & page.Letter & "'")
Next
Console.Read()
End Sub
<extension()> _
Private Sub ToConsole(ByVal items As IEnumerable(Of String), _
ByVal title As String)
If items IsNot Nothing Then
Console.WriteLine(title)
For Each item In items
Console.WriteLine(item)
Next
Console.WriteLine("")
End If
End Sub
End Module
This creates a list of some anonymous type where the Names
property of the anonymous type has more than zero items in it. We then take this list and apply the ToConsole
extension method to the Names
property of each item and pass in a title using the Letter
property.
With fewer lines, this code is easier to read and more durable as there are not as many lines of code to break. As you can see, we have also made use of VB's ability to infer the type of a variable from the assignment expression. Our rolodex variable is strongly typed, but since we used an anonymous type, there is no way to declare the type in a typical dim
statement. By allowing VB to infer the type, we can sidestep this requirement and still have the Letter
and Entries
properties appear in the intellisense list (along with all of the advantages of compile-time type checking).
View on CodeProject