LINQ – a game development focused introduction
I was recently asked for some pointers on how to get started with LINQ – and to maybe write a post about that. Using LINQ virtually every day I have to admit that it had not occurred to me that a C# programmer may not be familiar with it.
LINQ is a big topic, but this post is the first in a series to introduce the framework and its many uses – all from a game developer’s point of view.
Our approach
LINQ – or Language Integrated Query – (usually pronounced “link”) is a C# framework introduced with the .NET Framework 3.5.
Being a complex topic, there are many possible descriptions for what LINQ is. Many writers will emphasize how it is a query language that allows to query a multitude of different databases, and database-like storages. It provides an abstraction that allows the user to do this without any knowledge of underlying query languages like SQL and obtain type-safe results even from un-typed data.
This is all true, and these applications of LINQ are very valid and useful. However, as a game developer, this is not what I want to focus on.
Instead we will focus on LINQ to Objects, the part of the framework dealing with in-managed-memory collections only. I will only refer to Linq to Objects for the remainder of the post, so that we can call it simply LINQ for brevity.
Overview
Depending on your background and experience, as well as the problems you are trying to solve, it may be useful to think of LINQ in a number of different ways. We can think of it as a query language inside our object oriented C#, or we can emphasize the framework’s functional approach. Or we can simply say that LINQ offers a handy and unified way of working with collections of almost any type.
Extension methods
Most of LINQ is implemented using extension methods and can be called on interfaces, which means that LINQ can be used not only with the collections provided by .Net, but also any custom types that implement the same collections.
IEnumerable
The core type of the framework is IEnumerable<T>
which represents a read-only sequence of an arbitrary type. As such almost all LINQ methods require such a sequence as input. We may look at the few exceptions in a future post.
LINQ provides basic functions for virtually all operations on sequences we may be interested in. This includes mapping, filtering, folding, sorting, grouping, and more. It also provides more specific implementations (especially of folding) that can come in handy in many circumstances.
Deferred execution
An interesting – and potentially very useful – aspect of LINQ is how most of its methods are implemented using deferred execution. This means that the methods will not start enumerating their input collections, until the user starts enumerating the output collection.
In fact, many methods will only enumerate as much of the input as is needed to construct the output at the point it is consumed, and forget about any previous data. This means that algorithms often use a constant amount of space despite seemingly having to store intermediate collections between operations.
Next to lower memory usage, another advantage of deferred execution is its laziness. If – while enumerating our results – we find out that we really only cared about the first few elements, we can simply discard the LINQ query, which may never have to look at the entire input.
Examples
There is arguably no better way to learn than by examples, so let us give a few simple ones that show some of the things we can use LINQ for.
I will introduce two of the most important functions of the framework, and show how enumerating a collection would look with and without them.
Note that this is by no means an exhaustive list – in fact not even close. In addition, I will gloss over a number of subtleties in the name of simplicity. Suffice it to say, we will cover these (important) details later on, or in future post.
Where()
When working with collections, it is very common that we want to enumerate a list, but only care about a specific subset of items.
Without using LINQ, we could write code that looks for example like this:
foreach (var item in list)
{
if (item.Property != BadValue)
{
/* .. */
}
}
// or equivalently:
foreach (var item in list)
{
if (item.Property == BadValue)
continue;
/* .. */
}
Using LINQ and its Where()
method, we can get the same effect as follows:
foreach (var item in list.Where(i => i.Property != BadValue))
{
/* .. */
}
It may not be immediately clear why this approach would be beneficial, especially if the approach with LINQ is unfamiliar.
Apart from often being slightly less verbose, I consider the main advantage of using LINQ in a case like this how it clears up the loop body to only contain the actual code dealing with enumerated elements.
Select()
Something else that occurs frequently is that we do not actually care about the content of our collection, but instead would like to map each element to new value – pulling it through a function in a manner of speaking – and then enumerate this new value.
Without LINQ, we could look as follows:
foreach (var item in list)
{
var value = item.GetValue();
/* .. */
}
Using LINQ and Select()
we can instead write:
foreach (var value in list.Select(i => i.GetValue()))
{
/* .. */
}
Again this change clears up our loop body, but in this case it removes the variable item
which we needed before. That is good design since it means that we cannot accidentally use the variable, even though we did not mean to.
Lambda functions and delegates
As you already saw above, many LINQ methods require functions as parameter. These functions (delegates in C#) can be specified in a number of different ways. Above we used in-line lambda/anonymous functions. However, we could also provide method groups, or use the generic Func<>
delegates of C#.
For example:
// using a method group:
int square(int i)
{
return i * i;
}
foreach (var s in numbers.Select(this.square))
{
/* .. */
}
// using Func<>:
Func<int, int> square = i => i * i;
foreach (var s in numbers.Select(square))
{
/* .. */
}
These higher order functions (functions that take other functions as parameters) allow to approach problems from a functional programming perspective – directly within C#, which can often result in concise and yet clear code when using LINQ.
Queries vs. data
Above we only used LINQ methods inside foreach
loops. Of course this is not necessary.
We could for example write the following code:
var squares = numbers.Select(square);
foreach (var s in squares)
{
/* .. */
}
The interesting part of this code is the type of the variable squares
: It is an IEnumerable<int>
, a sequence of integers – no matter the type of the input collection.
Most importantly, the call to Select()
does not in fact read any data from the input collection. That means that any changes to numbers
before the loop will change the enumerated values.
In fact, we could enumerate the IEnumerable<int> squares
multiple times and each time we would see any changes to the input collection reflected in the output.
For example:
var numbers = new List<int>();
numbers.Add(1);
var squares = numbers.Select(square);
numbers.Add(2);
numbers.Add(3);
foreach (var s in squares)
{
Console.WriteLine(s);
}
// writes:
// 1
// 4
// 9
numbers.Add(10);
foreach (var s in squares)
{
Console.WriteLine(s);
}
// writes:
// 1
// 4
// 9
// 100
The reason for this is that the IEnumerable<int>
returned by Select()
– and all other deferred LINQ methods – does not represent a value, or even a list of values.
Instead it represents a query on a collection.
If that collection changes, naturally the output of the query will also change.
Conclusion
When it comes to game development, LINQ to Objects is the most important part of the LINQ framework.
This post has given a small taste and introduction to what can be done with LINQ.
I hope you found this useful, and be sure to stay tuned for upcoming posts delving further into all things LINQ.
If there are any particular aspects of the framework you are especially interested, let me know, and I will get to them as soon as I can.
Enjoy the pixels!
Reference: | LINQ – a game development focused introduction from our NCG partner Paul Scharf at the GameDev<T> blog. |