Events
17 Mar, 21 - 21 Mar, 10
Join the meetup series to build scalable AI solutions based on real-world use cases with fellow developers and experts.
Register nowThis browser is no longer supported.
Upgrade to Microsoft Edge to take advantage of the latest features, security updates, and technical support.
The tuples feature provides concise syntax to group multiple data elements in a lightweight data structure. The following example shows how you can declare a tuple variable, initialize it, and access its data members:
(double, int) t1 = (4.5, 3);
Console.WriteLine($"Tuple with elements {t1.Item1} and {t1.Item2}.");
// Output:
// Tuple with elements 4.5 and 3.
(double Sum, int Count) t2 = (4.5, 3);
Console.WriteLine($"Sum of {t2.Count} elements is {t2.Sum}.");
// Output:
// Sum of 3 elements is 4.5.
As the preceding example shows, to define a tuple type, you specify types of all its data members and, optionally, the field names. You can't define methods in a tuple type, but you can use the methods provided by .NET, as the following example shows:
(double, int) t = (4.5, 3);
Console.WriteLine(t.ToString());
Console.WriteLine($"Hash code of {t} is {t.GetHashCode()}.");
// Output:
// (4.5, 3)
// Hash code of (4.5, 3) is 718460086.
Tuple types support equality operators ==
and !=
. For more information, see the Tuple equality section.
Tuple types are value types; tuple elements are public fields. That makes tuples mutable value types.
You can define tuples with an arbitrary large number of elements:
var t =
(1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
11, 12, 13, 14, 15, 16, 17, 18,
19, 20, 21, 22, 23, 24, 25, 26);
Console.WriteLine(t.Item26); // output: 26
One of the most common use cases of tuples is as a method return type. That is, instead of defining out
method parameters, you can group method results in a tuple return type, as the following example shows:
int[] xs = new int[] { 4, 7, 9 };
var limits = FindMinMax(xs);
Console.WriteLine($"Limits of [{string.Join(" ", xs)}] are {limits.min} and {limits.max}");
// Output:
// Limits of [4 7 9] are 4 and 9
int[] ys = new int[] { -9, 0, 67, 100 };
var (minimum, maximum) = FindMinMax(ys);
Console.WriteLine($"Limits of [{string.Join(" ", ys)}] are {minimum} and {maximum}");
// Output:
// Limits of [-9 0 67 100] are -9 and 100
(int min, int max) FindMinMax(int[] input)
{
if (input is null || input.Length == 0)
{
throw new ArgumentException("Cannot find minimum and maximum of a null or empty array.");
}
// Initialize min to MaxValue so every value in the input
// is less than this initial value.
var min = int.MaxValue;
// Initialize max to MinValue so every value in the input
// is greater than this initial value.
var max = int.MinValue;
foreach (var i in input)
{
if (i < min)
{
min = i;
}
if (i > max)
{
max = i;
}
}
return (min, max);
}
As the preceding example shows, you can work with the returned tuple instance directly or deconstruct it in separate variables.
You can also use tuple types instead of anonymous types; for example, in LINQ queries. For more information, see Choosing between anonymous and tuple types.
Typically, you use tuples to group loosely related data elements. In public APIs, consider defining a class or a structure type.
You explicitly specify tuple fields names in a tuple initialization expression or in the definition of a tuple type, as the following example shows:
var t = (Sum: 4.5, Count: 3);
Console.WriteLine($"Sum of {t.Count} elements is {t.Sum}.");
(double Sum, int Count) d = (4.5, 3);
Console.WriteLine($"Sum of {d.Count} elements is {d.Sum}.");
If you don't specify a field name, it may be inferred from the name of the corresponding variable in a tuple initialization expression, as the following example shows:
var sum = 4.5;
var count = 3;
var t = (sum, count);
Console.WriteLine($"Sum of {t.count} elements is {t.sum}.");
That's called tuple projection initializers. The name of a variable isn't projected onto a tuple field name in the following cases:
Item3
, ToString
, or Rest
.In the preceding cases, you either explicitly specify the name of a field or access a field by its default name.
The default names of tuple fields are Item1
, Item2
, Item3
and so on. You can always use the default name of a field, even when a field name is specified explicitly or inferred, as the following example shows:
var a = 1;
var t = (a, b: 2, 3);
Console.WriteLine($"The 1st element is {t.Item1} (same as {t.a}).");
Console.WriteLine($"The 2nd element is {t.Item2} (same as {t.b}).");
Console.WriteLine($"The 3rd element is {t.Item3}.");
// Output:
// The 1st element is 1 (same as 1).
// The 2nd element is 2 (same as 2).
// The 3rd element is 3.
Tuple assignment and tuple equality comparisons don't take field names into account.
At compile time, the compiler replaces non-default field names with the corresponding default names. As a result, explicitly specified or inferred field names aren't available at run time.
Tip
Enable .NET code style rule IDE0037 to set a preference on inferred or explicit tuple field names.
Beginning with C# 12, you can specify an alias for a tuple type with a using
directive. The following example adds a global using
alias for a tuple type with two integer values for an allowed Min
and Max
value:
global using BandPass = (int Min, int Max);
After declaring the alias, you can use the BandPass
name as an alias for that tuple type:
BandPass bracket = (40, 100);
Console.WriteLine($"The bandpass filter is {bracket.Min} to {bracket.Max}");
An alias doesn't introduce a new type, but only creates a synonym for an existing type. You can deconstruct a tuple declared with the BandPass
alias the same as you can with its underlying tuple type:
(int a , int b) = bracket;
Console.WriteLine($"The bracket is {a} to {b}");
As with tuple assignment or deconstruction, the tuple member names don't need to match; the types do.
Similarly, a second alias with the same arity and member types can be used interchangeably with the original alias. You could declare a second alias:
using Range = (int Minimum, int Maximum);
You can assign a Range
tuple to a BandPass
tuple. As with all tuple assignment, the field names need not match, only the types and the arity.
Range r = bracket;
Console.WriteLine($"The range is {r.Minimum} to {r.Maximum}");
An alias for a tuple type provides more semantic information when you use tuples. It doesn't introduce a new type. To provide type safety, you should declare a positional record
instead.
C# supports assignment between tuple types that satisfy both of the following conditions:
Tuple element values are assigned following the order of tuple elements. The names of tuple fields are ignored and not assigned, as the following example shows:
(int, double) t1 = (17, 3.14);
(double First, double Second) t2 = (0.0, 1.0);
t2 = t1;
Console.WriteLine($"{nameof(t2)}: {t2.First} and {t2.Second}");
// Output:
// t2: 17 and 3.14
(double A, double B) t3 = (2.0, 3.0);
t3 = t2;
Console.WriteLine($"{nameof(t3)}: {t3.A} and {t3.B}");
// Output:
// t3: 17 and 3.14
You can also use the assignment operator =
to deconstruct a tuple instance in separate variables. You can do that in many ways:
Use the var
keyword outside the parentheses to declare implicitly typed variables and let the compiler infer their types:
var t = ("post office", 3.6);
var (destination, distance) = t;
Console.WriteLine($"Distance to {destination} is {distance} kilometers.");
// Output:
// Distance to post office is 3.6 kilometers.
Explicitly declare the type of each variable inside parentheses:
var t = ("post office", 3.6);
(string destination, double distance) = t;
Console.WriteLine($"Distance to {destination} is {distance} kilometers.");
// Output:
// Distance to post office is 3.6 kilometers.
Declare some types explicitly and other types implicitly (with var
) inside the parentheses:
var t = ("post office", 3.6);
(var destination, double distance) = t;
Console.WriteLine($"Distance to {destination} is {distance} kilometers.");
// Output:
// Distance to post office is 3.6 kilometers.
Use existing variables:
var destination = string.Empty;
var distance = 0.0;
var t = ("post office", 3.6);
(destination, distance) = t;
Console.WriteLine($"Distance to {destination} is {distance} kilometers.");
// Output:
// Distance to post office is 3.6 kilometers.
The destination of a deconstruct expression can include both existing variables and variables declared in the deconstruction declaration.
You can also combine deconstruction with pattern matching to inspect the characteristics of fields in a tuple. The following example loops through several integers and prints those that are divisible by 3. It deconstructs the tuple result of Int32.DivRem and matches against a Remainder
of 0:
for (int i = 4; i < 20; i++)
{
if (Math.DivRem(i, 3) is ( Quotient: var q, Remainder: 0 ))
{
Console.WriteLine($"{i} is divisible by 3, with quotient {q}");
}
}
For more information about deconstruction of tuples and other types, see Deconstructing tuples and other types.
Tuple types support the ==
and !=
operators. These operators compare members of the left-hand operand with the corresponding members of the right-hand operand following the order of tuple elements.
(int a, byte b) left = (5, 10);
(long a, int b) right = (5, 10);
Console.WriteLine(left == right); // output: True
Console.WriteLine(left != right); // output: False
var t1 = (A: 5, B: 10);
var t2 = (B: 5, A: 10);
Console.WriteLine(t1 == t2); // output: True
Console.WriteLine(t1 != t2); // output: False
As the preceding example shows, the ==
and !=
operations don't take into account tuple field names.
Two tuples are comparable when both of the following conditions are satisfied:
t1 != t2
doesn't compile if t1
and t2
have different numbers of elements.==
and !=
operators. For example, (1, (2, 3)) == ((1, 2), 3)
doesn't compile because 1
isn't comparable with (1, 2)
.The ==
and !=
operators compare tuples in short-circuiting way. That is, an operation stops as soon as it meets a pair of non equal elements or reaches the ends of tuples. However, before any comparison, all tuple elements are evaluated, as the following example shows:
Console.WriteLine((Display(1), Display(2)) == (Display(3), Display(4)));
int Display(int s)
{
Console.WriteLine(s);
return s;
}
// Output:
// 1
// 2
// 3
// 4
// False
Typically, you refactor a method that has out
parameters into a method that returns a tuple. However, there are cases in which an out
parameter can be of a tuple type. The following example shows how to work with tuples as out
parameters:
var limitsLookup = new Dictionary<int, (int Min, int Max)>()
{
[2] = (4, 10),
[4] = (10, 20),
[6] = (0, 23)
};
if (limitsLookup.TryGetValue(4, out (int Min, int Max) limits))
{
Console.WriteLine($"Found limits: min is {limits.Min}, max is {limits.Max}");
}
// Output:
// Found limits: min is 10, max is 20
C# tuples, which are backed by System.ValueTuple types, are different from tuples that are represented by System.Tuple types. The main differences are as follows:
System.ValueTuple
types are value types. System.Tuple
types are reference types.System.ValueTuple
types are mutable. System.Tuple
types are immutable.System.ValueTuple
types are fields. Data members of System.Tuple
types are properties.For more information, see:
.NET feedback
.NET is an open source project. Select a link to provide feedback:
Events
17 Mar, 21 - 21 Mar, 10
Join the meetup series to build scalable AI solutions based on real-world use cases with fellow developers and experts.
Register now