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.
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.
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.