Yet another developer journey ...

Yet another developer journey ...

Tuples, tuples... value tuples!

Photo by Alexander Popov on Unsplash

Tuples, tuples... value tuples!

Dominik Schischma's photo
Dominik Schischma
·May 29, 2022·

3 min read

Introduction

In the very first beginning of tuples in c# I firstly skipped it, because the readability had been not what I though of creating readable code.

Example from MSDN:

(bool, string) t1 = (true, "");
Console.WriteLine($"Tuple with value {t1.Item1} and string {t1.Item2}.");

When using tuples with more than 2 variables it was always about remembering which variable is which item.

With the introduction of ValueTuples this changed. Now it is possible to assign a 'name' to each tuple variable:

(bool rc, string error) t2 = (true, "");
Console.WriteLine($"Tuple with value {t2.rc} and string {t2.error}.");

Where for?

Now where should I use tuples?

Data storage

In general they can be used everywhere. However imho for complex data, the code readability will be worse, if you use it for storing big data instead of using classes. Let me show you an example:

(string FirstName, string LastName, DateTime BirthDay) person

Using it together with a list will result in:

List<(string FirstName, string LastName, DateTime BirthDay) person>

In this case using a dedicated data class should be prefered as the readability greatly improves and the data class can be extended with additional methods.

But for temporarily storing small data constructs, where Dictionaries, Collections or Hastables do not bring in any benefit, they are great also!

Method return type

That is the point where we switched to value tuples and now using them heavily. Instead of using out variables or ref variables for providing the error messages to the calling method we switched to tuples.

Previously:

string error = "";
bool rc = FirstMethod(ref string error);
if(!rc)
  _logger.Error(error);

rc = SecondMethod(ref string error);
if(!rc)
  _logger.Error(error);

Now with tuples:

(bool success, string error) callRc = FirstMethod();
if(!callRc.success)
  _logger.Error(callRc.error);

callRc = SecondMethod();
if(!callRc.success)
  _logger.Error(callRc.error);

It can be easily extended with a more complex tuple for also handling an additional variable providing e. g. data from the called method which does work great together with nullable:

(bool success, string error, string[] lines) callRc = LoadFile();
if(!callRc.success)
{
   _logger.Error(callRc.error);
   return;
}
// process data using callRc.lines ...

Replacement for exceptions

Instead of throwing exceptions from one sub method all the way up to the method which is handling the error (e.g. logging or displaying to the user), tuples provide a great way to save the costs of exception!

A full example method:

public static (bool success, string message, string[] lines) ReadAllFileLinesN(string fileName, Encoding encoding = null)
{
    try
    {
        // using FileStream + BufferedStream seems to be the fastest 
        using FileStream fs = File.Open(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
        using BufferedStream bs = new BufferedStream(fs);

        // encoding or no?
        using StreamReader sr = encoding != null ? new StreamReader(bs, encoding) : new StreamReader(bs);

        // read
        string s;
        List<string> fileLines = new List<string>();
        while ((s = sr.ReadLine()) != null) 
            fileLines.Add(s);
        return (true, null, fileLines.ToArray());
    }
    catch (Exception ex) when (ex is FileNotFoundException or OutOfMemoryException or UnauthorizedAccessException or IOException)
    {
        return (false, ex.Message, null);
    }
}

Tuples and var

Instead of specifying the return values each on it's own:

(bool success, string error) callRc = FirstMethod();

We can also use var which does work perfect with IntelliSense:

var callRc = FirstMethod();

Deconstruct

And last but not least, we can also 'desconstruct' the tuple and direct access the single variables:

(bool success, string error) = FirstMethod();
if(!success)
  _logger.Error(error);

// use them again but without declaration 
(success, error) = SecondMethod();
if(!success)
  _logger.Error(error);

Naming conventions

Internally we decided to use the following typing conventions with tuples:

  • For return tuples, we use tuple variables in lowerCase
  • For data tuples, we use tuple variables in UpperCase

Last but not least, please remind yourself that tuples are value types!

 
Share this