Part 2 of our adventures with Linq! In this series we are exploring how Linq queries can help us manage our lists and write tighter code. This is a beginner’s start-up guide, designed to show basic (yet powerful) techniques for how to use Linq, as well as common pitfalls to avoid. This part will cover handling IEnumerable collections and grabbing specific list items by a number of different criteria.
Welcome back! We’re going to pick up where we left off in Part 1, so if you haven’t had a chance to read that yet, you can do so right here.
Working With Linq Output
When last we met, we had used a Linq query to find any number greater than two in our “myNumbers” array. Now we have our… var… myBigNumbers, and we want to look at our first number. So we do this:
print( myBigNumbers[0] );
Uh oh. A wrinkle.
When we try to compile this code in Unity, we see this error:
error CS0021 Cannot apply indexing with [] to an expression of type `System.Collections.Generic.IEnumerable ‘
So the “var” that is myBigNumbers is actually an IEnumerable. And we also learned that IEnumerable’s don’t like being referenced with an index. Another thing to know about IEnumerables is that they don’t have a .Count property, so we can’t easily see how many items they hold. In short, treating them like Lists will only lead to disappointment.
They still can be iterated through with a foreach loop, but that kind of puts us right back where we were before, in terms of code space. So we can turn large, indexable Lists (capital “L”) into more specific, NON-indexable lists (little “L” – they are actually IEnumerables). That can be useful when we plan to iterate through a subset of list items, but what if we just want to grab one, specific object? That’s where .Single comes in.
For this example we’re gonna use something a little more complex than integers. Let’s make an enemy object an put ’em in a list.
public class Enemy{ public string name; public int health; public Enemy( string _name, int _health ){ name = _name; health = _health; } } public class EnemyController : MonoBehaviour{ // Variables List allEnemies = new List(); Start(){ allEnemies.Add( new Enemy("Kim", 35) ); allEnemies.Add( new Enemy("Kris", 10) ); allEnemies.Add( new Enemy("Justin", 18) ); allEnemies.Add( new Enemy("Kanye", 40) ); } }
OK now based on what we did last time, lets say we just want to reference our “Justin” enemy. Using the last pattern, we would do this:
var oneTrueEvil = allEnemies.Where( x => x.name == "Justin" );
This will give us an IEnumerable collection with a single object in it, the one named “Justin.” But we still have a problem, and it’s a big one – we can’t reference any Enemy-specific logic or members with this var. For example, if there was an enemy method called Enemy.AttackFans(), we wouldn’t be able to call “oneTrueEvil.AttackFans().” Even with one object in it, oneTrueEvil is still a collection type, not an Enemy. And we can’t say “oneTrueEvil[0].AttackFans()” because it doesn’t support indexing!
It seems our only recourse is the goddamn foreach loop – such a waste of time that I’m not going to write a sample for it. Let’s just skip to .Single()
Enemy oneTrueEvil = allEnemies.Where( x => x.name == "Justin" ).Single();
This cracks the whole thing wide open, let’s explore. The “Single()” method grabs just the object that matches our Where() query, and returns that object AS THE CORRECT TYPE. Now oneTrueEvil isn’t a collection of enemies, but a single Enemy. If used on a query that returns multiple matches, Single() will throw an error, but if you know what you’re looking for, this shouldn’t be an issue.
As someone who stumbled into Linq land without a clue to go on, this revelation could not have come soon enough. Now that we know the basics, I want to show you two other cool things you can do with Linq.
Snatch out an Object’s Member (Pai-Mei-style)
Dated reference achieved, let me share with you one of my new favorite Linq tricks. We now know how to grab one specific object from a list of objects, but what if we only care about a specific member of that object? Or what if, for some reason, we just want a list of the names of our enemies, instead of a list of Enemy objects? For these occasions, we can use the .Select() method, shown below:
// making a collection of strings // I could use "var" but I want to highlight the type here IEnumerable<string> sh_tList = allEnemies .Where( x => x.health > 0 ) .Select( x => x.name ); // or if we just want one value int krisRemainingHealth = allEnemies .Where( x => x.name == "Kris" ) .Select( x. => x.health ) .Single();
How cool is that? Where once we would need a foreach loop, a conditional statement, and then an object member reference, here we can fit it all into a single value-assignment statement. Could even probably get it on one line, depending on how strict you are with your margins (I am super stingy, note the type-A line breaks).
If you’re still not impressed, let me talk to you a minute about index-linked arrays (if you are impressed, then you can go on to the next paragraph). Part of the reason I like this pattern so much is that it reduces the tedium of working with stored data objects. In Part 1 we talked about how that helps us avoid sorting-lists, but it is also our best weapon against index-linking. Index-linked arrays occur when you keep multiple arrays of different data types that pertain to the same item, synchronized by an index number. Using our Enemy example, instead of having a class called “Enemy” I would have two arrays: one “int[] health” and one “string[] name.” Then, if I know Justin is the 0 index, I can use that index to reference both. This opens the door to so many potential bugs, it’s giving me goosebumps. We see this a lot in Unity when public arrays are set in the editor. (There’s a better way, check out tip #35 in 50 Tips for Working with Unity). In any case, it is much cleaner and safer to keep each enemy’s data neatly bundled in an object, and now with Linq it is super quick to reference that data directly, making it the clear choice.
The Hidden Count
My last nugget of knowledge for you today – counting with Linq. Remember way back when I said that you can’t get the count of IEnumerable collections? I kind of lied. To be fair, I just discovered this recently too, and experienced a “coulda’ had a V8” moment. The irony here is that finding an IEnumerable’s count is almost identical to how we would find the count of a List, thanks to an override of Count in the Linq library. The only difference is, instead of property, this count is a method.
var boyEnemies = allEnemies .Where( x => x.name == "Justin" || x.name == "Kanye" ); int numberOfBoyEnemies = boyEnemies.Count();
Don’t be like me, read the documentation. Although, you are reading this, so you are already not like me. Congratulations!
Anyhow, .Count() can be called on any IEnumerable to get the number of entries in the collection. But wait, there’s more! You can pass a lambda as the argument for Count() to quickly get the number of list-entries that match a certain criteria! I realize this is a lot of mind-blows for one sitting, feel free to take a moment and recover.
// Use it to store the count of a specific subset int numLivingEnemies = allEnemies.Count( x=> x.health > 0 ); // or drop it in wherever! if( allEnemies.Count(x => x.health == 100) > 2 ){ GameOverManGameOver(); }
The second example looks through all of our enemies, and if more than two of them are at maximum health, it calls our game over logic. And all on one line! Try doing that on one line with a loop, come talk to me after.
Conclusion