Chuyển tới nội dung chính

Tuples

Definition

A tuple is a data structure that groups multiple values into a single unit without defining a dedicated type. C# supports two tuple systems: the older System.Tuple (reference type, C# 4) and the modern System.ValueTuple (value type, C# 7+). ValueTuple is the recommended approach with first-class language support for named elements, deconstruction, and pattern matching.

// Lightweight grouping without a custom type
(int Id, string Name, double Score) result = (1, "Alice", 95.5);
Console.WriteLine($"{result.Name}: {result.Score}"); // Alice: 95.5

Core Concepts

System.Tuple (Reference Tuple)

Introduced in .NET Framework 4.0. Instances are reference types allocated on the heap. Elements are accessed as Item1, Item2, ..., Item8 — there is no language-level support for named elements.

// Creating a Tuple
Tuple<int, string, bool> person = Tuple.Create(1, "Alice", true);

// Accessing elements — no named members
int id = person.Item1;
string name = person.Item2;
bool active = person.Item3;

// Maximum 8 elements; 8th position uses TRest for nesting
var big = Tuple.Create(1, 2, 3, 4, 5, 6, 7, Tuple.Create(8, 9));
System.Tuple is legacy

System.Tuple is verbose, allocates on the heap, and has no language-level named elements. Prefer System.ValueTuple for all new code.

System.ValueTuple (Value Tuple)

Introduced in C# 7 (.NET Framework 4.7+ / .NET Core). Value type allocated on the stack with first-class language support for named elements, deconstruction, and equality comparison.

// Unnamed elements
(int, string) person1 = (1, "Alice");
int id = person1.Item1;

// Named elements (recommended)
(int Id, string Name) person2 = (2, "Bob");
Console.WriteLine(person2.Name); // Bob

// Target-typed with var
var point = (X: 3.0, Y: 4.0);
Console.WriteLine($"({point.X}, {point.Y})"); // (3, 4)

// No practical element limit
var big = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
Named elements are compile-time only

Names like (int Id, string Name) exist only at compile time. At runtime, they become Item1 and Item2. Reflection sees only the numbered names.

Tuple Deconstruction

Extract tuple elements into individual variables in a single statement. Supports var for type inference and _ for discarding elements.

// Basic deconstruction
var (id, name, active) = (1, "Alice", true);

// Discard elements with _
var (x, y, _) = (3.0, 4.0, "unused");

// Deconstruct method return
var (count, average) = GetStats(new[] { 80, 90, 100 });
Console.WriteLine($"Count: {count}, Avg: {average}");

// Deconstruct into existing variables
int a, b;
(a, b) = (10, 20);

Custom Deconstruct — any type can support deconstruction by defining a Deconstruct method:

public class Point
{
public double X { get; init; }
public double Y { get; init; }

public void Deconstruct(out double x, out double y)
{
x = X;
y = Y;
}
}

var p = new Point { X = 3, Y = 4 };
var (x, y) = p; // Calls Deconstruct

Tuples as Method Return Types

Tuples replace out parameters for returning multiple values from a method:

// Method returning a named tuple
public (int Count, double Average) GetStats(IEnumerable<int> numbers)
{
var list = numbers.ToList();
return (list.Count, list.Average());
}

// Caller usage
var stats = GetStats(new[] { 80, 90, 100 });
Console.WriteLine($"{stats.Count} items, avg {stats.Average}");

// Or deconstruct directly
var (count, avg) = GetStats(new[] { 80, 90, 100 });
Tuples vs public APIs

Tuples as return types are ideal for private/internal methods. For public APIs, prefer well-named classes or records for clarity, discoverability, and versioning.

Tuple Patterns in Switch Expressions

C# 8+ supports pattern matching on tuples in switch expressions — a powerful combination:

string Classify(int x, int y) => (x, y) switch
{
(0, 0) => "Origin",
(> 0, > 0) => "First quadrant",
(< 0, > 0) => "Second quadrant",
(< 0, < 0) => "Third quadrant",
(> 0, < 0) => "Fourth quadrant",
(0, _) or (_, 0) => "On axis"
};

// Rock-paper-scissors example
string Rps(string p1, string p2) => (p1, p2) switch
{
("rock", "scissors") or ("scissors", "paper") or ("paper", "rock")
=> "Player 1 wins",
("rock", "rock") or ("scissors", "scissors") or ("paper", "paper")
=> "Draw",
_ => "Player 2 wins"
};

See Pattern Matching for the full pattern matching guide.

Tuples with LINQ

Tuples provide a lightweight alternative to anonymous types in LINQ projections:

// Tuple projection instead of anonymous type
var topStudents = students
.Select(s => (s.Name, s.Grade))
.Where(t => t.Grade >= 90)
.ToList();

// GroupBy with tuple result
var grouped = orders
.GroupBy(o => (o.Category, o.Year))
.Select(g => (g.Key, Total: g.Sum(o => o.Amount)));

// Zip two sequences into tuples
var pairs = names.Zip(scores)
.Select(z => (z.First, z.Second));

// Aggregate with tuple accumulator
var (min, max) = numbers.Aggregate(
(Min: int.MaxValue, Max: int.MinValue),
(acc, n) => (Math.Min(acc.Min, n), Math.Max(acc.Max, n)));

Tuple Equality

C# 7.3+ supports == and != on tuples. Comparison is element-by-element using the default equality for each element's type:

(1, "a") == (1, "a")       // true
(1, "a") != (1, "b") // true
(1, 2, 3) == (1, 2, 3) // true

// Works with named elements too — names don't affect equality
(int X, int Y) p1 = (1, 2);
(int A, int B) p2 = (1, 2);
p1 == p2 // true — structural equality

Tuple vs ValueTuple Comparison

FeatureSystem.TupleSystem.ValueTuple
TypeReference type (class)Value type (struct)
AllocationHeapStack
Named elementsNo (only Item1, Item2, ...)Yes, at language level
Max elements8 (nesting required for more)No practical limit
Equality (==)Reference equalityValue equality (C# 7.3+)
DeconstructionNot supportedSupported
MutableNo (read-only)Yes (fields are mutable)
C# versionC# 4C# 7+
RecommendedNo (legacy)Yes (default choice)

When to Use

ScenarioRecommendation
Multiple return values from private/internal methodsUse ValueTuple
Temporary data grouping in LINQ queriesUse ValueTuple
Switch expression with multiple inputsUse ValueTuple
Dictionary composite keyValueTuple works well
Public API return typesUse records or named classes
More than 4-5 elementsConsider a record or class
Data that needs serializationAvoid tuples; use records/classes

Common Pitfalls

  1. Element names are erased at runtime — Named elements like (int Id, string Name) are compile-time only. Reflection sees Item1, Item2. They don't survive through object casts in all scenarios.

  2. ValueTuple is mutable — Unlike Tuple, ValueTuple fields are mutable. Using a ValueTuple as a dictionary key and then mutating it corrupts the dictionary because the hash code changes.

  3. Confusing System.Tuple with System.ValueTuple — Writing new Tuple<int, string>(1, "a") when you meant (1, "a"). The former allocates on the heap and lacks language features.

  4. Overusing tuples for complex data — When a tuple grows beyond 3-4 elements, a record provides better documentation, serialization support, and pattern matching. Tuples are for lightweight, temporary groupings.

Key Takeaways

  1. Always use System.ValueTuple ((T1, T2)) over System.Tuple for new code.
  2. Named elements improve readability: (int Id, string Name) over (int, string).
  3. Tuple deconstruction and switch expressions are a powerful combination.
  4. Tuples are best for internal code; prefer records for public APIs.
  5. ValueTuple is a mutable value type — do not use it as a dictionary key if you intend to mutate.

Interview Questions

Q: What is the difference between Tuple and ValueTuple?

Tuple (C# 4) is a reference type on the heap with elements accessed as Item1, Item2, etc. ValueTuple (C# 7) is a value type on the stack with language-level named elements, deconstruction, value equality, and no practical element limit. Always prefer ValueTuple for new code.

Q: Can you use tuples as dictionary keys?

Yes, ValueTuple implements GetHashCode and Equals based on its elements, so (int, string) works as a dictionary key. However, since ValueTuple is mutable, you must not change the tuple after using it as a key.

Q: What is tuple deconstruction?

Deconstruction extracts tuple elements into separate variables in a single statement: var (name, age) = GetPerson();. Any type with a Deconstruct method (or extension method) supports deconstruction, not just tuples.

Q: How are tuple element names handled at runtime?

Tuple element names like (int Id, string Name) are compile-time only. At runtime, the names become Item1 and Item2. They are preserved in attributes for reflection in some scenarios but are not part of the type at execution time.

Q: When should you use a tuple vs a record?

Use tuples for lightweight, temporary groupings of values in internal code (method returns, LINQ projections). Use records for public APIs, serialized data, or when you need structural equality, inheritance, or more than 3-4 named fields.

References