Summary#
A function that has two things to say should not have to invent a type for them. Declare the return as a tuple:
class Parser {
public (bool ok, decimal value) Try(string s) {
if (s == "1") { return (true, 1m); }
return (false, 0m);
}
}The caller takes both:
decimal Read(string s) {
var p = new Parser();
var (ok, value) = p.Try(s);
return ok ? value : 0m;
}Signature#
(T1 name1, T2 name2) Fn(…) { … } // a tuple RETURN type; names are optional
return (expr1, expr2); // a tuple LITERAL
var (a, b) = Fn(…); // DECONSTRUCTION into new locals
(T1 a, T2 b) t = Fn(…); // …or a typed local holding the whole tuple
t.name1 // an element by the name written on the type
t.Item1 // …or by position, 1-basedA tuple has at least two elements. Element names are optional ((bool, decimal) is the same type) and may differ
between declarations.
Description#
out parameters are a return#
C#'s TryParse shape writes its extra values back through parameters, and you may write it exactly that way:
declare out parameters on a function or a class method, and call it with out var x, out int x, out x (an
existing variable) or out _. The compiler turns the declaration into a function that RETURNS its result and its outs
as a tuple, and each call into the deconstruction of that tuple — because a call here can suspend and resume
later, possibly on the other side of the client/server boundary, and only a return survives that.
class Inventory {
public int Stock = 3;
public bool TryTake(int wanted, out int left) {
if (wanted > Stock) { left = Stock; return false; }
left = Stock - wanted;
return true;
}
}
void MinMax(List<int> xs, out int min, out int max) { min = xs.Min(); max = xs.Max(); }
string Report() {
var inv = new Inventory();
var said = inv.TryTake(2, out var left) ? "took 2, " + left + " left" : "not enough";
MinMax(new List<int> { 4, 1, 9 }, out var lo, out var hi);
return said + "; " + lo + ".." + hi; // "took 2, 1 left; 1..9"
}C#'s rules hold: every out is assigned on every path before the method returns, and is not read before it is
assigned. A call works in any statement, in a while condition (asked again on every pass) and in a lambda (per
element). An async method cannot declare one (C# refuses that too), and ref is not supported.
A class with a Deconstruct(out …) takes apart like a tuple — var (x, y) = point;, foreach (var (x, y) in points),
and the positional patterns below.
The identity is the element TYPES, not their names#
Two tuples with the same element types are the same type, whatever their elements are called — C#'s rule, and what keeps them interchangeable:
(bool ok, decimal value) First() { return (true, 1m); }
(bool success, decimal amount) Second() { return (false, 0m); }
decimal Either(bool pick) {
var a = First();
var b = Second();
a = b; // the same type — names are labels, not identity
return a.ok ? 1m : 0m;
}Names are read at the place they are written: a.ok works because a came from a type spelled with ok.
Reading the elements#
By name, or by position. Both address the same slots, so a tuple never has "two ways to be right":
decimal Both(string s) {
var p = new Parser();
var t = p.Try(s);
return t.ok == t.Item1 ? t.value : t.Item2;
}A tuple nobody declared#
A tuple does not have to be written as a type anywhere. var t = (1, "a") is a (int, string) built from its
elements, exactly as in C# — the compiler gives an inferred tuple its type from its SHAPE, and when the app does write
that shape somewhere (a return type, a typed local) the two are the same type.
string Describe() {
var t = (1, "a"); // (int, string)
var named = (count: 2, label: "b"); // names written on the literal
var label = "c";
var n = 3;
var inferred = (label, n); // names taken from the locals, as C# infers them
return t.Item2 + named.label + inferred.label + t; // "abc(1, a)" — a tuple writes itself "(1, a)"
}Two tuples compare element by element ((1, "a") == (1L, "a") is true, the int widened) and t.Equals(u) agrees, so a
tuple works as a Distinct or GroupBy key. A tuple element may be a value, an entity, or a list or array of one;
null alone has no type (C# refuses var t = (1, null) too), and a tuple held inside an inferred tuple has no slot —
write the type, (int, string) t = …;, for either.
Taking a tuple apart#
Deconstruction reads any tuple, and a KeyValuePair into its key and value; _ takes nothing. Assigning a tuple of
EXISTING variables is the swap:
string Parts() {
var (n, word) = (1, "x");
var (_, only) = (2, "y");
var a = 1;
var b = 2;
(a, b) = (b, a); // the right side is read first, so this swaps
(int count, string label) = (3, "z"); // each name with its own type…
(a, var fresh) = (4, 5); // …or an existing variable beside a new one
return n + word + only + a + b + count + label + fresh; // "1xy413z5"
}Deconstruction evaluates the call ONCE#
var (ok, value) = p.Try(s) reads the call a single time into a hidden local, then takes each element from it. That
matters whenever the call does something: writing p.Try(s).Item1 and p.Try(s).Item2 would run it twice.
A switch that reads a tuple by position#
(x, y) switch { … } and t is (1, var s) test each element — a constant, a relational test (> 0), null / not null, _ for
anything — and var name binds an element for the arm's value. No arm matching throws, as C#'s switch does.
string Quadrant(int x, int y) => (x, y) switch {
(0, 0) => "origin",
(_, 0) => "on the x axis",
(> 0, var up) => "right, " + up + " up",
_ => "elsewhere",
};Each arm reads the elements again, so the switched value is a tuple of locals, a local, or a member — a CALL is
refused, naming the fix (put it in a local first), because C# reads it once. An is pattern's var is read in its
own condition and in the if branch it guards; over a class, a null value does not match.
Pairs and tuples the LINQ verbs answer#
A dictionary whose type the source infers iterates its KeyValuePairs, and the verbs whose selector-less form answers
a pair or a tuple work without a selector:
string Tally() {
var words = new List<string> { "b", "a", "b" };
var counts = words.CountBy(w => w); // KeyValuePair<string, int>s, in first-seen order
var s = "";
foreach (var (word, count) in counts) { s += word + count; }
foreach (var (i, w) in words.Index()) { s += i; } // (int Index, string Item)
foreach (var p in new List<int> { 1, 2 }.Zip(words)) { s += p.First + p.Second; } // (int First, string Second)
return s + counts.First(); // "b2a1" + "012" + "1b2a" + "[b, 2]"
}AggregateBy(key, seed, f) folds each key's elements from ONE seed, as C# does — so the seed is written as a literal or
a local (a new List<int>() there is refused: C# shares that one instance across every key). KeyValuePair.Create(k, v)
makes a pair. A tuple projection over stored rows — Climb.Select(c => (c.Wall, c.Grade)).ToList() — reads the
columns in SQL and builds one tuple per row.
Examples#
A parse that reports whether it succeeded, without a nullable and without a wrapper class:
class Amounts {
public (bool ok, decimal value) Parse(string raw) {
if (raw == "") { return (false, 0m); }
return (true, 42m);
}
}
decimal Total(string raw) {
var a = new Amounts();
var (parsed, amount) = a.Parse(raw);
return parsed ? amount : 0m;
}A tuple as a parameter, when a pair travels together:
decimal Apply((bool ok, decimal value) result) {
return result.ok ? result.value : 0m;
}Errors#
| What you wrote | What you get |
|---|---|
(decimal) Fn(…) | a tuple needs at least TWO elements — (bool ok, decimal value). A one-element tuple has nothing to hold that the type itself does not; write the type on its own. |
var (only) = Fn(); | a deconstruction takes at least TWO names — var (ok, value) = …. For one value write var ok = … without the parentheses. |
var t = (1, null); | this tuple's element 2 is null, which has no type of its own (C# refuses var t = (1, null) too). Write the tuple's type where it is declared — (int, string) t = …; — and the literal will be that type. |
var (a, b) = 5; | a 'int' cannot be taken apart with var (a, b) = … — that reads a tuple or a KeyValuePair. |
(Next(), 2) switch { (1, var s) => … } | this switch's patterns test its value once per arm — C# reads it once, so put it in a local first (var v = …; v switch { … }), and it is read once. |
xs.AggregateBy(k, new List<int>(), f) | AggregateBy's seed is ONE value that every key starts from — C# evaluates it once and shares it. Write it as a literal or put it in a local first … |
t.valu | this tuple has no element 'valu'. Did you mean 'value'? — it holds ok, value |
bool F(out int x) { return true; } | the out parameter 'x' must be assigned before the method returns, on every path that reaches this return (C# requires it too, CS0177) … |
Try(s, out var v) where bool Try(string s) | Try declares no out parameter for this out argument to receive — C# refuses it too (CS1615). Declare it on Try … |
TryFoo(s, out var f) where bool TryFoo(string s, int n) | 'TryFoo' takes 'n' as a value, not an out — C# refuses an out argument for it too (CS1620). … |
void Bump(ref int v) | ref cannot be declared on a parameter — a parameter passes a value IN, and only the RETURN comes back out. … |
See also#
- Functions (the unit of work) — what a function is, and how it is called
- Classes — the alternative when a pair wants a name and behaviour of its own
- Generic classes — the other way one declaration serves many types