Well, here we are; the final post in this series on the new C#7 features. Over the last six weeks we have covered all but one, including:
- Binary literals
- Numeric literal digit separators
- out variables
- throw expressions
- Expression-bodied constructors
- Expression-bodied finalizers
- Expression-bodied property accessors
- Pattern matching
- Local functions
- Generalized async return types
- ref locals and returns
While pattern matching is exciting, local functions are useful, and some of the other enhancements are nice, I personally feel that I have saved the best for last, because this week it is time to look at tuples.
Tuples
Wait, don't we already have tuples in C#? I mean, we totally have a type called Tuple so it sure seems like it. In fact, we have at least one other tuple-type too, KeyValuePair<TKey,TValue>. However, these are not C# concepts, they are .NET types and they fall pretty short of being the kind of tuples we want. I did not know what was really wrong with them until I learned about the tuples we get with C#7.
It turns out, the old Tuple type is limited; the members have fixed names (Item1, Item2, etc.) which do not communicate their purpose, it is a reference type and as such can have a measurable impact on performance due to object allocations, and it has no syntactical support within the language1. To support the new C#7 feature, there is a new tuple type that is more performant and better suited to the awesomeness that is C#7 tuples; it is a value-type called ValueTuple and it comes with some friends like TupleElementNamesAttribute to give us something wonderful.
If you want to play with C#7 tuples in Visual Studio 2017 RC, you'll need to grab the NuGet package so that the appropriate types to support tuples are available for the compiler. LINQPad Beta already includes everything you need.
Syntax
To define a new tuple in C#7, we use parentheses in an assignment like this:
var myTuple = ("John", "Smith", 45, ConsoleColor.DarkRed);
This creates a new tuple that looks an awful lot like something we might have created using Tuple.Create("John", "Smith", 45, ConsoleColor.DarkRed), since the values of the tuple are referenced via Item1, Item2, Item3, and Item4. That does not seem like a particular useful addition. However, we can make this tuple so much better with some simple changes to our assignment, like this:
var myTupleOfAwesomeness = ( FirstName: "John", LastName: "Smith", Age: 45, FavouriteConsoleColor: ConsoleColor.DarkRed);
Now, instead of the ItemX members, we can access our values using FirstName, LastName, Age, and FavouriteConsoleColor, as if we had instead created an anonymous type. Of course, the great thing is that we did not introduce a new type to do this; the C# compiler pulled some magic along with the new ValueTuple type2 . Of course, it does not end there; we can take advantage of a new feature called deconstruction to name the fields on the left of the assignment instead:
(string FirstName, string LastName, int Age, ConsoleColor FavouriteConsoleColor) myTupleOfAwesomeness = ("John", "Smith", 45, ConsoleColor.DarkRed);
In fact, we can provide names on both sides although the ones provided on the left take precedent and you will get a compiler warning. However, being able to provide names via the right-hand side of an assignment is useful when a method returns a tuple and you want to provide some meaningful names locally. This is even more useful when you find out that you can drop the tuple variable name:
(string FirstName, string LastName, int Age, ConsoleColor FavouriteConsoleColor) = ( "John", "Smith", 45, ConsoleColor.DarkRed);
We basically just assigned values to four local variables that we can then use in the rest of our code, but if we had a method like this:
ValueTuple<string,string,int,ConsoleColor> GetPerson() { return ("John", "Smith", 45, ConsoleColor.DarkRed); }
We could do this on the right-hand side of that assignment:
(var firstName, var lastName, var age, var favouriteConsoleColor) = GetPersonInfo();
What we did there was deconstruct the returned tuple into four separate fields. Our method declares its return type as ValueType<string, string, int, ConsoleColor> and returns a tuple, then we assign that tuple to four variables by using the deconstruction syntax. In this example, I even switched to using var instead of explicitly typing the deconstruction. The var keyword can be used for all of the deconstructed fields, just a couple with the remainder strongly typed, or none; it's up to you and your code reviewers.
Deconstruction is going to change the way you write methods and some of your types, and will probably mean a lot less uses for out variables. That said, do not get carried away. Tuples are best for internal and private methods rather than public APIs where a static type is much more helpful to the consumer of your API.
The Deconstruct Method
If a method returns a tuple (e.g. return ("John", "Smith")), we can use deconstruction to assign its parts to different variables if we wish. However, deconstruction is not reserved for just tuples. We can also define how our own types can be deconstructed into separate variables. To do this, we define a Deconstruct method on our type that tells the compiler how to deconstruct it into separate values. For example:
public class Person { public Person(string firstName, string lastName, int age, ConsoleColor favouriteConsoleColor) { FirstName = firstName; LastName = lastName; Age = age; FavouriteConsoleColor = favouriteConsoleColor; } public void Deconstruct(out string firstName, out string lastName, out int age, out ConsoleColor favouriteConsoleColor) { firstName = FirstName; lastName = LastName; age = Age; favouriteConsoleColor = FavouriteConsoleColor; } public string FirstName { get; } public string LastName { get; } public int Age { get; } public ConsoleColor FavouriteConsoleColor { get; } }
With this type defined, we can use the deconstruction syntax with it just as we would with a ValueTuple:
var person = new Person("John", "Smith", 45, ConsoleColor.DarkRed); (string firstName, string lastName, int age, ConsoleColor favouriteConsoleColor) = person;
I am not sure when I will use this ability to define deconstruction for my types, but I can imagine when the moment arises I will know. Also, you can see from its extensive use in the Deconstruct method, out is certainly not redundant.
Anonymous Types
new {a, b} // -or- (a, b)
Tuples in C#7 seem to out-do anonymous types in every way; tuples don't define a new type, they can be returned from methods without the overheads of object or dynamic, they support deconstruction, and they take up less typing; yet as far as I can tell they can be used everywhere that anonymous types can be used. It is likely that I am missing something important, but from my perspective thus far, I think tuples in C#7 not only kills off any new uses of the Tuple type, but also anonymous types.
In Conclusion
I really like this feature and the simplicity of its usage. It is light on extra syntax, adding the concept of a parenthetically-typed variable, but powerful in what it allows us to do with method returns and type deconstruction. I would like to give it some more thought and use it in more scenarios, but I also think tuples in C#7 will render anonymous types obsolete.
You can skim the overview of tuples in the What's new in C# 7 document, however, this is a simple feature with a complex background, so I highly recommend reading the more in-depth coverage on the Tuples official documentation, especially with regards to assignment of tuples with explicit member names3. It is a shame that it took two attempts to get tuples to be where they should be in C#, leaving yet another type4 by the wayside, but they are here at last and we will all be grateful.
What do you think? Are anonymous types relegated to legacy code only? Will Tuple.Create ever get used again? Is there something I should cover that is missing here or in the official documentation? Sound off in the comments and let me know.
- C# Concepts: Tuples [↩]
- Actually, like dynamic, the field names use an attribute to communicate the names of each tuple field [↩]
- Hint: member names are not attached to the value, but instead stay with the variable for which they were declared [↩]
- Tuple.Create may never get used again [↩]