Lesson 19.3 – Clean up the source code by converting foreach to LINQ

Lesson objectives

At the end of this lesson, you will know…

  • How to make your program easier to read by using LINQ when working with lists

 

We still have several places in the game that can use improvement.

One way to make you code smaller, cleaner, and easier to understand is to replace some of the “foreach” loops (that usually are at least six lines long) with a LINQ statement (which is often one line long).

LINQ is short for Language-Integrated Query.

There are several different ways you can work with this, including one that looks similar to SQL (Structured Query Language – the language you use when you work with many databases). But we’re going to use one of the other methods that I like – lambdas. To me, lambdas make the code very easy to read.

 

How to replace a “foreach” loop with a LINQ statement

Step 1: Open the SuperAdventure solution in Visual Studio and open the Player.cs class.

Step 2: Find the HasRequiredItemToEnterThisLocation() method (it should be on line 30, if you haven’t changed anything). It should look like this:

In the first few lines, if there isn’t anything required to enter the location, we return “true”, to allow the player move to the location.

For the rest of the method, if there is an item required for the player to move to the location, we have a “foreach” loop through the player’s inventory, looking for the required item. The “foreach” loop, and the “return false” (if the item isn’t found), take twelve lines of code.

Let’s make it a little simpler.

Step 3: In order to use LINQ, we need to have it available in the class, with a “using” statement. In this case, we already have “using System.Linq;” at the top of the class, so we’re ready to make our change.

Our objective is to see if there is an item in the player’s inventory with an ID that matches the ID of the item required to enter the location. With the “foreach”, we do this by looping through each item, checking it’s ID.

With LINQ, we can reduce this to one line:

Let’s compare the old method with the new one.

The Exists() function will check the items in the Inventory list, to see if any item matches the expression between the parentheses. If it finds an item, it returns “true”. If it doesn’t, it returns “false”.

Inside the parentheses, what we see is similar to what was between the parentheses in the “foreach” and the “if” statements in the old method.

To the left of the “=>” is “ii”. This is the variable name the LINQ expression will use for each item in the list – just like it did with the “foreach”.

To the right of the “=>” is the expression that is going to be evaluated. In this case, check if the inventory item’s ID matches the ID of the required item’s ID.

That’s how these lambda expressions work. The variable declaration for the list item is to the left of the “arrow”, and the expression is to the right.

Step 4: Now we’ll do the same thing to the HasThisQuest() method, and change it to this:

Step 5: Find the HasAllQuestCompletionItems() method.

This one is a little more complex. We want to see if the player has the item required to complete a quest and if they have enough of those items in their inventory.

So, we’ll use this for our LINQ statement:

This expression will see if the item exists in the player’s inventory (ii.Details.ID == qci.Details.ID) and if the quantity in the player’s inventory is greater than, or equal to, the quantity required to complete the quest (ii.Quantity >= qci.Quantity).

If the program doesn’t find an item in the list that matches both conditions, we’ll stop checking and return “false” for the method. If it gets through all the items required to complete the quest, the method returns “true” at the end.

We could go even further in cleaning up this method by writing a LINQ query for the remaining “foreach” in this method, but the query would be a little more complex than I want to show you right now.

Step 6: You can also get a specific item from a list with the SingleOrDefault method. However, you’ll need to check if it returned “null”, since nothing matched the condition. SingleOrDefault also only works if you’ll only ever have one item in the list that matches the condition. You’ll need to use a different LINQ method if you want to get more than one item from the list.

Here is how you can use SingleOrDefault in the RemoveQuestCompletionItem(), AddItemToInventory(), and MarkQuestCompleted() methods:

In this situation, it doesn’t really reduce the amount of code. But you may find this useful in a future program.

 

Check your work

Build the solution and make sure there are no errors in the “output” box at the bottom of Visual Studio. If you see any problems, double-check the changes you made in this lesson.

 

Summary

This covers just one way to use LINQ. You can also do things such as calculating the sum of a property for items in a list:

You can build a chain of LINQ statements, like this (which will give you the sum of the Quantity of all items in the Inventory list, for items that have a Quantity greater than five):

For a good list of everything you can do with LINQ, check out this page.

 

 

Source code for this lesson

Get it from GitHub: https://gist.github.com/ScottLilly/74bbf53d1070b56cd232

or DropBox: Lesson 19.3 – https://www.dropbox.com/sh/rzj1fzqx4tugy12/AADwyeAi_gHWvzxiSvKSctNsa?dl=0

 

Previous lesson: Lesson 19.2 – Use a calculated value for a property

Next lesson: Lesson 19.4 – Saving and loading the player information

All lessons: Learn C# by Building a Simple RPG Index

6 thoughts on “Lesson 19.3 – Clean up the source code by converting foreach to LINQ

  1. Hi Scott,

    Thanks for the great tutorial!! It’s really really fun to be able to learn C# and make a complete (and fun) program along the way.

    I have a question though: in your new RemoveQuestCompletionItems method, you retained the break statement after subtracting the item quantity. If there was more than one QuestCompletionItem in the QuestCompletionItems list, wouldn’t this cause the foreach loop to stop after the first QuestCompletionItem, and not continue on to the other QuestCompletionItem(s)?

    Also, for the same method, is the null check necessary? We only call it after making sure that playerHasAllItemsToCompleteQuest == true, so there will always be an item. If OTOH we mistakenly call it when playerHasAllItemsToCompleteQuest == false, wouldn’t the absence of a null check throw an exception, and make it easier for us to discover and fix the problem (problem being the incorrect call)? If indeed the null check is necessary, then shouldn’t we also, for consistency, re-check that item.Quantity >= qci.Quantity so that we don’t accidentally make the quantity negative? Sorry for the long question, I’m trying to figure out when and when not to use a null check.

    Thanks for all the help!

    1. Hi Julian,

      You are correct. That “break;” statement needed to be removed. I remove it in Lesson 20.4, when we make some more changes. However, it should not be in that function. I fixed the posts and files for the next few lessons, where it had the “break;”. Thank you for catching that.

      The null check is not really needed here, because we only call this function after passing HasAllQuestCompletionItems. However, I have a habit of always doing a null check after using the SingleOrDefault LINQ function, to get an item from a list. At my job, I work on programs that do work asynchronously (not all parts are working at the same time). We always need to do this check, in case the object does not exist any more.

      If we were writing a more complex game, like World of Warcraft, we would definitely need to do the null check and include re-checking the quantity. Not re-checking everything could lead to exploits.

  2. Thanks for the great explanation, Scott! Asynchronous programs sound very challenging. Indeed it does seem like all those checks would be very crucial for those type of programs.

    Thanks again!

  3. For anyone else going through these lessons, I noticed a bug where a player could attack from their home after dying to a monster, here’s the fix:

    Under this if statement in SuperAdventure.cs
    if (_player.CurrentHitPoints <= 0)

    Add this code
    btnUseWeapon.Visible = false;
    btnUsePotion.Visible = false;
    cboWeapons.Visible = false;
    cboPotions.Visible = false;

    1. You could even do it on one line as follows:
      btnUseWeapon.Visible = btnUsePotion.Visible = cboWeapons.Visible = cboPotions.Visible = false;

Leave a Reply

Your email address will not be published. Required fields are marked *