Skip to main content

C# Language support for Tuples.

// ***************************************************************************
// C# Language support for Tuples
// From: https://riptutorial.com/csharp/example/6329/language-support-for-tuples
// ***************************************************************************

// ----------------------------------------------------------------------------
// Tuple basics
// ----------------------------------------------------------------------------

// A tuple is an ordered, finite list of elements. Tuples are commonly used in
// programming as a means to work with one single entity collectively instead of
// individually working with each of the tuple's elements, and to represent
// individual rows (ie. "records") in a relational database.

// In C# 7.0, methods can have multiple return values. Behind the scenes, the
// compiler will use the new ValueTuple struct.
public (int sum, int count) GetTallies()
{
    return (1, 2);
}

// If a tuple-returning method result is assigned to a single variable you can
// access the members by their defined names on the method signature:
var result = GetTallies();
// > result.sum
// 1
// > result.count
// 2

// ----------------------------------------------------------------------------
// Creating tuples
// ----------------------------------------------------------------------------

// Tuples are created using generic types Tuple<T1>-Tuple<T1,T2,T3,T4,T5,T6,T7,T8>.
// Each of the types represents a tuple containing 1 to 8 elements. Elements can be
// of different types.
//
// Tuple with 4 elements:
var tuple = new Tuple<string, int, bool, MyClass>("foo", 123, true, new MyClass());

// Tuples can also be created using static Tuple.Create methods. In this case,
// the types of the elements are inferred by the C# Compiler.
//
// Tuple with 4 elements (using `Tuple.Create()`):
var tuple = Tuple.Create("foo", 123, true, new MyClass());

// Since C# 7.0, Tuples can be easily created using `ValueTuple`.
var tuple = ("foo", 123, true, new MyClass());

// Elements can be named for easier decomposition (see topic below, "Tuple Deconstruction" for more info.)
(int number, bool flag, MyClass instance) tuple = (123, true, new MyClass());

// ----------------------------------------------------------------------------
// Accessing tuple elements
// ----------------------------------------------------------------------------

// To access tuple elements use Item1-Item8 properties. Only the properties with
// index number less or equal to tuple size are going to be available (i.e. one
// cannot access Item3 property in Tuple<T1,T2>).
var tuple = new Tuple<string, int, bool, MyClass>("foo", 123, true, new MyClass());
var item1 = tuple.Item1; // "foo"
var item2 = tuple.Item2; // 123
var item3 = tuple.Item3; // true
var item4 = tuple.Item4; // new My Class()

// ----------------------------------------------------------------------------
// Return multiple values from a method
// ----------------------------------------------------------------------------

// Tuples can be used to return multiple values from a method without using out
// parameters. In the following example AddMultiply is used to return two values
// (sum, product).
void Write()
{
    var result = AddMultiply(25, 28);
    Console.WriteLine(result.Item1); // 53
    Console.WriteLine(result.Item2); // 700
}

Tuple<int, int> AddMultiply(int a, int b)
{
    return new Tuple<int, int>(a + b, a * b);
}

// Now C# 7.0 offers an alternative way to return multiple values from methods
// using value tuples More info about ValueTuple struct.

// ----------------------------------------------------------------------------
// Tuple Deconstruction
// ----------------------------------------------------------------------------

// Tuple deconstruction separates a tuple into its parts.
//
// For example, invoking GetTallies and assigning the return value to two
// separate variables deconstructs the tuple into those two variables:
(int tallyOne, int tallyTwo) = GetTallies();

// var also works:
(var s, var c) = GetTallies();

// You can also use shorter syntax, with var outside of `()`:
var (s, c) = GetTallies();

// You can also deconstruct into existing variables:
int s, c;
(s, c) = GetTallies();

// Swapping is now much simpler (no temp variable needed):
(b, a) = (a, b);

// Interestingly, any object can be deconstructed by defining a Deconstruct method in the class:
class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }

    public void Deconstruct(out string firstName, out string lastName)
    {
        firstName = FirstName;
        lastName = LastName;
    }
}

var person = new Person { FirstName = "John", LastName = "Smith" };
var (localFirstName, localLastName) = person;

// In this case, the (localFirstName, localLastName) = person syntax is invoking Deconstruct on the person.

// Deconstruction can even be defined in an extension method. This is equivalent to the above:
public static class PersonExtensions
{
    public static void Deconstruct(this Person person, out string firstName, out string lastName)
    {
        firstName = person.FirstName;
        lastName = person.LastName;
    }
}

var (localFirstName, localLastName) = person;

// An alternative approach for the Person class is to define the Name itself as a Tuple. Consider the following:
class Person
{
    public (string First, string Last) Name { get; }

    public Person((string FirstName, string LastName) name)
    {
        Name = name;
    }
}

// Then you can instantiate a person like so (where we can take a tuple as an argument):
var person = new Person(("Jane", "Smith"));

var firstName = person.Name.First; // "Jane"
var lastName = person.Name.Last;   // "Smith"

// ----------------------------------------------------------------------------
// Tuple Initialization
// ----------------------------------------------------------------------------

// You can arbitrarily create tuples in code:
var name = ("John", "Smith");
Console.WriteLine(name.Item1); // John
Console.WriteLine(name.Item2); // Outputs Smith

// When creating a tuple, you can assign ad-hoc item names to the members of the tuple:
var name = (first: "John", middle: "Q", last: "Smith");
Console.WriteLine(name.first); // John

// ----------------------------------------------------------------------------
// Type inference
// ----------------------------------------------------------------------------

// Multiple tuples defined with the same signature (matching types and count)
// will be inferred as matching types.For example:
public (int sum, double average) Measure(List<int> items)
{
    var stats = (sum: 0, average: 0d);
    stats.sum = items.Sum();
    stats.average = items.Average();

    return stats;
}

// stats can be returned since the declaration of the stats variable and
// the method's return signature are a match.

// ----------------------------------------------------------------------------
// Reflection and Tuple Field Names
// ----------------------------------------------------------------------------

// Member names do not exist at runtime. Reflection will consider tuples with
// the same number and types of members the same even if member names do not
// match. Converting a tuple to an `object` and then to a tuple with the same
// member types, but different names, will not cause an exception either.

// While the ValueTuple class itself does not preserve information for member
// names the information is available through reflection in a
// TupleElementNamesAttribute. This attribute is not applied to the tuple itself
// but to method parameters, return values, properties and fields. This allows
// tuple item names to be preserved across assemblies i.e. if a method returns
// (string name, int count) the names name and count will be available to
// callers of the method in another assembly because the return value will be
// marked with TupleElementNameAttribute containing the values "name" and
// "count".

// ----------------------------------------------------------------------------
// Use with generics and async
// ----------------------------------------------------------------------------

// The new tuple features (using the underlying `ValueTuple` type) fully support
// generics and can be used as generic type parameter. That makes it possible to
// use them with the async/await pattern:
public async Task<(string value, int count)> GetValueAsync()
{
    string fooBar = await _stackoverflow.GetStringAsync();
    int num = await _stackoverflow.GetIntAsync();

    return (fooBar, num);
}

// ----------------------------------------------------------------------------
// Use with collections
// ----------------------------------------------------------------------------

// It may become beneficial to have a collection of tuples in (as an example) a
// scenario where you're attempting to find a matching tuple based on conditions
// to avoid code branching.

// Example:
private readonly List<Tuple<string, string, string>> labels = new List<Tuple<string, string, string>>()
{
    new Tuple<string, string, string>("test1", "test2", "Value"),
    new Tuple<string, string, string>("test1", "test1", "Value2"),
    new Tuple<string, string, string>("test2", "test2", "Value3"),
};

public string FindMatchingValue(string firstElement, string secondElement)
{
    var result = labels
        .Where(w => w.Item1 == firstElement && w.Item2 == secondElement)
        .FirstOrDefault();

    if (result == null)
        throw new ArgumentException("combo not found");

    return result.Item3;
}

// With the new tuples can become:
private readonly List<(string firstThingy, string secondThingyLabel, string foundValue)> labels = new
    List<(string firstThingy, string secondThingyLabel, string foundValue)>()
{
    ("test1", "test2", "Value"),
    ("test1", "test1", "Value2"),
    ("test2", "test2", "Value3"),
}

public string FindMatchingValue(string firstElement, string secondElement)
{
    var result = labels
        .Where(w => w.firstThingy == firstElement && w.secondThingyLabel == secondElement)
        .FirstOrDefault();

    if (result == null)
        throw new ArgumentException("combo not found");

    return result.foundValue;
}

// Though the naming on the example tuple above is pretty generic, the idea of
// relevant labels allows for a deeper understanding of what is being attempted
// in the code over referencing "item1", "item2", and "item3".

// ----------------------------------------------------------------------------
// Differences between ValueTuple and Tuple
// ----------------------------------------------------------------------------

// The primary reason for introduction of ValueTuple is performance.
//
// |                  Type name                  | ValueTuple |                          Tuple                          |
// |---------------------------------------------|------------|---------------------------------------------------------|
// | Class or structure                          | struct     | class                                                   |
// | Mutability (changing values after creation) | mutable    | immutable                                               |
// | Naming members and other language support   | yes        | no (TBD, https://github.com/dotnet/roslyn/issues/11031) |

// ----------------------------------------------------------------------------
// Comparing and sorting Tuples
// ----------------------------------------------------------------------------

// Tuples can be compared based on their elements.
//
// As an example, an enumerable whose elements are of type Tuple can be sorted
// based on comparisons operators defined on a specified element:
List<Tuple<int, string>> list = new List<Tuple<int, string>>();
list.Add(new Tuple<int, string>(2, "foo"));
list.Add(new Tuple<int, string>(1, "bar"));
list.Add(new Tuple<int, string>(3, "qux"));

// sort based on the string element
list.Sort((a, b) => a.Item2.CompareTo(b.Item2));

foreach (var element in list) {
    Console.WriteLine(element);
}

// Output:
//
// (1, bar)
// (2, foo)
// (3, qux)

// Or to reverse the sort use:
list.Sort((a, b) => b.Item2.CompareTo(a.Item2));