Tuples are great additions to C#. They're simple immutable structures made of a small number of members organized in a specific order. For example, a point on a plane could be represented as a pair of X and Y coordinates, or a person's name could be represented as a title, first, middle and last names. If the types are that simple, and have no custom logic associated with them, using a tuple looks like a lightweight and simple implementation that requires a lot less effort than a simple class or struct. Dynamic and functional languages have long enjoyed tuples, of course, but I like how the C# implementation manages to stay true to the core tenets of the language, in particular type safety.
However, I personally cringe a little every time I see a public API returning a tuple. I really, really dislike them as return types. Let me explain why.
As a user of a method, you will use what you know of the return type, and use the data contained therein according to your needs. There's a whole category of backwards-compatible changes to a class, such as adding a new member, that are trivial with classes and require no change or even recompilation of client code, that become very difficult with tuples.
This comes from the fact that with tuples, you need to copy the very definition of the type everywhere you use it, whereas with a class, you only use its name as a reference to the type. Almost no refactoring is possible with tuples without finding all of the code in the world that uses your tuple return type, and modifying it as well. This is at best impractical. Tooling can't even help you much beyond CTRL+SHIFT+F and finding all references to the method. I know from experience that tracking down those types in a large codebase can be far from trivial.
It's actually less effort in the end to write a simple class. I have a lot of hope for records (vote) as a better option to use wherever you need lightweight types, but in the meantime, I prefer in most cases to write a simple class rather than to return a tuple.
There's also the fact that a named type can in most cases better convey the semantics and intentions of your code than a tuple, which is pure structure. Finally, the temptation to write Cthulhuesque abominations such as
IEnumerable<(string, IDictionary<string, (string, string)>)> exists. When you have to put the name of your method on a different line than its return type, you know you've gone too far.
So at this point, you may ask, what do I still think tuples are good for? That is a fair question... I have several answers to that.
First, I don't think tuples should always be avoided as return types. When the semantics of a method are to manipulate or produce a tuple, they are more than acceptable, in fact nothing else makes sense:
I think tuples are also fantastic at simplifying code that produces and processes intermediary results. I particularly like to use them as return types of Lambdas in LINQ expressions:
This totally non-optimized, allocation-happy and non parameter validating piece of code conveys the idea: it uses a simple tuple as an intermediary structure between the
Zip call and the
SelectMany. The tuple brings real value here, as doing the same thing without it would be considerably more tedious and less expressive (let's put aside the existence of a
Pair class that could be used in this simple case, the general point remains). The proximity of the code that produces the tuple to the code that consumes it, associated with a tight scope means none of the objections I have to using tuples as public API return types apply here. It's pure goodness.
This is really lovely, and the good news is that tuples are not the only types this works on: you can add type-safe deconstructing capabilities to your own classes by implementing something like this:
GetMiddlePoint deconstruction sample above will work exactly the same whether the method returns a
(double, double) tuple or an instance of a
Point class that implements a
Deconstruct method. This is a great way to refactor a tuple-returning method to return an instance of a class instead, while keeping client code untouched (recompiling may be necessary). To me this is the best of both worlds: a deconstruct-aware class remains easy to refactor cleanly, and can still present the greatly expressive deconstruction capabilities of a tuple.