In this article, we will take an in-depth look at tuple aliases: what they are, how to use them, and the different rules that apply to them. 

To download the source code for this article, you can visit our GitHub repository.

Now let’s dig in!

What Are Tuples in C#?

Tuples in C# are lightweight data structures that store related data elements. A tuple can be a value or reference type. However, the focus of this article is on the ValueTuple type. 

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

We can declare a tuple by enclosing a comma-separated list of elements in parentheses and assign values to it:

(int id, string firstName, string lastName, float salary) EmployeeDetails = (1, "John", "Doe", 116000);

Here, EmployeeDetails is a ValueTuple of four elements id, firstName, lastName, and salary.

Tuple Aliases

Aliases make it easier to associate an identifier with a namespace, type, or member. We are not introducing a new type when creating an alias. Instead, we are creating a synonym for an existing type. 

C# 12 introduced a new feature that allows us to create aliases for tuple types with the help of using directive.

Before we dive into this new feature, we need to ensure that we are using the latest .Net 8 SDK in our project.

Now let’s see how we can create an alias for a tuple!

We declare an alias EmployeeFinanceDetails for a tuple with four elements (int, string, string, double) with the help of a using directive:

using EmployeeFinanceDetails = (int id, string name, string familyName, double salary);

The scope of the EmployeeFinanceDetails will be the current file.

Now, let’s use a global using directive to create an alias at the assembly level:

global using EmployeeDetails = (int id, string firstName, string lastName, float salary);

We declare EmployeeDetails as an alias for a tuple type with four elements (int, string, string, and float).

After declaring tuple aliases, we can use them as any other type: as a return type, a method parameter type, or a variable type.

Let’s demonstrate this!

We will create instances of our declared tuple aliases and assign values to them:

EmployeeDetails employee = (1, "John","Doe", 116000.25f);
EmployeeFinanceDetails financeDetails = (1, "John", "Doe", 116000.25);

Here, employee and financeDetails are instances from the tuple aliases EmployeeDetails and EmployeFinanceDetails respectively.

We can access their elements using the instance name:

var employeeFullName = $"Employee Full Name: {employee.firstName} {employee.lastName}";

firstName and lastName are the elements from the tuple instance employee.

Precedence Rule for Local and Global Tuple Aliases

When working with aliases, it’s crucial to remember if a local alias shares the same name as a global alias and is used in the same scope, the local alias will take precedence over the global alias. 

In short, within the scope of the local alias, only the local alias is considered even if it shares the same name as a global alias.

The same precedence rule can be applied when concrete types and tuple aliases share the same name.

Consider a scenario where we have a concrete type (class) that shares the same name as a tuple alias. To demonstrate this, let’s create two classes EmployeeFinanceDetails and  EmployeeDetails that share the same name as our local and global tuple aliases respectively, and instantiate them:

using System;
using EmployeeFinanceDetails = (int id, string name, string familyName, double salary);

public class EmployeeFinanceDetails
{
}

public class EmployeeDetails
{
}

public class TupleAlias
{
    public void PrintTupleValues()
    {
        //GLOBAL TUPLE ALIAS
        EmployeeDetails employee = (1, "John", "Doe", 116000.25f);
        
        //LOCAL TUPLE ALIAS
        EmployeeFinanceDetails financeDetails = (1, "John", "Doe", 116000.25);        
    }
}

We’ll encounter compile-time errors while creating instances for both EmployeeFinanceDetails and EmployeeDetails. But, the errors will be different. Creating an instance of EmployeeDetails will result in an implicit type conversion error. On the other hand, creating an instance of EmployeeFinanceDetails will lead to a name conflict error.

The underlying cause for the distinct errors lies in the precedence of local and global aliases. In the scenario of a local class and a global tuple alias EmployeeDetails sharing the same name, the local class takes precedence. This explains why a type conversion error arises instead of a name conflict error which we encountered while creating an instance for our local tuple alias EmployeeFinanceDetails.

Tuple Assignment

Tuples can be assigned to each other as long as they match in arity and member types (exact or implicitly convertible). Only the arity and element type matter in tuples, not the field names themselves.

Let’s use our tuple instances to understand these conditions:

financeDetails = employee;

We are assigning values from the tuple instance employee to financeDetails. This tuple assignment works because float is implicitly convertible to double making these instances match in both arity and type.

However, we will encounter a compile-time implicit conversion error if the assignment is reversed i.e. financeDetails to employee:

employee = financeDetails;

This is due to the fourth element of financeDetails which is of type double. This type is neither the same nor implicitly convertible to float. 

Tuple Aliases and Deconstruction

Tuple deconstruction lets us extract elements and assign them to individual variables. We can use the assignment operator = to deconstruct a tuple instance into separate variables.

We can explicitly declare the type of each variable inside the parenthesis:

(int id, string fName, string lName, float sal) = employee;

Or, we can use a var keyword outside the parentheses to declare implicitly typed variables: 

var (id, fName, lName, sal) = employee;

Here, the compiler will infer the type of each element.

We can use the variable directly instead of tuple fields:

string employeeFullName = fName + " " + lName";

Tuples Aliases vs. Classes

The lightweight and easy-to-write nature of tuples may tempt us to overuse them. We may even consider replacing classes with them to write simple and maintainable code. But there are some downsides to using tuples extensively that we must consider. 

Tuples do not store element names at runtime, so we cannot access them directly using reflection. Additionally, there are separate tuple types for each length of the tuple up to 7. Tuples with more than 7 elements require additional steps to access the remaining elements. Due to this, it is quite impractical to use tuples with a large number of elements.

In contrast to ValueTuple, classes do persist their element names and we can access their element names via reflection at runtime. Classes also offer more flexibility than tuples.

To decide whether we need a class or a tuple, we can ask ourselves:

  • Are we using a design pattern where having a class is significant to the design of our system
  • Do we need a type that describes the behavior that it provides
  • Do we need to access the names of the elements at runtime
  • Do we need the flexibility to add or remove elements in the future

If the answer is yes to any of these questions, then a class may be a better choice. Otherwise, a tuple may be a good option. In short, we should use tuples when design significance is not a factor and we need a lightweight data structure to move information around.

Conclusion

In this article, we learned what tuple aliases are and how to use them. We also explored how to use tuple aliases to write shorter and more readable code, and discussed how to choose between classes and tuples for different situations.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!