C# Deconstruction
Deconstruction in C# means taking one value that contains several parts and assigning those parts to separate variables in one statement. It is most often used with tuples, records, and types that define a Deconstruct method. Deconstruction matters because it keeps small data transformations readable: you can return several values, ignore the parts you do not need, and make local code say exactly which pieces it uses.
Overview: How Deconstruction Works
Deconstruction is a compile-time feature. When the compiler sees a statement such as (string name, int age) = person;, it does not create a special runtime deconstruction object. Instead, it rewrites the operation into assignments from tuple fields or calls to a suitable Deconstruct method with out parameters.
For tuples, deconstruction is positional. A tuple such as (Name: "Ada", Age: 36) has friendly element names, but (string name, int age) = tuple; assigns the first element to name and the second element to age. The local variable names on the left do not have to match the tuple element names. That flexibility is useful, but it also means order matters more than names.
For records and your own classes or structs, deconstruction works through a method named Deconstruct. A positional record automatically gets one. For a normal class, you can write your own method such as public void Deconstruct(out string name, out int age). The method returns void, and each extracted value is assigned through an out parameter. The compiler chooses a Deconstruct overload whose number of out values matches the variables on the left.
Deconstruction can declare new variables, assign into existing variables, or do both in some cases. You can also use the discard symbol _ when you intentionally do not need a value. A discard tells the compiler, and the next person reading the code, that the value was ignored on purpose. It is not a normal variable you should read later.
Internally, deconstruction still follows normal C# typing rules. Each target variable must be compatible with the corresponding source value. Assignment deconstruction evaluates the right side first, then assigns the extracted values to the left side. This is why swapping two variables with (a, b) = (b, a); works without needing a temporary variable in your code.
Syntax
var person = (FirstName: "Ada", LastName: "Lovelace");
(string firstName, string lastName) = person;
var (_, familyName) = person;
Console.WriteLine(firstName);
Console.WriteLine(familyName);
| Syntax | Meaning |
|---|---|
(string firstName, string lastName) = person |
Declares two local variables and assigns values from person by position. |
var (name, age) = value |
Lets the compiler infer the variable types from the deconstructed values. |
(existingName, existingAge) = value |
Assigns into variables that were already declared. |
_ |
Discards a value that is intentionally ignored. |
Deconstruct(out T first, out U second) |
A method pattern that lets your own type support deconstruction. |
foreach (var (key, value) in items) |
Deconstructs each item during iteration when the item type supports it. |
Examples
Example 1: Deconstructing a Tuple
using System;
class Program
{
static void Main()
{
var score = (Player: "Mina", Points: 42, Level: 7);
(string player, int points, _) = score;
Console.WriteLine(player);
Console.WriteLine(points);
}
}
Output:
Mina
42
The tuple has three elements, but the program only needs the player and points. The discard ignores the level. Notice that player and points are new local variables; their names do not have to match Player and Points, but matching the meaning is usually clearer.
Example 2: Returning Several Values from a Method
using System;
using System.Globalization;
class Program
{
static (int Count, int Min, int Max, double Average) Analyze(int[] values)
{
int min = values[0];
int max = values[0];
int total = 0;
foreach (int value in values)
{
if (value < min)
{
min = value;
}
if (value > max)
{
max = value;
}
total += value;
}
return (values.Length, min, max, (double)total / values.Length);
}
static void Main()
{
int[] temperatures = { 68, 72, 75, 70 };
var (count, min, max, average) = Analyze(temperatures);
Console.WriteLine($"Readings: {count}");
Console.WriteLine($"Range: {min}-{max}");
Console.WriteLine($"Average: {average.ToString("0.0", CultureInfo.InvariantCulture)}");
}
}
Output:
Readings: 4
Range: 68-75
Average: 71.3
The method returns a named tuple, and the caller immediately deconstructs it into four local variables. This is clearer than writing result.Item1, result.Item2, and so on. It also keeps the tuple short-lived, which is where tuples and deconstruction are strongest.
Example 3: Deconstructing Your Own Class
using System;
public class Product
{
public Product(string sku, string name, decimal price)
{
Sku = sku;
Name = name;
Price = price;
}
public string Sku { get; }
public string Name { get; }
public decimal Price { get; }
public void Deconstruct(out string sku, out string name, out decimal price)
{
sku = Sku;
name = Name;
price = Price;
}
}
class Program
{
static void Main()
{
Product product = new Product("BK-101", "C# Guide", 29.95m);
var (sku, name, price) = product;
Console.WriteLine(sku);
Console.WriteLine(name);
Console.WriteLine(price);
}
}
Output:
BK-101
C# Guide
29.95
The Product class supports deconstruction because it defines a matching Deconstruct method. The method does not return a tuple. It fills the out parameters, and the compiler assigns those values to sku, name, and price.
Example 4: Deconstruction in a Foreach Loop
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
List<(string Course, int Completed)> progress = new List<(string Course, int Completed)>
{
("C#", 12),
("SQL", 8),
("JavaScript", 15)
};
foreach (var (course, completed) in progress)
{
Console.WriteLine($"{course}: {completed} lessons");
}
}
}
Output:
C#: 12 lessons
SQL: 8 lessons
JavaScript: 15 lessons
Each list item is a tuple. The foreach statement deconstructs the current tuple before running the loop body, so the body can use course and completed directly. This is useful when the loop works with pairs or small projections.
How Deconstruction Works Step by Step
- The compiler checks the right side. If it is a tuple, the compiler reads tuple elements by position. If it is another type, the compiler looks for an accessible
Deconstructmethod. - The compiler counts the variables, discards, or assignment targets on the left side and requires the same number of extracted values.
- For new variables, their types are taken from explicit declarations or inferred when you use
var. - For existing variables, normal assignment conversions must be valid. A string cannot be assigned to an
intjust because it appears in a deconstruction statement. - The right side is evaluated before left-side assignments are completed. This makes swaps and multiple assignment predictable.
- Discards receive values but do not create a readable local variable. They are a signal that the value is intentionally unused.
- At runtime, the generated code is ordinary field access, property access through a generated record method, or a method call with
outarguments. There is no special CLR instruction for deconstruction.
Common Mistakes
Forgetting That Order Matters
using System;
class Program
{
static void Main()
{
var point = (X: 10, Y: 20);
(int y, int x) = point;
Console.WriteLine($"x={x}, y={y}");
}
}
Output:
x=20, y=10
This compiles, but it is easy to read incorrectly. Deconstruction assigns by position, not by matching the names x and y. The corrected version keeps the left side in the same order as the tuple:
using System;
class Program
{
static void Main()
{
var point = (X: 10, Y: 20);
(int x, int y) = point;
Console.WriteLine($"x={x}, y={y}");
}
}
Output:
x=10, y=20
Writing the Wrong Deconstruct Shape
public class Customer
{
public (string Name, int Age) Deconstruct()
{
return ("Ada", 36);
}
}
This looks reasonable, but it is not the deconstruction pattern C# looks for. A deconstructable type needs a void Deconstruct method with out parameters. The corrected shape is:
using System;
public class Customer
{
public Customer(string name, int age)
{
Name = name;
Age = age;
}
public string Name { get; }
public int Age { get; }
public void Deconstruct(out string name, out int age)
{
name = Name;
age = Age;
}
}
class Program
{
static void Main()
{
Customer customer = new Customer("Ada", 36);
var (name, age) = customer;
Console.WriteLine($"{name}: {age}");
}
}
Output:
Ada: 36
Trying to Assign Into Variables That Do Not Exist
var person = (Name: "Ada", Age: 36);
(name, age) = person;
The left side here is assignment deconstruction, so name and age must already exist. Use a declaration deconstruction when creating new variables:
using System;
class Program
{
static void Main()
{
var person = (Name: "Ada", Age: 36);
var (name, age) = person;
Console.WriteLine($"{name}: {age}");
}
}
Output:
Ada: 36
Best Practices
- Use deconstruction when the extracted names make the next few lines easier to read.
- Keep tuple deconstruction local. If the data crosses method or assembly boundaries often, consider a named record or class.
- Remember that tuple and object deconstruction are positional. Keep the variable order visually aligned with the source data.
- Use discards for values you intentionally ignore, especially with tuples returned from helper methods.
- Prefer clear variable names on the left side. Deconstruction can improve readability only if the extracted names carry meaning.
- Define
Deconstructon your own types only when the extracted values are obvious and stable. Do not make callers guess what the positions mean. - Avoid deconstructing very large values. Four or more extracted variables often indicate that a named result type would communicate better.
- Use deconstruction in
foreachloops for pairs and small tuples, but avoid hiding complex logic inside the loop header. - Be careful when two adjacent values have the same type, such as two
intcoordinates. The compiler cannot catch a swapped meaning.
Practice Exercises
- Create a tuple named
bookwithTitle,Author, andYear. Deconstruct it, discard the author, and print the title and year. - Write a method named
GetBoundsthat returns(int Min, int Max)for an array of integers. Deconstruct the result at the call site. - Create a
Studentclass withNameandGradeproperties. Add a properDeconstructmethod and usevar (name, grade) = student;.
Summary
- Deconstruction splits one value into multiple local variables or assignment targets.
- Tuple deconstruction is based on element position, not element names.
- Records automatically support deconstruction for their positional parameters.
- Custom classes and structs can support deconstruction with
void Deconstructandoutparameters. - Discards let you ignore values deliberately with
_. - The compiler rewrites deconstruction into ordinary assignments or
Deconstructmethod calls; the CLR does not need a special deconstruction feature. - Use deconstruction for small, clear groups of data, and switch to named types when the meaning becomes larger or more permanent.
