lozst-link3

C# in Unity3D – Linq Queries and the Death of the For Loop

This is the second post in my series on advanced C# language features, and using them in Unity3D. If you want to read my last post on Lambda expressions, you can do so here. It’s worth checking out, because I’ll build on those concepts in this post.

So you have a collection. A list. Of stuff.

And you need to get something out of that list…

Of stuff.

Woah, before you start typing your ‘foreach’, or heaven forbid… for… let me introduce you to my friend, Linq (pronounced Link)

Linq is a querying language, much like SQL, but you can use it on collections in C#. It’s also well supported in Unity3D, and once you start using it, you’ll never want to loop through anything again… well… most of the time.

Before you do anything with Linq, you’ll want to include the Linq namespace. This will pull in a bunch of extension methods, and allow you to start writing Linq expressions. At the top of your C# script, simply add:

using System.Linq;

So, what can we do with it? Well, let’s look at something I did last week, which was get all of the even numbers out of a list of integers. If we wanted to loop through the collection, we’d do something like this.

void doBoringOldLoop()
{
   int[] numbers = new int[] { 1,2,3,4,5,6,7,8,9 };
   var evens = new List<int>();

   for (int i = 0; i < numbers.Length; i++)
   {
      if (numbers[i] % 2 == 0) evens.Add(numbers[i]);
   }
}

Pretty standard stuff, but now, let’s see the same code using Linq

void doFancyLinq()
{
   int[] numbers = new int[] { 1,2,3,4,5,6,7,8,9 };

   var evens = 
      from n in numbers
      where (n % 2) == 0
      select n;

   foreach (var number in evens) 
      { Debug.Log("The even number " + number); }
}

The expression in cleaner, and more concise, than the crusty old for loop. It’s also worth noting that this doesn’t only work on arrays of integers, it will work on any collection, including List<string>, Vector[], List<GameObject>… anything really, even a Dictionary<int, MyClass>.

A linq expression can take this format

from value in collection
where condition
select value

So instead of focusing on implementation of code, you’re focusing on what you want. It makes a big difference in how you think about getting data. But you can do more than just provide a conditional, let’s say you want to order a list of swords, in descending order of price – let’s take this a step further and add an “order by”.

Something important to note here is that OrderBy doesn’t play nice with iOS. Ordering Value Types, such as int,  is not supported – but you can sort a class. So, instead of a collection of numbers, let’s have a collection of ShopItems.

public class ShopItem {
   public int cost;
   public string name; 
   public ItemCategoryEnum category;
}

void doOrderedLinq()
{
   ShopItem[] items = new ShopItem[] { /* Our Items */ };

   var swords = 
      from item in items
      where item.category == ItemCategoryEnum.Sword
      orderby item.cost descending
      select item;

   foreach (var sword in swords) 
      { Debug.Log(sword.name + " costs " + sword.cost + "G"); }
}

That’s really it! If you wanted to do that by looping through it manually, it would be a lot of boring (and error prone) code. This is a quick and easy way to order. If you wanted to get the swords in ascending order, simply remove the “descending”, and it will default to an ascending sort. Finally, let’s say we want to just get the most expensive sword, the Linq namespace also pulls in a number of extension methods that let you quickly pull what you want out of the results.

Let’s say we want to get the highest even number in a collection – we can use the FirstOrDefault() extension method.

ShopItem getMostExpensiveItem(IEnumerable<ShopItem> items, 
                              ItemCategoryEnum type)
{
   var swords = 
      from item in items
      where item.category == type
      orderby item.cost descending
      select item;

   return swords.FirstOrDefault();
}

There are a different methods available for pulling data out of your results – Last, LastOrDefault, First, FirstOrDefault are just a few. You might be wondering why I used FirstOrDefault instead of just First, and that’s because FirstOrDefault is safer. In the case of an empty collection, if you call First, an exception will be thrown. If we use FirstOrDefault, then a default value will be returned – null if it’s an object (say, a GameObject), or the default value if it’s a Value Type (say, an int). The Last methods operation much the same way at First, but obviously take the last element in the result.

Let’s have a quick look at how we could use this in the context of Unity3D. A good example is managing a pool of GameObjects. Let’s look at a simple MonoBehaviour for managing your pool.

public class PoolManager : MonoBehaviour {

  public GameObject pooledObjectPrefab;
  private List<GameObject> objectPool;  
  public int poolSize;

  void Start()
  {
    objectPool = new List<GameObject>();

    for (int i = 0; i < poolSize; i++)
    {
       var newObject = 
         GameObject.Instantiate(pooledObjectPrefab) as GameObject;
       newObject.SetActiveRecursively(false);
       objectPool.Add(newObject);
    }
  }

  public GameObject GetInactiveInstance()
  {
    var availableGameObjects = 
      from o in objectPool
      where !o.active
      select o;

    var gameObject = availableGameObjects.FirstOrDefault();

    if (gameObject == null) { 
      //Nothing is available, expand the pool
      gameObject = 
        GameObject.Instantiate(pooledObjectPrefab) as GameObject;
      objectPool.Add(gameObject);
    } 

    gameObject.SetActiveRecursively(true);
    return gameObject;
  }
}

And that’s about it, a very simple GameObject pool using Linq. When an object is ready to be returned to the pool, whoever owns it can just SetActiveRecursively(false), and it will be available again for selection from the pool.

I’ll just quickly tie this back to the last post I did on Lambda. There are a number of methods you can run on collections that will take Lambda expressions for quickly getting access to values from a collection. Basically, Where and OrderBy can both be used without writing a Linq expression, but using Lambda instead. Here are a number are some examples of querying data using the extension methods in the Linq namespace.

var evenNumbers = numbers.Where(n => n % 2 == 0);

//this will not work on iOS
var evenOrderedNumbers = numbers.Where(n => n % 2 == 0)
                                .OrderByDescending(n => n);

//this is fine on iOS
var availableGameObject = objectPool.Where(go => !go.active)
                                    .FirstOrDefault();
var availableGameObject = objectPool
                            .FirstOrDefault(go => !go.active);

These are quick and easy ways to pull data out of collections, and to be honest, easier to write for simple queries, but wanted to demonstrate how build Linq queries. Another couple of methods available are Skip() and Take(), which are great for pagination.

Say you have a list of items for sale in a store, and you want to let the user paginate through it. With these extension methods, you can very simply implement pagination.

var pageSize = 10;
var currentPageNumber = 2;
var pageItems = storeItems
   .Where(item => item.category == ItemCategoryEnum.Swords)
   .OrderBy(item => item.cost)
   .Skip(pageSize * currentPage)
   .Take(pageSize);

So there you have it, a very powerful tool for getting useful information out of collections. I’ve only touched on what you can do with Linq, but there are some great examples at Linq queries 101 Linq Samples.

I’ve written some pretty complicated queries using Linq, and only hit a few snags in Unity3D. You may run into some issues on iOS because some extension methods, like Min() or Max() for comparable types, don’t work. You can get around that though by implementing your own Min() method, so it’s not a deal breaker.

You don’t want to go too crazy on these things, you will be generating garbage while doing this, so you probably don’t want to do it every Update(). For less frequent queries though, it’s great. Once you start thinking in terms of what you want rather than how to code it you’ll never go back.

[EDIT - This has had some modifications from the original post. Big thanks to Alistair from Bane Games for helping out]