Tuple
In a C# program, we store things (like strings or ints) together. A name might be associated with an index. Tuples help us keep our logic clear and simple.
Tuple
detailsIn .NET we have 2 kinds of tuples—the regular tuple and the ValueTuple
which uses a more modern syntax. Both tuple types work well.
Consider that the Tuple
type is a class
. Once we create the Tuple
, we cannot change the values of its fields. This makes the Tuple
more like a string
.
Tuple
constructor. We read the Item1
, Item2
and Item3
properties and test them in if
-statements.using System; // Version 1: create three-item tuple. Tuple<int, string, bool> tuple = new Tuple<int, string, bool>(1, "cat", true); if (tuple.Item1 == 1 && tuple.Item2 == "cat" && tuple.Item3 == true) { Console.WriteLine(tuple); } // Version 2: use value tuple syntax. var tuple2 = (1, "cat", true); if (tuple2.Item1 == 1 && tuple2.Item2 == "cat" && tuple2.Item3 == true) { Console.WriteLine(tuple2); }(1, cat, True) (1, cat, True)
Continuing on, a Tuple
can have more complex items inside it, such as arrays. We can also pass the Tuple
to other methods.
Tuple
with two arrays—string
and int
arrays.Tuple
variable to another method.var
keyword? The reason is pure syntactic sugar. Var shortens the lines in the code example.using System; class Program { static void Main() { // Create four-item tuple. // ... Use var implicit type. var tuple = new Tuple<string, string[], int, int[]>("perl", new string[] { "java", "c#" }, 1, new int[] { 2, 3 }); // Pass tuple as argument. M(tuple); } static void M(Tuple<string, string[], int, int[]> tuple) { // Evaluate the tuple's items. Console.WriteLine(tuple.Item1); foreach (string value in tuple.Item2) { Console.WriteLine(value); } Console.WriteLine(tuple.Item3); foreach (int value in tuple.Item4) { Console.WriteLine(value); } } }perl java c# 1 2 3
Tuples have different names. A sextuple has 6 items. To create a sextuple, use the Tuple
constructor. You have to specify each type of the items in the type parameter list.
using System; class Program { static void Main() { var sextuple = new Tuple<int, int, int, string, string, string>(1, 1, 2, "dot", "net", "perls"); Console.WriteLine(sextuple); } }(1, 1, 2, dot, net, perls)
Tuple.Create
Next we invoke this method. We use Create()
with three arguments: a string
literal, an integer and a boolean value.
Create()
method returns a class
instance of type Tuple
that has 3 typed items.Tuple
. It tests Item1
, Item2
and Item3
.using System; class Program { static void Main() { // Use Tuple.Create static method. var tuple = Tuple.Create("cat", 2, true); // Test value of string. string value = tuple.Item1; if (value == "cat") { Console.WriteLine(true); } // Test Item2 and Item3. Console.WriteLine(tuple.Item2 == 10); Console.WriteLine(!tuple.Item3); // Write string representation. Console.WriteLine(tuple); } }True False False (cat, 2, True)
There is no elaborate algorithm devoted to tuple creation. The Tuple.Create
method calls a constructor and returns a reference.
Tuple.Create
. It might have more pleasing syntax.public static Tuple<T1> Create<T1>(T1 item1) { return new Tuple<T1>(item1); }
Class
, read-onlyTuple
is not a struct
—it is a class
. It will be allocated upon the managed heap. Its items like Item1
, Item2
are read-only properties.
Item1
) after the constructor has run.Item1
, Item2
and further do not have setters. We cannot assign them. A Tuple
is immutable once created in memory.using System; class Program { static void Main() { var tuple = new Tuple<int, string>(200, "Greece"); // This will not work. tuple.Item1 = 300; } }Property or indexer 'System.Tuple...Item1' cannot be assigned to--it is read-only.
Sort
Tuples can be sorted. A Tuple
is a great way to encapsulate units of data, but it can make sorting harder—a Comparison
delegate is needed.
List
and adds 3 new Tuple
instances to it. We invoke the Sort
method on the List
.CompareTo
on the Item2
string
property.int
, change the lambda to compare "b" to "a" rather than "a" to "b".using System; using System.Collections.Generic; class Program { static void Main() { List<Tuple<int, string>> list = new List<Tuple<int, string>>(); list.Add(new Tuple<int, string>(1, "cat")); list.Add(new Tuple<int, string>(100, "apple")); list.Add(new Tuple<int, string>(2, "zebra")); // Use Sort method with Comparison delegate. // ... Has two parameters; return comparison of Item2 on each. list.Sort((a, b) => a.Item2.CompareTo(b.Item2)); foreach (var element in list) { Console.WriteLine(element); } } }(100, apple) (1, cat) (2, zebra)
This is a classic problem—a method may need to return many things, not just one. A tuple can return multiple values (with less code than a class
would require).
ref
and out parameters would be faster for a method that is hot.Tuple
has advantages. It is a reference and can be reused. Less copying is needed when passed to other methods.using System; class Program { static Tuple<string, int> NameAndId() { // This method returns multiple values. return new Tuple<string, int>("Purple Dinosaur", 700); } static void Main() { var result = NameAndId(); string name = result.Item1; int id = result.Item2; // Display the multiple values returned. Console.WriteLine(name); Console.WriteLine(id); } }Purple Dinosaur 700
ValueTuple
This type has clear advantages over Tuple
. We can specify a ValueTuple
by including values in an expression (with no type names).
Console.WriteLine
.using System; class Program { static void Main() { var values = (10, "bird", "plane"); Console.WriteLine(values); Console.WriteLine(values.Item1); Console.WriteLine(values.Item2); Console.WriteLine(values.Item3); } }(10, bird, plane) 10 bird plane
ToValueTuple
We can convert a Tuple
into its equivalent ValueTuple
form. The ValueTuple
will have some performance advantages, and also simpler syntax.
using System; class Program { static void Main() { var tuple = new Tuple<int, string>(-20, "Bolivia"); // Convert tuple to a value tuple, and pass it to a method. Print(tuple.ToValueTuple()); } static void Print((int, string) items) { // Print value tuple. Console.WriteLine(items); } }(-20, Bolivia)
Suppose we have a tuple, and want to test if the items in the tuple have specific constant values. We can use pattern matching with the is
-operator to test this.
using System; var values = ("bird", 10, false); // Try to match the tuple with is-operator. if (values is ("frog", 20, true)) { Console.WriteLine("???"); } else if (values is ("bird", 10, false)) { Console.WriteLine("Is bird, 10, false!"); }Is bird, 10, false!
Consider 4 possible performance tests: allocation, argument passing, returning, and loading a field. We test the performance of 3 types with these 4 tests.
Tuple
in the 4 performance tests. We only use 2-item objects in this test.KeyValuePair
struct
instead of a Tuple
class
instance as part of the benchmark.ValueTuple
literal syntax.ValueTuples
tend to perform as well as anything in the tests. Tuples can be used as part of fast code.using System; using System.Collections.Generic; using System.Diagnostics; using System.Runtime.CompilerServices; class Program { static void Main() { Allocation(); Argument(); Return(); Load(); } static void Allocation() { // Time allocating the object. const int max = 1000000; var a = new Tuple<string, string>("", ""); var b = new KeyValuePair<string, string>("", ""); var c = ("", ""); var s1 = Stopwatch.StartNew(); // Version 1: allocate Tuple. for (var i = 0; i < max; i++) { var tuple = new Tuple<string, string>("cat", "dog"); } s1.Stop(); var s2 = Stopwatch.StartNew(); // Version 2: allocate KeyValuePair. for (var i = 0; i < max; i++) { var pair = new KeyValuePair<string, string>("cat", "dog"); } s2.Stop(); var s3 = Stopwatch.StartNew(); // Version 3: allocate tuple literal. for (var i = 0; i < max; i++) { var pair = ("cat", "dog"); } s3.Stop(); Console.WriteLine(((double)(s1.Elapsed.TotalMilliseconds * 1000000) / max) + " allocation, Tuple"); Console.WriteLine(((double)(s2.Elapsed.TotalMilliseconds * 1000000) / max) + " allocation, KeyValuePair"); Console.WriteLine(((double)(s3.Elapsed.TotalMilliseconds * 1000000) / max) + " allocation, Tuple literal"); Console.WriteLine(); } static void Argument() { // Time passing the object as an argument to a function. const int max = 10000000; var a = new Tuple<string, string>("", ""); var b = new KeyValuePair<string, string>("", ""); var c = ("", ""); X(a); X(b); X(c); var s1 = Stopwatch.StartNew(); for (var i = 0; i < max; i++) { X(a); } s1.Stop(); var s2 = Stopwatch.StartNew(); for (var i = 0; i < max; i++) { X(b); } s2.Stop(); var s3 = Stopwatch.StartNew(); for (var i = 0; i < max; i++) { X(c); } s3.Stop(); Console.WriteLine(((double)(s1.Elapsed.TotalMilliseconds * 1000000) / max) + " argument, Tuple"); Console.WriteLine(((double)(s2.Elapsed.TotalMilliseconds * 1000000) / max) + " argument, KeyValuePair"); Console.WriteLine(((double)(s3.Elapsed.TotalMilliseconds * 1000000) / max) + " argument, Tuple literal"); Console.WriteLine(); } static void Return() { // Time returning the object itself. const int max = 10000000; var a = new Tuple<string, string>("", ""); var b = new KeyValuePair<string, string>("", ""); var c = ("", ""); Y(a); Y(b); Y(c); var s1 = Stopwatch.StartNew(); for (var i = 0; i < max; i++) { Y(a); } s1.Stop(); var s2 = Stopwatch.StartNew(); for (var i = 0; i < max; i++) { Y(b); } s2.Stop(); var s3 = Stopwatch.StartNew(); for (var i = 0; i < max; i++) { Y(c); } s3.Stop(); Console.WriteLine(((double)(s1.Elapsed.TotalMilliseconds * 1000000) / max) + " return, Tuple"); Console.WriteLine(((double)(s2.Elapsed.TotalMilliseconds * 1000000) / max) + " return, KeyValuePair"); Console.WriteLine(((double)(s3.Elapsed.TotalMilliseconds * 1000000) / max) + " return, Tuple literal"); Console.WriteLine(); } static void Load() { // Time accessing an element. const int max = 10000000; var a = new Tuple<string, string>("cat", "dog"); var b = new KeyValuePair<string, string>("cat", "dog"); var c = ("cat", "dog"); var list1 = new List<Tuple<string, string>>(); list1.Add(a); Z(list1); var list2 = new List<KeyValuePair<string, string>>(); list2.Add(b); Z(list2); var list3 = new List<(string, string)>(); list3.Add(c); Z(list3); var s1 = Stopwatch.StartNew(); for (var i = 0; i < max; i++) { Z(list1); } s1.Stop(); var s2 = Stopwatch.StartNew(); for (var i = 0; i < max; i++) { Z(list2); } s2.Stop(); var s3 = Stopwatch.StartNew(); for (var i = 0; i < max; i++) { Z(list3); } s3.Stop(); Console.WriteLine(((double)(s1.Elapsed.TotalMilliseconds * 1000000) / max) + " load, Tuple"); Console.WriteLine(((double)(s2.Elapsed.TotalMilliseconds * 1000000) / max) + " load, KeyValuePair"); Console.WriteLine(((double)(s3.Elapsed.TotalMilliseconds * 1000000) / max) + " load, Tuple literal"); Console.WriteLine(); } [MethodImpl(MethodImplOptions.NoInlining)] static void X(Tuple<string, string> a) { // This and following methods are used in the benchmarks. } [MethodImpl(MethodImplOptions.NoInlining)] static void X(KeyValuePair<string, string> a) { } [MethodImpl(MethodImplOptions.NoInlining)] static void X((string, string) a) { } [MethodImpl(MethodImplOptions.NoInlining)] static Tuple<string, string> Y(Tuple<string, string> a) { return a; } [MethodImpl(MethodImplOptions.NoInlining)] static KeyValuePair<string, string> Y(KeyValuePair<string, string> a) { return a; } [MethodImpl(MethodImplOptions.NoInlining)] static (string, string) Y((string, string) a) { return a; } static char Z(List<Tuple<string, string>> list) { return list[0].Item1[0]; } static char Z(List<KeyValuePair<string, string>> list) { return list[0].Key[0]; } static char Z(List<(string, string)> list) { return list[0].Item1[0]; } }8.3944 allocation, Tuple 0.4949 allocation, KeyValuePair 0.3457 allocation, Tuple literal (FASTEST) 2.16168 argument, Tuple 2.17551 argument, KeyValuePair 2.17316 argument, Tuple literal 1.84421 return, Tuple (FASTEST) 5.42422 return, KeyValuePair 5.32932 return, Tuple literal 2.44545 load, Tuple 3.27982 load, KeyValuePair 2.56207 load, Tuple literal
The Tuple
is a typed, immutable, generic construct. Tuples are helpful—they can store related data. For important things, a simple class
is easier to use and document.