Dissecting the tuples in C# 7


System.Tuple types were introduced in .NET 4.0 with two significant drawbacks: (1) tuple types are classes and (2) there was no language support for constructing/deconstructing them. To solve these issues, C# 7 introduces new language feature as well as a new family of types (*).

Today, if you need to glue together two values to return them from a function or put two values in a hash set you can use System.ValueTuple types and construct them using a handy syntax:

// Constructing the tuple instance
var tpl = (1, 2
);
           

// Using tuples with a dictionary
var d = new Dictionary<(int x, int y), (byte a, short
b)>();

// Tuples with different names are compatible
d.Add(tpl, (a: 3, b: 4
));

// Tuples have value semantic
if (d.TryGetValue((1, 2), out var
r))
{
   
// Deconstructing the tuple ignoring the first element
    var (_, b) =
r;
               
   
// Using named syntax as well as predefined name
    Console.WriteLine($"a: {r.a}, b: {r.Item2}");
}

(*) System.ValueTuple types are introduced in .NET Framework 4.7. But you still can use the feature and target lower framework versions, in this case, you have to reference a special nuget package: System.ValueTuple.

  • Tuple declaration syntax is similar to function parameter declaration: (Type1 name1, Type2 name2).
  • Tuple construction syntax is similar to argument construction: (value1, optionalName: value2).
  • Two tuples with the same element types but with different names are compatible (**): (int a, int b) = (1, 2).
  • Tuples have value semantic: (1,2).Equals((a: 1, b: 2)) and (1,2).GetHashCode() == (1,2).GetHashCode() are both true.
  • Tuples do not support == and !=. There is a pending discussion about it on github: "Support for == and != on tuple types".
  • Tuples can be "deconstructed" but only into "variable declaration" but not into "out var" or in the case block: var (x, y) = (1,2) - OK, (var x, int y) = (1,2) - OK, dictionary.TryGetValue(key, out var (x, y)) - not OK, case var (x, y): break; - not OK.
  • Tuples are mutable: (int a, int b) x (1,2); x.a++;.
  • Tuple elements can be accessed by the name (if provided) or via generic names like Item1, Item2 etc.

(**) We'll see when this is not the case in a moment.

Tuple element names

Lack of user-defined names makes System.Tuple types not very useful. I can use System.Tuple as an implementation detail of a small method but if I need to pass it around I prefer a named type with descriptive property names. New tuple feature addresses this issue quite elegantly: you can specify names for tuple elements and unlike anonymous classed these names are available even across different assemblies.

The C# compiler emits a special attribute TupleElementNamesAttribute(***) for each tuple type used in a method signature:

(***) The attribute TupleElementNamesAttribute is special and can't be used directly in the user code. The compiler emits an error if you try to use it.

public (int a, int b) Foo1((int c, int d) a) => a;

[
return: TupleElementNames(new[] { "a", "b" })]
public ValueTuple<int, int
> Foo(
    [
TupleElementNames(new[] { "c", "d" })] ValueTuple<int, int
> a)
{
   
return a;
}

This helps an IDE and the compiler to "see" what the element names are and warn if they used incorrectly:

// Ok: tuple literal can skip element names
(int x, int y) tpl = (1, 2
);

// Warning: The tuple element 'a' is ignored because a different name
// or no name is specified by the target type '(int x, int y)'.

tpl = (a:1, b:2
);

// Ok: tuple deconstruction ignore element names
var (a, b) =
tpl;

// x: 2, y: 1. Tuple names are ignored
var (y, x) = tpl;

The compiler has stronger requirements for inherited members:

public abstract class Base
{
   
public abstract (int a, int
b) Foo();
   
public abstract (int, int
) Bar();
}

public class Derived : Base
{
   
// Error: Cannot change tuple element names when overriding method
    public override (int c, int d) Foo() => (1, 2
);
   
// Error: Cannot change tuple element names when overriding method
    public override (int a, int b) Bar() => (1, 2);
}

Regular method arguments can be freely changed in overriden members, tuple element names in overriden members should exactly match ones from a base type.

Element name inference

C# 7.1 introduces one additional enhancement: element name inference similar to what C# does for anonymous types.

public void NameInference(int x, int y)
{
   
// (int x, int y)
    var tpl =
(x, y);

   
var a = new {X = x, Y =
y};

   
// (int X, int y)
    var tpl2 = (a.X, a.Y);
}

Value semantic and mutability

Tuples are mutable value types with elements as public fields. This sounds concerning because we know that mutable value types considered harmful. Here is a small example of their evil nature:

var x = new { Items = new List<int> { 1, 2, 3 }.GetEnumerator() };
while (x.Items.
MoveNext())
{
   
Console.WriteLine(x.Items.Current);
}

If you'll run this code you'll get ... an infinite loop. List<T>.Enumerator is a mutable value type but Items is a property. This means that x.Items returns a copy of the original iterator on each loop iteration causing an infinite loop.

But mutable value types are dangerous only when the data is mixed with a behavior: an enumerator holds a state (current element) and has a behavior (an ability to advance an iterator by calling MoveNext method). This combination can cause issues because it's so easy to call a method on a copy instead of on an original instance -- causing effectively no-op. Here is a set of examples that can cause an unobvious behavior due to a hidden copy of a value type: gist.

But one issue with mutability still remains:

var tpl = (x: 1, y: 2);
var hs = new HashSet<(int x, int
y)>();
hs
.
Add(tpl);

tpl
.x++;
Console.WriteLine(hs.Contains(tpl));
// false

Tuples are very useful as keys in dictionaries and can be stored in hash sets due to a proper value semantics. But you should not mutate the state of a tuple variable between different operations with the collection.

Deconstruction

Even though the tuple construction is special to the tuples, deconstruction is generic and can be used with any type.

public static class VersionDeconstrucion
{
   
public static void Deconstruct(this Version v, out int major, out int minor, out int build, out int
revision)
    {
        major
= v.
Major;
        minor
= v.
Minor;
        build
= v.
Build;
        revision
= v.
Revision;
    }
}


var version = Version.Parse("1.2.3.4");
var (major, minor, build, _) =
version;

// Prints: 1.2.3
Console.WriteLine($"{major}.{minor}.{build}");

 

Deconstruction uses "duck-typing" approach: if the compiler can find a method called Deconstruct for a given type - instance method or an extension method - the type is deconstructable.

Aliasing tuples

Once you start using the tuples, you'll quickly realize that you want to "reuse" a tuple type with named elements in multiple places in your source code. But there are few issues with that. First, C# does not support global aliases for a given type. You can use 'using' alias directive, but it creates an alias visible in one file. And second, you can't even alias the tuple:

// You can't do this: compilation error
using Point = (int x, int
y);

// But you *can* do this
using SetOfPoints = System.Collections.Generic.HashSet<(int x, int y)>;

There is a pending discussion on github at "Tuple types in using directives". So if you'll find yourself using one tuple type in multiple places you have two options: keep copy-pasting or create a named type.

What casing for elements should I use?

Here is an interesting question: what casing rule we should follow for tuple elements? Pascal case like ElementName or camel case like elementName? On one hand, tuple elements should follow the naming rule for public members (i.e. PascalCase), but on the other hand, tuples are just bags with variables and variables are camel cased.

You may consider using a different naming scheme based on the usage and use PascalCase if a tuple is used as an argument or return type of a method and use camelCase if a tuple is created locally in the function. But I prefer to use camelCase all the time.

Conclusion

I've found tuples very useful in my day to day job. I need more than one return value from a function, or I need to put a pair of values into a hashset, or I need to change a dictionary and keep not the one value but two, or the key becomes more complicated and I need to extend it with another "field".

I even use them to avoid closure allocation with methods such a ConcurrentDictionary.TryGetOrAdd that now takes an extra argument. And in many cases the state is a tuple as well.

The feature is very useful but I really want to see a few enhancements:

  1. Global aliases: an ability to "name" a tuple and use them in the whole assembly (****).
  2. Deconstruct a tuple in the pattern matching: in out var and in case var.
  3. Use operator == for equality comparison.

(****) I know that this feature is debatable, but I think it'll be very useful. We can wait for record types, but I'm not sure if the records will be value types or reference types.


Comments (7)

  1. Great article, thanks!
    Would be great to know more about planned (?) improvements in tuple serialization. The biggest problem with it I experience right now is that aliased tuple (int foo, int bar) = (4, 2) serializes into {“Item1″: 4,”Item2”:2}.
    As far as I understand, that is because of the absence of aliases in compiled code (aliases as syntax sugar implementation, attribute with return string[]) which makes accessing tuple aliases through reflection quite impossible.

    So, are there planned improvements for that?

    1. > So, are there planned improvements for that?
      I don’t think so.

      I think the name erasure is a feature that makes tuple types compatible. The only possible “solution” to your problem is to wait for record types, IMO. I don’t think that tuples will ever change this behavior (but you can open an issue on github/dotnet/roslyn repo to make sure this is indeed the case).

  2. pips says:

    Why use brackets for initialization when everything else uses braces?

    1. Braces for classes, and brackets – for arguments. The idea that tuples are very similar to argument list and in this case using the same syntax makes a perfect sense, IMO.

  3. Maxim says:

    Thanks for another great article, Sergey.

    I’d like also to share with you my small benchmarks related to usage Tuples as collection keys: https://fsou1.github.io/Yet_another_mapper/#кеширование

    Here you could notice that in my case the performance of custom reference type which is used as a key of a collection a little bit better than a Tuple analogue. The source code of benchmark is also available here: https://github.com/FSou1/FsMapper/blob/master/benchmarks/FsMapper.Benchmarks/Storage/DelegateStorageRetrieve.cs

    1. > I’d like also to share with you my small benchmarks related to usage Tuples as collection keys: https://fsou1.github.io/Yet_another_mapper/#кеширование

      The reason for performance difference lies in implementation details of generics. You can get exactly the same results if instead of using a special type `TypeTuple` you’ll add a custom 2 elements tuple like this: class TypeTuple and inside `Equals` method compare `Item1` and `Item2` with `EqualityComparer.Default.Equals(this.Source, other.Source)`.

      I’ve run the following benchmarks:
      1. System.ValueTuple
      2. TypeTuple (that I’ve changed Equals method to use `EqualityComparer.Default.Equals`
      3. GenericTypeTuple class
      4. GenericStructTuple struct with Equals method that calls Equals directly
      5. System.Tuple

      Here is a gist: https://gist.github.com/SergeyTeplyakov/6b6c118650204b7f085b7b4ae41cd848

      And results:

      DictionaryTuple | 101.18 ns | 0.4298 ns | 0.3810 ns | – | 0 B |
      DictionaryTypeTuple | 72.63 ns | 0.2599 ns | 0.2304 ns | 0.0101 | 32 B |
      DictionaryGenericTypeTuple | 99.30 ns | 1.7590 ns | 1.6454 ns | 0.0101 | 32 B |
      DictionaryStructTypeTuple | 48.71 ns | 0.1830 ns | 0.1429 ns | 0.0101 | 32 B |
      DictionarySystemTuple | 132.99 ns | 0.4867 ns | 0.3800 ns | 0.0100 | 32 B |

      As you can see no there is almost no difference between CustomGenericTupleClass and ValueTuple.

      The moral of this story is simple: generics in .NET are not perfect, and there is nothing to do with tuples directly.

      1. fsou11 says:

        Thank you for reply, i think i got it.

Skip to main content