Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

C# & .NET (C# 14, .NET 10 LTS)  Module 8 – Structs, records, tuples and value semantics

Tuples, anonymous types and deconstruction

Return several values from a C# method with named value tuples, take values apart with deconstruction, and use anonymous types in LINQ queries.

  • Intermediate
  • 15 minutes
  • Examples run with .NET 10.0.401 and Python 3.14.8
  • By MySmartCoPilot
Free core, Pro depth: 6 of 15 minutes free
Free: A named value tuple returns several values from one method · Deconstruction takes a value apart into variables · Anonymous types name a query's results inside one method. With a pass: 2 in-depth parts and 3 interview answers, about 9 minutes.

What you will learn

  • Write a method that returns several values as a named value tuple, and predict what its names do
  • Use deconstruction to take tuples, records, dictionary entries and types of your own apart
  • Use an anonymous type in a query, and predict its equality and its limits
  • Choose between a tuple, an anonymous type and a record for a group of values
  • Prevent tuple names from being lost in JSON and in positional assignments

Before you start

What you’ll learn · Before you start

What you will learn

  • Write a method that returns several values as a named value tuple, and predict what its names do
  • Use deconstruction to take tuples, records, dictionary entries and types of your own apart
  • Use an anonymous type in a query, and predict its equality and its limits
  • Choose between a tuple, an anonymous type and a record for a group of values
  • Prevent tuple names from being lost in JSON and in positional assignments

Before you start

On this page

MinMax(marks) has two answers for the marks 72, 91, 58, 84 and 66: the lowest, 58, and the highest, 91. A method returns one value, and C# lets that value hold both without a type of your own: (int Min, int Max) is a value tuple: A tuple such as (int Min, int Max): a System.ValueTuple struct whose elements are public fields named Item1, Item2 and so on. The element names you write exist only for the compiler., and var (lowest, highest) = MinMax(marks) takes it apart again into two variables. For the rows of a query, new { City, Total } makes an anonymous type: A class without a name that the compiler makes for an expression such as new { City, Total }. Its properties are read-only, Equals compares their values, and it can be used only inside the method that creates it.: a read-only class that the compiler writes and that stays inside one method.

You will return several values with named tuples, take tuples, records and your own types apart, use anonymous types in queries, and choose which of the three a group of values deserves.

A named value tuple returns several values from one method

minmax.cs returns the lowest and the highest mark as one tuple, then reads it back three ways:

minmax.cs: two results from one method C# · minmax/minmax.cs
// minmax.cs: two results from one method as a named value tuple, and what the names are.
int[] marks = [72, 91, 58, 84, 66];

var range = MinMax(marks);
Console.WriteLine($"by name: {range.Min} to {range.Max}; by position: {range.Item1} to {range.Item2}");

var (lowest, highest) = MinMax(marks);
Console.WriteLine($"deconstructed: lowest {lowest}, highest {highest}");

Console.WriteLine($"the type when the program runs: {range.GetType()}");
Console.WriteLine($"printed: {range}");

// A value tuple is a mutable struct: its elements are public fields.
range.Min = 0;
Console.WriteLine($"after range.Min = 0: {range}");

// == compares element by element, in order. The names take no part.
var report = (Min: 58, Max: 91);
var flipped = (Max: 58, Min: 91);
Console.WriteLine($"(Min: 58, Max: 91) == (Max: 58, Min: 91): {report == flipped}");
Console.WriteLine($"(Min: 58, Max: 91) == (58, 92): {report == (58, 92)}");

static (int Min, int Max) MinMax(int[] values)
{
    int min = values[0], max = values[0];
    foreach (int v in values)
    {
        if (v < min) min = v;
        if (v > max) max = v;
    }
    return (min, max);
}

Output

by name: 58 to 91; by position: 58 to 91
deconstructed: lowest 58, highest 91
the type when the program runs: System.ValueTuple`2[System.Int32,System.Int32]
printed: (58, 91)
after range.Min = 0: (0, 91)
(Min: 58, Max: 91) == (Max: 58, Min: 91): True
(Min: 58, Max: 91) == (58, 92): False

Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run minmax.cs

range.Min and range.Item1 read the same element. The names Min and Max are for the compiler: when the program ran, the type was System.ValueTuple<int, int>, whose fields are Item1 and Item2. A tuple is a mutable struct, so range.Min = 0 changed the element. ==, which C# 7.3 added for tuples, compares element by element, in order, and never reads the names: (Min: 58, Max: 91) == (Max: 58, Min: 91) was True, although the names say the opposite.

You write What runs
(int Min, int Max) ValueTuple<int, int>, a struct
range.Min the field Item1
(Min: 58, Max: 91) == (Max: 58, Min: 91) 58 == 58 && 91 == 91, so True

The names do not survive outside the compiler: System.Text.Json writes {} for a tuple, and an assignment between tuples matches elements by position, not by name.

Checkpoint

Check: Named value tuples and their names

  1. Question 1 of 2 MinMax returns (int Min, int Max). What did range.GetType() print when minmax.cs ran?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: System.ValueTuple`2[System.Int32,System.Int32]

    The element names exist for the compiler, which turns range.Min into range.Item1.

  2. Question 2 of 2 What does (Min: 58, Max: 91) == (Max: 58, Min: 91) give?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: True: 58 equals 58 and 91 equals 91, in order

    Names are labels for readers; positions are what assignment and equality use.

Deconstruction takes a value apart into variables

deconstruction: Taking a value apart into variables in one statement, as var (min, max) = range does. A tuple gives its elements in order; any other type needs a Deconstruct method with one out parameter per variable. works on more than tuples. deconstruct.cs takes apart a Money struct, the entries of a dictionary, a DateOnly, a tuple with a value it does not need, and two variables it swaps:

deconstruct.cs: six deconstructions C# · deconstruct/deconstruct.cs
// deconstruct.cs: taking values apart: a type of your own, a dictionary entry, a DateOnly, a swap and discards.
var price = new Money(1_250_50);
var (rupees, paise) = price;
Console.WriteLine($"Money {price.Paise} paise -> {rupees} rupees and {paise} paise");

var pins = new Dictionary<string, string> { ["Pune"] = "411001", ["Nagpur"] = "440001" };
foreach (var (city, pin) in pins)
{
    Console.WriteLine($"{city}: {pin}");
}

var (year, month, day) = new DateOnly(2027, 1, 26);
Console.WriteLine($"DateOnly -> year {year}, month {month}, day {day}");

var (_, highest) = MinMax([72, 91, 58]);
Console.WriteLine($"only the highest: {highest}");

int first = 1, second = 2;
(first, second) = (second, first);
Console.WriteLine($"swapped: first {first}, second {second}");

// C# 10: one deconstruction may assign an existing variable and declare a new one.
string station;
(station, var platform) = ("Pune Junction", 3);
Console.WriteLine($"{station}, platform {platform}");

static (int Min, int Max) MinMax(int[] values) => (values.Min(), values.Max());

readonly struct Money(long paise)
{
    public long Paise { get; } = paise;

    // Called by var (rupees, paise) = price: one out parameter for each variable.
    public void Deconstruct(out long rupees, out int paise)
    {
        rupees = Paise / 100;
        paise = (int)(Paise % 100);
    }
}

Output

Money 125050 paise -> 1250 rupees and 50 paise
Pune: 411001
Nagpur: 440001
DateOnly -> year 2027, month 1, day 26
only the highest: 91
swapped: first 2, second 1
Pune Junction, platform 3

Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run deconstruct.cs

var (rupees, paise) = price gave 1250 rupees and 50 paise, because Money declares a Deconstruct method with two out parameters. Follow how the compiler decides what a deconstruction calls:

Interactive · stage by stage What var (rupees, paise) = price compiles to Step through three deconstructions from deconstruct.cs. Each takes the checks from the top; the arrow aside is the way a tuple takes.
What var (rupees, paise) = price compiles to, step 6 of 6. Showing var (a, b) = value (var (city, pin) in pins), Is the value a tuple?, Find Deconstruct, 2 outs, Call it (entry.Deconstruct(…)), The variables (Pune, 411001). In focus: Find Deconstruct, 2 outs, Call it, The variables.yes: in ordervar (a, b) = valuevar (city, pin) in pinsIs the value a tuple?elements, by positionFind Deconstruct, 2 outsone out per variableCall itentry.Deconstruct(…)The variablesPune, 411001

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. Money has two parts. var (rupees, paise) = price asks for two variables. price is a Money struct holding 125050 paise.
  2. Not a tuple. Money is a struct of its own, not a tuple, so the compiler looks for a method named Deconstruct with two out parameters.
  3. Deconstruct is found. Money declares Deconstruct(out long rupees, out int paise). The compiler calls it, and the method splits 125050 into 1250 and 50.
  4. Two variables. rupees is 1250 and paise is 50, as the first line of deconstruct.cs printed. Their types come from the out parameters: long and int.
  5. A tuple goes straight. var (_, highest) = MinMax(...) deconstructs a tuple: its elements are assigned in order, and the discard _ drops the first. No method is looked for.
  6. A dictionary entry. foreach (var (city, pin) in pins) deconstructs each KeyValuePair<string, string>. KeyValuePair declares a Deconstruct with two out parameters, so each pass gets a city and its PIN.

DateOnly has had a Deconstruct of its own since .NET 8, and KeyValuePair has one too, which is what foreach (var (city, pin) in pins) calls. A discard, _, fills a position you do not need. (first, second) = (second, first) swapped two variables through a tuple, and since C# 10 one deconstruction may assign an existing variable and declare a new one. A type you do not own can get a Deconstruct as an extension method, and any Deconstruct also lets a switch match a value by its parts.

Checkpoint

Check: Deconstruction into variables

  1. Question 1 of 2 Money is a struct with Deconstruct(out long rupees, out int paise). What does var (r, p) = new Money(1_250_50) give?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: r is 1250 and p is 50

    A deconstruction of a type other than a tuple is a call to its Deconstruct method, one out parameter per variable.

  2. Question 2 of 2 Which statement keeps only the highest mark from MinMax, which returns (int Min, int Max)?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: var (_, highest) = MinMax(marks);

    Each position needs a variable or a discard, and the positions are what count.

Anonymous types name a query’s results inside one method

An anonymous type is a class the compiler writes for an expression such as new { City = g.Key, Orders = g.Count(), Total = … }. anonymous.cs groups three orders by city:

anonymous.cs: orders grouped by city C# · anonymous/anonymous.cs
// anonymous.cs: anonymous types from a query: read-only, equal by Equals, but == compares references.
Order[] orders = [new("Asha", "Pune", 1200m), new("Ravi", "Nagpur", 450m), new("Meena", "Pune", 300m)];

var byCity = orders
    .GroupBy(o => o.City)
    .Select(g => new { City = g.Key, Orders = g.Count(), Total = g.Sum(o => o.Amount) });
foreach (var row in byCity)
{
    Console.WriteLine(row);
}

var a = new { City = "Pune", Orders = 2 };
var b = new { City = "Pune", Orders = 2 };
Console.WriteLine($"a.Equals(b) {a.Equals(b)}, a == b {a == b}, same type {a.GetType() == b.GetType()}");
Console.WriteLine($"a class? {!a.GetType().IsValueType}; its name: {a.GetType().Name}");

var c = a with { Orders = 3 };
Console.WriteLine($"with {{ Orders = 3 }}: {c}, a is still {a}");

record Order(string Customer, string City, decimal Amount);

Output

{ City = Pune, Orders = 2, Total = 1500 }
{ City = Nagpur, Orders = 1, Total = 450 }
a.Equals(b) True, a == b False, same type True
a class? True; its name: <>f__AnonymousType1`2
with { Orders = 3 }: { City = Pune, Orders = 3 }, a is still { City = Pune, Orders = 2 }

Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run anonymous.cs

Each row printed its property names and values. Two anonymous objects with the same properties were of one type and equal by Equals, yet a == b was False: an anonymous type writes Equals and GetHashCode but no ==, so == compares references. Its properties are read-only, with copies it since C# 10, and because the type has no name, no parameter, field or return value can have it: it stays inside the method that made it.

Feature Value tuple Anonymous type Record
Kind struct class class or struct
Names when the program runs no: Item1, Item2 yes, real properties yes
== compares elements compares references compares values
Can change yes, public fields no init only, or settable in a record struct
Use it outside its method yes no yes

For two or three values from a private method, a tuple is enough; for a query’s rows inside one method, an anonymous type or a tuple; for anything that crosses an API or is stored, a record. When the rows are built inside an Code kept as data: a tree of objects that describes an expression, such as a lambda, so that a library can read it and translate it, for example into SQL, instead of running it., as in a database library’s IQueryable queries, take the anonymous type: Microsoft’s comparison of the two notes that tuples cannot be used in expression trees.

Checkpoint

Check: Anonymous types, and choosing among the three

  1. Question 1 of 2 a and b are new { City = "Pune", Orders = 2 }, created separately. What do a.Equals(b) and a == b give?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Equals True, == False

    Use Equals, or a record, when you compare anonymous values; == on them asks whether they are one object.

  2. Question 2 of 2 A public API method returns the start and end of a booking window as two DateOnly values. Which type should it return?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: A record, such as record BookingWindow(DateOnly Start, DateOnly End)

    Tuples for private multi-value returns, anonymous types inside one method, records for anything public or stored.

Tuple names at run time: JSON, reflection, swapped counts and System.Tuple, measured Pro: opens with a Pro, Premium or Ultimate pass

A tuple’s names live in the compiler, so every place where the program meets the outside, JSON, reflection, another method’s signature, loses or ignores them. The runs below show each case and the type that keeps the names.

In this part:

  • JSON writes a tuple’s fields, not its names
  • Where the names really live: an attribute on the method
  • Names do not travel with a value: two counts swapped
  • A tuple as a dictionary key, and the comparer each element brings
  • System.Tuple, the class from before C# 7

6 runnable examples (C#, Python) · about 5 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Deconstruction in depth: types you do not own, overloads and positional patterns Pro: opens with a Pro, Premium or Ultimate pass

Deconstruct is an ordinary method that the compiler calls by its name and its number of out parameters. That one rule lets you add it to types you do not own, offer several shapes of one type, and match values by their parts.

In this part:

  • A Deconstruct for a type you do not own
  • Overloads by count: two shapes of one passenger
  • Positional patterns: a switch that reads Deconstruct
  • Deconstruction in loops: Zip, Index and CountBy
  • When a tuple should become a record

2 runnable examples (C#) · about 5 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Practise: statistics as one named tuple

stats.cs checks Stats, which returns the count, the sum, the mean and the median of some fares as one named tuple, and Summary, which deconstructs it into a line of text. Both start out throwing, so all 7 checks fail:

stats.cs: seven checks of Stats and Summary C# · practice/stats.cs
// stats.cs: seven checks of Stats, which returns four results as one named tuple, and of Summary, which uses them.
using System.Globalization;

decimal[] odd = [400m, 250m, 600m];
decimal[] even = [400m, 250m, 600m, 100m];
var passed = 0;

Check("three fares: count 3, sum 1250, median 400", () => Stats(odd) is (3, 1250m, _, 400m));
Check("three fares: mean 416.67 when rounded", () => Math.Round(Stats(odd).Mean, 2) == 416.67m);
Check("four fares: median 325, the mean of 250 and 400", () => Stats(even).Median == 325m && Stats(even).Mean == 337.5m);
Check("one fare is its own median", () => Stats([99m]).Median == 99m);
Check("leaves the caller's array in its order", () => { Stats(even); return even.SequenceEqual([400m, 250m, 600m, 100m]); });
Check("refuses no fares with ArgumentException", () => { try { Stats([]); return false; } catch (ArgumentException) { return true; } });
Check("Summary of four fares", () => Summary(even) == "4 fares, total 1350.00, mean 337.50, median 325.00");

Console.WriteLine($"{passed} of 7 checks pass");
return passed == 7 ? 0 : 1;

void Check(string name, Func<bool> test)
{
    bool ok;
    try { ok = test(); }
    catch (Exception e) { Console.WriteLine($"FAIL  {name}: {e.GetType().Name}"); return; }
    Console.WriteLine($"{(ok ? "pass" : "FAIL")}  {name}");
    if (ok) passed++;
}

// ---- your code: complete Stats and Summary --------------------------------------------------------------------
static (int Count, decimal Sum, decimal Mean, decimal Median) Stats(IEnumerable<decimal> values) => throw new NotImplementedException();

static string Summary(IEnumerable<decimal> values) => throw new NotImplementedException();

Output (exit status 1)

FAIL  three fares: count 3, sum 1250, median 400: NotImplementedException
FAIL  three fares: mean 416.67 when rounded: NotImplementedException
FAIL  four fares: median 325, the mean of 250 and 400: NotImplementedException
FAIL  one fare is its own median: NotImplementedException
FAIL  leaves the caller's array in its order: NotImplementedException
FAIL  refuses no fares with ArgumentException: NotImplementedException
FAIL  Summary of four fares: NotImplementedException
0 of 7 checks pass

Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run stats.cs

Write Stats and Summary

Copy stats.cs. In Stats, sort a copy of the values with values.Order(), throw ArgumentException when it is empty, and take the middle value, or the mean of the two middle values for an even count. In Summary, write var (count, sum, mean, median) = Stats(values); and format with CultureInfo.InvariantCulture. Run dotnet run stats.cs until it prints 7 of 7 checks pass.

Then check yourself on tuples, deconstruction and anonymous types:

Practice that adapts to you

Practise this lesson

9 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 9 Which two expressions read the same element of range, a (int Min, int Max)?

    Choose one answer.

    Show the answer: Question 1 of 9

    Answer: range.Min and range.Item1

    The default names Item1, Item2 always work, even when the elements have names.

  2. Question 2 of 9 After var range = MinMax(marks); range.Min = 0; what does range print?

    Choose one answer.

    Show the answer: Question 2 of 9

    Answer: (0, 91): a value tuple's elements are public fields that can change

    Value tuples are mutable structs: copied on assignment, and changeable through a variable.

  3. Question 3 of 9 Why does Microsoft's tuple reference suggest a class or a struct, not a tuple, in public APIs?

    Choose one answer.

    Show the answer: Question 3 of 9

    Answer: A tuple's names exist only for the compiler, so reflection, serializers and other tools see Item1 and Item2

    A type with real member names carries its meaning to JSON, logs and other languages; a tuple does not.

  4. Question 4 of 9 What must a type declare so that var (a, b) = value compiles for it?

    Choose one answer.

    Show the answer: Question 4 of 9

    Answer: A void Deconstruct method with two out parameters

    Deconstruct(out T1 a, out T2 b) is the whole contract.

  5. Question 5 of 9 Why does foreach (var (city, pin) in pins) compile for a Dictionary<string, string>?

    Choose one answer.

    Show the answer: Question 5 of 9

    Answer: Each item is a KeyValuePair<string, string>, which declares a Deconstruct with two out parameters

    Types such as KeyValuePair and DateOnly ship their own Deconstruct, so they work in deconstructions.

  6. Question 6 of 9 first is 1 and second is 2. What do they hold after (first, second) = (second, first)?

    Choose one answer.

    Show the answer: Question 6 of 9

    Answer: first 2, second 1

    A tuple on the right is evaluated completely before the assignment, so a swap needs no temporary variable.

  7. Question 7 of 9 Where can an anonymous type such as new { City, Total } be used?

    Choose one answer.

    Show the answer: Question 7 of 9

    Answer: Only inside the method that creates it

    Anonymous types are for shaping data within one method, typically a LINQ query.

  8. Question 8 of 9 row is new { City = "Pune", Orders = 2 }. How do you get the same row with 3 orders?

    Choose one answer.

    Show the answer: Question 8 of 9

    Answer: var updated = row with { Orders = 3 };

    Read-only properties and with: the same rules as an immutable record.

  9. Question 9 of 9 A LINQ query inside one method groups orders and the rows are printed with ToString for a log. Which shape suits the rows best?

    Choose one answer.

    Show the answer: Question 9 of 9

    Answer: An anonymous type: its real property names appear in ToString, and the rows never leave the method

    Inside one method both work; the anonymous type keeps names in its output, the tuple allocates nothing.

Interview questions

Fresher · What is the difference between a value tuple and an anonymous type?

A value tuple such as (int Min, int Max) is a struct of public fields, so it is copied on assignment and can change, and its names exist only for the compiler: when the program runs, the fields are Item1 and Item2. A method can return it. An anonymous type such as new { City, Total } is a class with read-only properties whose names are real. It is equal to another by Equals but not by ==, and having no name, it cannot leave the method that made it.

  1. Fresher to mid

    A refactor changed a minimal API endpoint to return (string Status, int Count) instead of a small class. Clients now receive {} and their dashboards show nothing. Explain what happened, and how you would fix the endpoint without breaking the clients again.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Fresher to mid

    A method returns (int Late, int OnTime), and a caller writes var (onTime, late) = GetCounts(). The daily report shows 40 late trains and 3 on time; the truth is the reverse. Why did nothing catch it, and how would you make the mistake impossible?

    Model answer, Pro. First step and follow-ups

  3. Mid to senior

    A LINQ query builds new { City, Orders, Total } rows, and a teammate wants another class to use them, by returning object or dynamic from the method. What problems does each choice bring, and what would you do instead?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • A named value tuple returns several values as one struct; its names exist only for the compiler, and == compares its elements in order without reading them.
  • Deconstruction assigns a tuple’s elements by position, or calls a Deconstruct method with one out parameter per variable, as Money, KeyValuePair and DateOnly declare.
  • An anonymous type is a read-only class with real property names, equal by Equals but not by ==, that cannot leave its method; a record is the type to use once values cross an API.

Terms in this lesson

anonymous type
A class without a name that the compiler makes for an expression such as new { City, Total }. Its properties are read-only, Equals compares their values, and it can be used only inside the method that creates it.
deconstruction
Taking a value apart into variables in one statement, as var (min, max) = range does. A tuple gives its elements in order; any other type needs a Deconstruct method with one out parameter per variable.
value tuple
A tuple such as (int Min, int Max): a System.ValueTuple struct whose elements are public fields named Item1, Item2 and so on. The element names you write exist only for the compiler.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.