Skip to content

Instantly share code, notes, and snippets.

@BenMakesGames
Last active July 30, 2021 23:08
Show Gist options
  • Save BenMakesGames/240459cde4e96be0491e80b51a1757cd to your computer and use it in GitHub Desktop.
Save BenMakesGames/240459cde4e96be0491e80b51a1757cd to your computer and use it in GitHub Desktop.

why LINQ is great

LINQ lets you manipulate collections super-easily, eliminating the need for loops and temporary variables in many cases.

here's a simple example: finding even numbers from a list of numbers, using a loop:

public List<int> FindEvenNumbers(List<int> numbers)
{
    var evenNumbers = new List<int>();

    foreach(var n in numbers)
    {
        if(n % 2 == 0)
            evenNumbers.Add(n);
    }
    
    return evenNumbers;
}

these kinds of loops are really common, but usually with more interesting conditions...

public Tile FindBestMonsterMove(Monster monster)
{
    var allTiles = GetAllTiles();
    var availableTiles = new List<Tile>();

    foreach(var t in allTiles)
    {
        if(TileMath.Distance(monster.Tile, t) <= monster.MovementSpeed && t.CanBeMovedInto())
            availableTiles.Add(t);
    }

    // TODO: decide which availableTile is the best to move the monster into
    // (probably more loops...)
    
    return bestMove;
}

but let's go back to finding even numbers for now.

foreach(var n in numbers)
{
    if(n % 2 == 0)
        evenNumbers.Add(n);
}

any time you're transforming one list into another - be it filtering, duplicating, changing from one type to another, etc - LINQ can probably help you do it way-easier!

"LINQ" is just the name of a library that contains a bunch of methods that operate on collections (arrays, Lists, IEnumerables, etc) to let you turn one collection into another (or a collection into a single element; more on that later). you tell these methods what logic they should use (by giving it a function - called a "delegate" in this context), and it applies that logic to every element.

(LINQ comes with C#. add using System.Linq; to your C# files, if your IDE didn't already include it for you.)

for example: in order to take a list of numbers, and get just the even numbers, you apply some evaluation to every number - n % 2 == 0 - and if a number passes that evaluation, it's kept!

the LINQ method that does this "keep it or leave it" evaluation is called Where. you're finding the elements in some list where some condition is met. (in JavaScript they call this method filter, which I feel is a little more descriptive, but whatever. Where is fine, too.)

public List<int> FindEvenNumbers(List<int> numbers)
{
    return numbers.Where(n => n % 2 == 0).ToList();
}

that's approximately WAY shorter than the loop-based FindEvenNumbers method!

I can imagine two questions to the above code: "what's up with the funny n => n % 2 == 0", and "what's up with the .ToList() at the end?"

first: what's up with the funny n => n % 2 == 0.

that's a clever short-hand way of writing functions in C#, as an ANONYMOUS LAMBDA FUNCTION (more on these exciting words in a moment). by comparison, a more-verbose way would have been:

public List<int> FindEvenNumbers(List<int> numbers)
{
    return numbers.Where(IsEven).ToList();
}

private bool IsEven(int n)
{
    return n % 2 == 0;
}

Where needs to know the logic to apply to every element, so it needs a function which takes in a value of the type in the list (in this case, int), and returns a bool that tells it "keep it (true), or don't (false)".

you CAN write out the whole dang function, and then give the name of that function to Where, like the example immediately above, but the developers of C# were like "hey, sometimes you just want to define a quick little function that you don't really care about re-using, or even naming! what if we made a way to do that?" so they did!

here's some vocabulary that will make super-duper computer science architecture algorithm people (i.e. NERRRRDS) happy:

  • delegate: a function that you're probably not going to call yourself, but instead pass to some other function, for it to invoke on your behalf
  • anonymous function: a function without a name
  • lambda expression: an anonymous function where you use the slick => syntax to describe the function's parameters and return value

second question: what's up with the .ToList() at the end?

return numbers.Where(n => n % 2 == 0).ToList();

Where (and other LINQ methods) are sneakily efficient by not actually allocating a List until you need one. if you leave off the .ToList() at the end, then the above code would instead return an IEnumerable<int>! IEnumerables are lazily evaluated when you loop over them. if that doesn't make a ton of sense, don't worry about it too much, but the point is: you can string together .Where, and other LINQ methods, one after another, and you don't have to be afraid that each one is making a whole new list, because they aren't! only when you call .ToList() is a list finally created!

  • there's also .ToArray(), if you prefer arrays for some reason
  • in some cases, using an IEnumerable is fine - no need to ever allocate a list or array! you can worry about it later, when/if you start dealing with HUGE collections; for now, just tuck this away in some corner of your mind-brain: if you can get away without calling .ToList() or .ToArray(), and just pass around IEnumerables instead, that's probably pretty cool

so what are some other LINQ methods?

well, here's another problem, solved in a traditional, loop-y way, which - spoilers - there's a LINQ method for:

public List<int> SquareNumbers(List<int> numbers)
{
    var squareNumbers = new List<int>();

    foreach(var n in numbers)
        squareNumbers.Add(n * n);
    
    return squareNumbers;
}

we're taking one list, and generating a new one by performing some operation on every element. in this case, we're squaring.

the LINQ method that does this is Select. (and since I mentioned JavaScript before, in JavaScript this method is called map. I don't like either of these names.)

public List<int> SquareNumbers(List<int> numbers)
{
    return numbers.Select(n => n * n).ToList();
}

by the way, since we're using that => syntax, now, mind if I BLOW YOUR MIND?

public List<int> SquareNumbers(List<int> numbers) => numbers.Select(n => n * n).ToList();

yeah, you can do that. but don't get too carried away: the quest for one-liners often leads to madness.

anyway: in that example, we turned ints into ints, but you can turn things into different kinds of things, if you want!

let's turn some numbers into some strings!

public List<string> NumberOfBeersOnTheWall(List<int> numbers)
{
    return numbers.Select(n => n + " bottles of beer on the wall, " + n + " bottles of beer. Take one down, pass it around, " + (n - 1) + " bottles of beer on the wall.").ToList();
}

not to get off topic, but we can use string interpolation to make things a little tidier. also, the line is getting kinda' long, so I'm gonna split it up a little:

public List<string> NumberOfBeersOnTheWall(List<int> numbers)
{
    return numbers
        .Select(n =>
            $"{n} bottles of beer on the wall, {n} bottles of beer. " +
            $"Take one down, pass it around, {n - 1} bottles of beer on the wall."
        )
        .ToList()
    ;
}

(pro tip: when the LINQ statements start getting big, split them across multiple lines! this was kind of a wonky example, with the super-long string, but you get it; you're smart.)

and don't be shy: you can use .Select to do some pretty large transformations:

// a class that stores a number, and some facts about that number:
class NumberFact
{
    public int Number;
    public string HexidecimalRepresentation;
    public bool IsEven;
    public int Square;
    public bool IsMyFavorite;
}

...

// takes a list of numbers, and returns a list of number facts about those numbers!
public List<NumberFact> GetNumberFacts(List<int> numbers)
{
    return numbers
        .Select(n => new NumberFact() {
            Number = n,
            HexidecimalRepresentation = n.ToNumber("X"), // some ToString magic bullshit; don't worry about it
            IsEven = n % 2 == 0,
            Square = n * n,
            IsMyFavorite = n == 7
    	})
        .ToList()
    ;
}

there are several other LINQ methods for manipulating lists, like .OrderBy, .OrderByDescending, and the mind-bending .SelectMany!

also, so far we've been turning lists into lists, but a few LINQ methods pick a single item out of a list - like .First and .FirstOrDefault - or reduce a list down to a single item - like Aggregate.

here's an easy example:

public Person GetOldestPerson(List<Person> people)
{
    return people
        .OrderBy(p => p.Birthday) // order by the "Birthday" property on each person
        .FirstOrDefault() // return the first one (or null - the "default" value of type Person - if the set is empty)
    ;    
}

stop for a moment to think about how you'd write something like this with loops...

public Person GetOldestPerson(List<Person> people)
{
    Person oldestPerson = null;

    foreach(var p in people)
    {
        if(oldestPerson == null || oldestPerson.Birthday < p.Birthday)
            oldestPerson = p;
    }
    
    return oldestPerson;
}

gross.

and, sorry, but I just can't help myself:

public Person GetOldestPerson(List<Person> people) => people.OrderBy(p => p.Birthday).First();

again: the quest for one-liners often leads to madness, but this demonstrates just how much we've been able to simplify our method. it's not even CONCEIVABLE that the loop-based implementation would fit on one line. the LINQ-based solution CAN, even if it probably SHOULDN'T!

but the REAL power/fun comes in chaining LINQ statements together, one after another. a more-exciting example is coming up, but for now, what if we wanted to square only the even numbers?

public List<int> SquaresOfEvenNumbers(List<int> numbers) => numbers
    .Where(n => n % 2 == 0) // only even numbers, plz!
    .Select(n => n * n) // square 'em!
    .ToList()
;    

(pro tip: one-liners don't have to be on one line! even when each LINQ statement is short, putting each on its own line greatly increases readability, even without silly comments! pro-pro tip: you don't need to include silly comments!)

when you're comfy with LINQ, you can accomplish a lot with very few lines of code:

public Tile FindBestMonsterMove(Monster monster) => GetAllTiles()
    .Where(t =>
        TileMath.Distance(monster.Tile, t) <= monster.MovementSpeed &&
        t.CanBeMovedInto() // breaking long && statements across multiple lines helps, too!
    )
    .Select(t => new TileConsideration() {
        Tile = t,
        Weight = EvaluateHowGoodItIsToMoveToThisTile(monster, t)
    })
    .OrderByDescending(t => t.Weight) // sort by "Weight" property, from largest to smallest
    .FirstOrDefault() // pick the first element, or null if the list is empty
;

of course, there's a lot of logic hidden in that EvaluateHowGoodItIsToMoveToThisTile method... spoilers: it probably contains more LINQ :P

LINQ is great!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment