I will now start adding more meat to the dummy implementation of the GetAverageAndCount() method we created in the previous recipe. If you are new to Tuples, and have not worked through the previous recipe, I encourage you to do so first before starting to work through this recipe.
Working with Tuples - going deeper
Getting ready
You need to have completed the code steps in the recipe Working with Tuples - getting started, in order to work through this recipe. Ensure that you have added the required NuGet package as specified in the previous recipe.
How to do it...
- Let's take a look at the calling code again. We can further simplify the code in the static void Main method by getting rid of the var s. When we called the GetAverageAndCount() method, we returned the Tuple into var s.
var s = ch1.GetAverageAndCount(scores);
- We do not have to do this. C# 7.0 allows us to immediately split the Tuple into its respective parts as follows:
var (average, studentCount) = ch1.GetAverageAndCount(scores);
- We can now consume the values returned by the Tuple directly as follows:
WriteLine($"Average was {average} across {studentCount} students");
- Before we implement the GetAverageAndCount() method, make sure that your static void Main method looks as follows:
static void Main(string[] args)
{
int[] scores = { 17, 46, 39, 62, 81, 79, 52, 24 };
Chapter1 ch1 = new Chapter1();
var (average, studentCount) = ch1.GetAverageAndCount(scores);
WriteLine($"Average was {average} across {
studentCount} students");
ReadLine();
}
- Secondly, ensure that the GetAverageAndCount() method's dummy implementation looks as follows:
public (int average, int studentCount)
GetAverageAndCount(int[] scores)
{
var returnTuple = (ave:0, sCount:0);
return returnTuple;
}
- Go ahead and run your console application. You will see that the two values, average and studentCount are returned from our dummy implementation of GetAverageAndCount().
- The values are obviously still zero because we have not defined any logic inside the method. We will do this next. Before we write the implementation, make sure that you have added the following using statement:
using System.Linq;
- Because we are using an array of integers for the variable scores, we can easily return the results we need. LINQ allows us to get the sum of the student scores contained in the scores array, simply by writing scores.Sum(). We can also easily get the count of the student scores from the scores array by writing scores.Count(). The average, therefore, would logically be the sum of the scores divided by the count of the student scores (scores.Sum()/scores.Count()). We then put the values into our returnTuple literal as follows:
public (int average, int studentCount)
GetAverageAndCount(int[] scores)
{
var returnTuple = (ave:0, sCount:0);
returnTuple = (returnTuple.ave = scores.Sum()/scores.Count(),
returnTuple.sCount = scores.Count());
return returnTuple;
}
- Run your console application to see the result displayed as follows:
- We can see that the class average isn't too great, but that is of little importance to our code. Another piece of code that isn't too great is this line:
returnTuple = (returnTuple.ave = scores.Sum()/scores.Count(),
returnTuple.sCount = scores.Count());
- It is clunky and doesn't read very nicely. Let's simplify this a bit. Remember that I mentioned previously that Tuples play nicely together as long as their types match? This means that we can do this:
public (int average, int studentCount)
GetAverageAndCount(int[] scores)
{
var returnTuple = (ave:0, sCount:0);
returnTuple = (scores.Sum()/scores.Count(), scores.Count());
return returnTuple;
}
- Run your console application again and notice that the result stays the same:
- So why did we give the Tuple literal names to begin with? Well, it allows you to reference them easily within your GetAverageAndCount() method. It is also really very useful when using a foreach loop in your method. Consider the following scenario. In addition to returning the count and average of the student scores, we need to return an additional Boolean value if the class average is below a certain threshold. For this example, we will be making use of an extension method called CheckIfBelowAverage() and it will take a threshold value as an integer parameter. Start off by creating a new static class called ExtensionMethods.
public static class ExtensionMethods
{
}
- Inside the static class, create a new method called CheckIfBelowAverage() and pass it an integer value called threshold. The implementation of this extension method is pretty straightforward, so I will not go into much detail here.
public static bool CheckIfBelowAverage(
this int classAverage, int threshold)
{
if (classAverage < threshold)
{
// Notify head of department
return true;
}
else
return false;
}
- In the Chapter1 class, overload the GetAverageAndCount() method by changing its signature and passing a value for the threshold that needs to be applied. You will remember that I mentioned that a Tuple return type method can return several values, not just two. In this example, we are returning a third value called belowAverage that will indicate if the calculated class average is below the threshold value we pass to it.
public (int average, int studentCount, bool belowAverage)
GetAverageAndCount(int[] scores, int threshold)
{
}
- Modify the Tuple literal, adding it to subAve ,and default it to true, because a class average of zero will logically be below any threshold value we pass to it.
var returnTuple = (ave: 0, sCount: 0, subAve: true);
- We can now call the extension method CheckIfBelowAverage() on the returnTuple.ave value we defined in our Tuple literal and pass through it the threshold variable. Just how useful giving the Tuple literal logical names becomes evident when we use it to call the extension method.
returnTuple = (scores.Sum() / scores.Count(), scores.Count(),
returnTuple.ave.CheckIfBelowAverage(threshold));
- Your completed GetAverageAndCount() method will now look as follows:
public (int average, int studentCount, bool belowAverage)
GetAverageAndCount(int[] scores, int threshold)
{
var returnTuple = (ave: 0, sCount: 0, subAve: true);
returnTuple = (scores.Sum() / scores.Count(), scores.Count(),
returnTuple.ave.CheckIfBelowAverage(threshold));
return returnTuple;
}
- Modify your calling code to make use of the overloaded GetAverageAndCount() method as follows:
int threshold = 51;
var (average, studentCount, belowAverage) = ch1.GetAverageAndCount(
scores, threshold);
- Lastly, modify the interpolated string to read as follows:
WriteLine($"Average was {average} across {studentCount}
students. {(average < threshold ?
" Class score below average." :
" Class score above average.")}");
- The completed code in your static void Main method should now look as follows:
static void Main(string[] args)
{
int[] scores = { 17, 46, 39, 62, 81, 79, 52, 24 };
Chapter1 ch1 = new Chapter1();
int threshold = 51;
var (average, studentCount, belowAverage) =
ch1.GetAverageAndCount(scores, threshold);
WriteLine($"Average was {average} across {studentCount}
students. {(average < threshold ?
" Class score below average." :
" Class score above average.")}");
ReadLine();
}
- Run your console application to view the result.
- To test that the ternary operator ? is working correctly inside the interpolated string, modify your threshold value to be lower than the average returned.
int threshold = 40;
- Running your console application a second time will result in a passing average class score.
- Finally, there is one glaring problem that I need to highlight with this recipe. It is one that I am sure you have picked up on already. If not, don't worry. It is a bit of a sneaky one. This is the gotcha I was referring to at the start of this recipe and I intentionally wanted to include it to illustrate the bug in the code. Our array of student scores is defined as follows:
int[] scores = { 17, 46, 39, 62, 81, 79, 52, 24 };
- The sum of these equals to 400 and because there are only 8 scores, the value will work out correctly because it divides up to a whole number (400 / 8 = 50). But what would happen if we had another student score in there? Let's take a look. Modify your scores array as follows:
int[] scores = { 17, 46, 39, 62, 81, 79, 52, 24, 49 };
- Run your console application again and look at the result.
- The problem here is that the average is incorrect. It should be 49.89. We know that we want a double (unless your application of this is intended to return an integer). We, therefore, need to pay attention to casting the values correctly in the return type and the Tuple literal. We also need to handle this in the extension method CheckIfBelowAverage(). Start off by changing the extension method signature as follows to act on a double.
public static bool CheckIfBelowAverage(
this double classAverage, int threshold)
{
}
- Then we need to change the data type of the average variable in the Tuple method return type as follows:
public (double average, int studentCount, bool belowAverage)
GetAverageAndCount(int[] scores, int threshold)
{
}
- Then, modify the Tuple literal so ave is a double by using ave: 0D.
var returnTuple = (ave: 0D, sCount: 0, subAve: true);
- Cast the average calculation to a double.
returnTuple = ((double)scores.Sum() / scores.Count(),
scores.Count(),
returnTuple.ave.CheckIfBelowAverage(threshold));
- Add the following using statement to your application:
using static System.Math;
- Lastly, use the Round method to format the average variable in the interpolated string to two decimals.
WriteLine($"Average was {Round(average,2)} across {studentCount}
students. {(average < threshold ?
" Class score below average." :
" Class score above average.")}");
- If everything is done correctly, your GetAverageAndCount() method should look as follows:
public (double average, int studentCount, bool belowAverage)
GetAverageAndCount(int[] scores, int threshold)
{
var returnTuple = (ave: 0D, sCount: 0, subAve: true);
returnTuple = ((double)scores.Sum() / scores.Count(),
scores.Count(),
returnTuple.ave.CheckIfBelowAverage(
threshold));
return returnTuple;
}
- Your calling code should also look as follows:
static void Main(string[] args)
{
int[] scores = { 17, 46, 39, 62, 81, 79, 52, 24, 49 };
Chapter1 ch1 = new Chapter1();
int threshold = 40;
var (average, studentCount, belowAverage) =
ch1.GetAverageAndCount(scores, threshold);
WriteLine($"Average was {Round(average,2)} across
{studentCount} students. {(average < threshold ?
" Class score below average." :
" Class score above average.")}");
ReadLine();
}
- Run the console application to see the correctly rounded average for the student scores.
How it works...
Tuples are structs, and therefore value types that are created locally. You, therefore, do not have to worry about using and assigning Tuples on-the-fly or that it creating a lot of allocations. Their contents are merely copied when passed. Tuples are mutable and the elements are publicly scoped mutable fields. Using the code example in this recipe, I can, therefore, do the following:
returnTuple = (returnTuple.ave + 15, returnTuple.sCount - 1);
C# 7.0 is allowing me to first update the average value (shifting the average up) and then decrementing the count field. Tuples are a very powerful feature of C# 7.0, and it will be of great benefit to many developers when implemented it correctly.