C# Deconstruction: Syntax, Custom Types, and Pitfalls
c# deconstruction: Learn how to use C# deconstruction with tuples and custom types, including the Deconstruct method, discards, and common pitfalls.
c# deconstruction requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.
Deconstruction in C# lets you split an object into its component values in a single assignment. It's commonly used with tuples, but you can also enable it for your own types by implementing a Deconstruct method. This feature reduces boilerplate when you need to extract multiple values from a single object, and it integrates cleanly with pattern matching.
What Is Deconstruction in C#?
Deconstruction is a syntax that unpacks an object into separate variables. For example, a tuple (int, string) can be deconstructed into two variables in one statement. The compiler looks for a Deconstruct method on the type, which uses out parameters. For tuples, the compiler provides built-in support. For custom types, you define Deconstruct yourself.
The key difference between deconstruction and a regular method call is that deconstruction is designed for the assignment syntax var (a, b) = obj;. The compiler rewrites this into calls to Deconstruct with out variables. This is not reflection-based; it's resolved at compile time.
Deconstructing Tuples and Built-in Types
Tuples are the most common use case. You can assign each element to a variable directly:
var person = ("Alice", 30); var (name, age) = person;
This is equivalent to var name = person.Item1; var age = person.Item2;. You can also specify explicit types instead of var:
(string name, int age) = person;
The compiler checks that the number of variables matches the tuple arity. If you try to deconstruct a two-element tuple into three variables, you get a compile error.
Many BCL types also support deconstruction. For example, KeyValuePair<TKey, TValue> has a Deconstruct method, so you can write:
var pair = new KeyValuePair<string, int>("key", 42); var (key, value) = pair;
This works because the .NET runtime defines Deconstruct for that type. When you're unsure whether a type supports deconstruction, look for a public Deconstruct method or extension method.
Implementing Deconstruct for Custom Types
To support deconstruction for your own class or struct, define a Deconstruct method with out parameters. It must be an instance method or an extension method. Here's an example:
public class Point { public int X { get; } public int Y { get; } public Point(int x, int y) => (X, Y) = (x, y); public void Deconstruct(out int x, out int y) => (x, y) = (X, Y); }
Now you can write:
var point = new Point(3, 4); var (x, y) = point; Console.WriteLine($"({x}, {y})");
You can have multiple Deconstruct overloads with different numbers of out parameters. For instance, a Rectangle might provide a two-parameter version for width and height, and a four-parameter version for the corner coordinates. The compiler picks the overload based on the number of variables in the deconstruction statement.
If you cannot modify the type, you can define an extension method named Deconstruct. The extension method must be in scope at the call site. This is useful for types you don't own, but be careful: adding an extension method can affect all deconstruction attempts for that type in the namespace.
Using Discards to Skip Values
When you only need some of the values, use discards. A discard is a variable named _ that you intentionally ignore. For example:
var (_, age) = person;
This assigns only age and discards the name. You can use multiple discards in the same deconstruction:
var (_, _) = tuple; // discards both elements
Discards are not variables; you cannot read from them later. They signal to the compiler that the value is intentionally unused. This is clearer than declaring a throwaway variable like dummy because it communicates intent.
Deconstruction in Pattern Matching
Deconstruction is also used in pattern matching, especially with positional patterns. In a switch expression, you can match on a type and deconstruct its values in one step:
var description = point switch { Point(0, 0) => "origin", Point(0, _) => "on y-axis", Point(_, 0) => "on x-axis", _ => "other" };
Here Point(0, 0) is a positional pattern. It calls Deconstruct on the Point instance and checks whether the first out value equals 0 and the second equals 0. The _ in Point(0, _) discards the second value while still matching the first. This works with any type that has a Deconstruct method.
Positional patterns are especially useful with records, because records automatically generate a Deconstruct method based on their positional parameters. For example:
public record Person(string Name, int Age);
This record has an implicit Deconstruct method, so you can write var (name, age) = person; and use Person("Alice", 30) in a pattern.
Common Mistakes and Pitfalls
A frequent mistake is defining Deconstruct with ref parameters instead of out. The signature must use out because the compiler assigns to those parameters after the method returns. Using ref causes a compile error.
Another issue is mismatched arity. If you write var (a, b) = obj; but obj only has a Deconstruct with three parameters, the code won't compile. The compiler does not automatically discard extra values; you must use discards explicitly.
Extension method Deconstruct can cause subtle problems if multiple extension methods with the same name exist in different namespaces. The compiler resolves the one that is in scope. If you get an ambiguity error, check which namespaces are imported.
Finally, don't confuse deconstruction with destructuring in other languages. In C#, deconstruction is purely a compile-time convenience; it does not change the object itself or create a new object. It simply copies values into variables.
Performance and Allocation Considerations
Deconstruction itself does not introduce runtime overhead beyond the method call. For tuples, the compiler often inlines the access to Item1 and Item2. For custom types, the Deconstruct method is called like any other method. If that method allocates, then deconstruction can allocate, but that's true for any method call.
A common performance concern is deconstructing large objects in hot paths. For example, if Deconstruct creates a new collection or performs a heavy computation, the cost is the same as calling that method directly. There is no hidden magic. In practice, deconstruction is usually used with lightweight value types or records, where the overhead is negligible.
If you're implementing Deconstruct for a struct, keep it simple and avoid allocations inside the method. Use out parameters to copy fields directly. This keeps the operation cheap and predictable.
When to Use Deconstruction in Your Codebase
Deconstruction improves readability when you need multiple values from an object and want to avoid repetitive property access. It's particularly effective with tuples and records, where the shape is clear. Use it in local variables, foreach loops over collections of tuples, and pattern matching.
Avoid overusing deconstruction when the meaning of the individual values is unclear. For example, deconstructing a DateTime into (year, month, day) is fine, but deconstructing a complex object with many fields into a long list of variables can hurt readability. If you find yourself deconstructing the same object repeatedly, consider whether a dedicated class or struct would be clearer.
Deconstruction is also a good fit for value objects that represent a coordinate, a range, or a pair of related values. By providing a Deconstruct method, you make the type easier to work with in pattern matching and in code that needs to unpack the values. This is a maintainability win because the method is defined in one place and the syntax is consistent across call sites.