Unless you live in a hole far away from anything Microsoft,
you must have heard of LINQ. LINQ stands for Language Integrated Query and it
consists of two parts, which I’ll refer to as the icing and the cake. The icing
is a set of language extensions to VB and C# to allow programmers to very
simply write powerful, declarative query expressions. The cake is a set of
libraries containing static (*shared) methods that implement the actual
querying to arbitrary collections of data, such as arrays, lists, datasets, or
databases. To see LINQ in action, icing and cake, you can see Anders Hejlsberg,
the brain behind C#, talk about LINQ in C#.
Although the icing is very good and makes the cake test even
better, it’s not necessary for savoring the cake. ASNA Visual RPG (AVR) lacks
the icing—the built-in language extensions to use LINQ directly like VB and C#
have. However, despite the lack of the built-in language extensions, with a
little knowledge of the way these extensions exploit the .NET Framework and
LINQ-related classes, you can put
LINQ to work with AVR. (The code in this article was tested on both AVR 9.0 and
AVR 9.1—because of .NET 3.5 dependencies, it will not work with any version of
AVR earlier than AVR 9.0)
Here are some examples on how to use the LINQ libraries in
AVR. It requires some grunt work as we need to explicitly call those library
methods, but the payback can be big.
We start by creating a console application project that uses
framework 3.5, required for LINQ. Be sure your using statements include LINQ
and the Generic collections:
Using System
Using System.Collections.Generic
Using System.Text
Using
System.Linq
Now in Main declare an array of strings which we’ll use for
the examples (idea shamelessly stolen from Joseph C. Ratz Pro LINQ
book):
BegSr Main Shared(*Yes) Access(*Public) Attributes(System.STAThread())
DclSrParm
args Type(*String)
Rank(1)
dclarray
pres type(*string)
rank(1)
pres = *new
*string[] { "Adams",
"Lincoln", "Roosevelt", "Jefferson", +
"Bush", "Jackson", "Obama", "Reagan",
"Clinton", "Johnson", +
"Adams", "Roosevelt", "Bush" }
We will be using static (*shared) methods contained in the System.Linq.Enumerable
class
so we’ll enclose our calls to them in a WITH statement:
With Enumerable
...
Endwith
We are ready to take a look at some of the methods. We start
with the Where method which filters a collection by applying a predicate, that
is, a Boolean function, to each of its elements and generates a new IEnumerable
collection containing the filtered elements. So let’s take our presidents and
get only those whose name starts with “J”. We need to define the predicate to
pass to Where as a *shared class member:
begfunc filter type(*boolean) shared(*yes)
dclsrparm
arg *string
leavesr
arg.StartsWith( "J" )
endfunc
and add this code inside the With
dclfld filteredNames type(IEnumerable(*of *string))
Console.WriteLine( ">> Filter names that
start with 'J'")
filteredNames = .Where(*of *string)(pres, *new
Func(*of *string,
*boolean)+
( filter ))
foreach Name(s) type(*string) Collection(filteredNames)
Console.WriteLine( s )
endfor
A couple of observations, Where is a generic method so we
need to qualify it with the type of the elements in the collection we are
filtering. Func is a generic delegate in the System namespace that requires the
types of the arguments and results.
If you want to make it more general and filter names that
start with arbitrary strings then you could define a *shared field that will
hold the prefix for the filter and set it before the Where call:
dclfld startString *string
shared(*yes)
begfunc filter type(*boolean) shared(*yes)
dclsrparm
arg *string
leavesr
arg.StartsWith( startString )
endfunc
We can combine method calls and pass as the collection
argument the result of a different method call. The next example shows Select,
which creates a collection with the values that result after applying a
function to each element. We will take our list of “J” presidents and prepend
“Mr and Mrs “ to each president name. We start with declaring the function to
apply as a *shared class member:
begfunc address type(*string) shared(*yes)
dclsrparm
arg *string
leavesr
"Mr and Mrs " + arg
endfunc
and in the With body we add:
Console.WriteLine( ">> Prepend 'Mr and
Mrs' to the previous list")
filteredNames = .Select(*of
*string, *string)(
+
.Where(*of
*string)(pres, *new
Func(*of *string,
*boolean)(filter)),+
*new
Func(*of *string,
*string)( address ) )
foreach Name(s) type(*string) Collection(filteredNames)
Console.WriteLine( s )
endfor
The first argument to Select is the result of the Where
call, and the second argument is the Func that will be applied to each element
of the Where result.
Now let’s sort the presidents’ names according to their
length. For that we use the OrderBy method which uses as ordering key what
results of applying a function to each element of the collection, in our case
the length of the name:
begfunc getLength type(*integer4) shared(*yes)
dclsrparm
arg *string
leavesr
arg.Length
endfunc
And in the With we add:
Console.WriteLine( ">> Sort names by
length")
filteredNames = .OrderBy(*of
*string, *integer4)(pres,
+
*new
Func(*of *string,
*integer4)( getLength ))
foreach Name(s) type(*string) Collection(filteredNames)
Console.WriteLine( s )
endfor
Finally, let’s show the use of GroupBy, where we’ll
create a collection on the names grouped by name length. The result of GroupBy
is an IEnumerable(*of IGrouping(…)) so we need to define the result field:
dclfld
groupedNames type(IEnumerable(*of IGrouping(*of *integer4,+
*string)))
Console.WriteLine( ">> Group them by name
length")
groupedNames = .GroupBy(*of
*string, *integer4)(
pres, +
*new
Func(*of *string,
*integer4)( getLength ))
Each element of the result is a grouping. To iterate over
the collection and print the grouping we use:
foreach Name(g) type(IGrouping(*of *integer4, *string))
+
Collection(groupedNames)
Console.WriteLine( "Length =
{0}", g.key )
foreach
name(n) type(*string) Collection(g)
Console.WriteLine( n )
endfor
endfor
You can use GroupBy to remove the duplicates by passing
as the grouping selector a function that just returns its argument:
begfunc identity type(*string) shared(*yes)
dclsrparm
arg *string
leavesr
arg
endfunc
A very important feature of the LINQ methods is that they
use lazy evaluation, that is, the resulting collections are evaluated only when
you iterate over the collection. Thus their performance is ‘amortized’ in the
foreach loop. You can read more about LINQ‘s lazy evaluation here.
I hope you get a gist of the power these methods have and
not feel like it’s a daunting task to use them. Try the cake even without the icing,
I’m sure you’ll like it!
[photo credit: SXC]