Skip to content

Tuples

A tuple is the lightweight way to group or return a few values without declaring a class. You make one with parentheses, read its elements by position (.Item1, .Item2, …) or by name, deconstruct it into separate variables, match it in a switch, compare it, and store it in collections.

var pair = (3, 4);                  // a 2-tuple (2+ elements: (a, b, c), …)
int sum = pair.Item1 + pair.Item2;  // 7

Making and reading tuples

A tuple literal is just comma‑separated values in parentheses. Read elements positionally with .Item1, .Item2, … (1‑based).

var p = (10, 20, 30);     // 3-tuple
int a = p.Item1;          // 10
int b = p.Item2;          // 20
int c = p.Item3;          // 30

Tuples can hold any types, and can nest:

var mixed = ("Ada", 42, true);          // (string, int, bool)
var nested = ((1, 2), 3);               // a tuple inside a tuple
int inner = nested.Item1.Item2;         // 2

Returning multiple values

The headline use: return several values from a method with a tuple return type — no out parameter, no helper class.

static (int, int) MinMax(int[] xs)
{
    int lo = xs[0], hi = xs[0];
    foreach (int v in xs) { if (v < lo) lo = v; if (v > hi) hi = v; }
    return (lo, hi);
}

var (low, high) = MinMax(scores);            // deconstruct the result (below)
var r = MinMax(scores);                       // or keep the tuple
int spread = r.Item2 - r.Item1;

Tuples also work as parameters and fields:

static int Add((int, int) p) { return p.Item1 + p.Item2; }   // tuple parameter

class Score : NexusBehaviour
{
    (int, int) lastResult;                                    // tuple field
}

Named elements

Give the elements names for readability — then read them by name or by position:

static (int count, string label) Summarize() { return (3, "items"); }

var s = Summarize();
Debug.Log(s.count + " " + s.label);   // named access
int n = s.Item1;                       // positional still works

Names are per shape

Element names are a per‑shape convenience — two tuples with the same element types share the same names. That's fine for normal use; just don't expect two different name sets on the same (int, int) shape.

Deconstruction

Split a tuple into separate variables. Three forms:

var (x, y) = pair;            // 1. infer the element types
(int x2, string y2) = pair;   // 2. declare with explicit types
(x, y) = MinMax(scores);      // 3. assign into existing variables

Use _ to discard an element you don't need, and deconstruct a method result directly:

var (a, _, c) = (1, 99, 4);          // skip the middle element → a=1, c=4
var (low, high) = MinMax(scores);    // deconstruct the returned tuple

The number of targets must match the tuple's size (a 2‑tuple needs two targets).

switch on a tuple

Tuples shine in pattern matching. Both the expression and statement forms support tuple patterns with _ wildcards.

// Expression form
string Quadrant(int x, int y) => (x, y) switch
{
    (0, 0) => "origin",
    (_, 0) => "on the x-axis",
    (0, _) => "on the y-axis",
    _      => "elsewhere"
};

// Statement form
switch ((x, y))
{
    case (0, 0): Reset();      break;
    case (_, 0): SlideAlongX(); break;
    default:     FreeMove();   break;
}

Patterns are matched top to bottom; the first match wins. A _ element matches anything; a bare _ arm/default is the catch‑all.

Relational element patterns — an element can be a comparison (> < >= <= == !=) instead of an exact value:

string Sign(int x, int y) => (x, y) switch
{
    (> 0, > 0) => "both positive",
    (< 0, _)   => "x negative",
    (0, _)     => "x is zero",
    _          => "other"
};

when guards — add a boolean condition that must also hold for the arm to match (it can reference any variables in scope). Works on any pattern, including _:

string Compare(int x, int y) => (x, y) switch
{
    (0, 0)            => "both zero",
    (> 0, _) when x > y => "x positive and bigger",   // pattern AND guard
    _ when x > y      => "x bigger",
    _ when x < y      => "y bigger",
    _                 => "equal"
};

// also in statement form:
switch ((x, y))
{
    case (_, _) when x > y: TakeLead(); break;
    default:                Hold();     break;
}

Equality

Tuples compare structurally with == and != — element by element, not by reference. Two tuples are equal when every element is equal.

if ((x, y) == (0, 0)) { /* exactly at the origin */ }

var a = (1, 2);
var b = (1, 2);
bool same = a == b;     // true (same values), even though they're different objects

Tuples in collections

Tuples work as collection elements — a list of pairs, a dictionary keyed to points, etc. Named elements work through the collection too.

var points = new List<(int, int)>();
points.Add((3, 4));
points.Add((10, 20));
int firstX = points[0].Item1;            // 3
for (int i = 0; i < points.Count; i++)
{
    var pt = points[i];
    Debug.Log(pt.Item1 + "," + pt.Item2);
}

var spawns = new Dictionary<string, (int count, int total)>();
spawns["wave1"] = (5, 100);
int n = spawns["wave1"].count;           // 5 — named access through the dictionary

A worked example

A scoreboard helper that returns the leader and reacts to outcomes:

using UnityEngine;
using NexusVM.Unity;

class Scoreboard : NexusBehaviour
{
    int red;
    int blue;

    // Return two related values at once.
    (int redScore, int blueScore) Scores() => (red, blue);

    string Status()
    {
        var (r, b) = Scores();
        return (r, b) switch
        {
            (0, 0)        => "Kickoff!",
            _ when r > b  => "Red leads",     // when guard for comparisons
            _ when b > r  => "Blue leads",
            _             => "Tied"
        };
    }

    // Tuple patterns are perfect for exact, enumerable states:
    string Corner(int x, int y) => (x, y) switch
    {
        (0, 0) => "bottom-left",
        (1, 0) => "bottom-right",
        (0, 1) => "top-left",
        (1, 1) => "top-right",
        _      => "not a corner"
    };
}

Gotchas & limits

Good to know

  • A tuple is a reference object (everything in NexusScript is), so passing one around shares it — reassign with a fresh (…) for an independent copy.
  • Tuples need 2+ elements; (x) is just a parenthesized value, not a tuple.
  • The lookalike (a, b) => … is a lambda parameter list, not a tuple — they're unrelated.

Not supported yet

  • A tuple stored in a user‑defined generic class's T field (Box<(int,int)>.value) reads elements incorrectly — this is a general generic‑field limitation, not specific to tuples. Collections (List, Dictionary) are fine.

Quick reference

Feature Syntax
Literal (a, b) · (a, b, c) (2+)
Element access t.Item1 · t.Item2 · named t.count
Return type (int, int) F() { return (a, b); }
Parameter / field void M((int,int) p) · (int,int) f;
Deconstruct var (x,y)=t · (int x,…)=t · (x,y)=t · _ discards
switch t switch { (0,0)=>… , _=>… } · case (0,_): · relational (>0, _) · when guard
Equality a == b · a != b (structural)
Collections List<(int,int)> · Dictionary<string,(int,int)>

→ Related: Collections · Basics & lifecycle.