What is the deconstructor
In C# a deconstructor or deconstructing method is the opposite of the constructor. It allows us to return class fields to variables in one statement.
A deconstruction method is a method that is called Deconstruct inside our struct or class and has one or more out parameters. Although a Deconstruct method with only one out parameter is allowed, this is not very useful as we will see.
Let’s start with an example, we have the class:
public class Enemy
{
public readonly int MaxHp;
public readonly int AttackDamage;
private float _currentHp;
public Enemy (int maxHp, int attackDamage)
{
MaxHp = maxHp;
AttackDamage = attackDamage;
_currentHp = MaxHp;
}
public void TakeDamage(int damage)
{
_currentHp -= damage;
}
public void Deconstruct (out int maxHp, out float currentHp, out int attackDamage)
{
maxHp = MaxHp;
currentHp = _currentHp;
attackDamage = AttackDamage;
}
}
and we create an instance of this class with:
var goblin = new Enemy (20, 4);
The above class has a Deconstruct method, that has three out parameters and can be called like this:
goblin.Deconstruct(out int maxHp, out float currentHp, out int attackDamage);
but that is not very useful, as this can happen with any method we create and it won’t have to be named Deconstruct. The Deconstruct method though, give us some very nice syntactic sugar. The above statement is equal to:
(int maxHp, float currentHp, int attackDamage) = goblin
or to:
(var maxHp, var currentHp, var attackDamage) = goblin;
or even better:
var (maxHp, currentHp, attackDamage) = goblin;
all the above statements initialize variables and add the appropriate values to them. The following piece of code:
var goblin = new Enemy (20, 4);
goblin.TakeDamage(2);
var (maxHp, currentHp, attackDamage) = goblin;
Console.WriteLine (maxHp + " " + currentHp + " " + attackDamage);
will have the output:
20 18 4
that is why a Deconstruct method with only one out parameter wouldn’t be that useful. Writing var maxHp = goblin
is actually an assignment. The variable maxHp
will be of type Enemy
and the output of the statement:
Console.WriteLine(maxHp);
will be Enemy
.
Mix and match of var and explicit type is allowed:
(int maxHp, var currentHp, int attackDamage) = goblin;
but also not very convenient.
Our variables don’t have to be declared before the assignment, the following is still valid:
int maxHp = 200;
float currentHp = 4.17f;
int attackDamage = 100;
(maxHp, currentHp, attackDamage) = goblin;
from C#10 declarations and assignments can be mixed:
int maxHp = 200;
int attackDamage = 100;
(maxHp, var currentHp, attackDamage) = goblin;
How to use the deconstructor
Other than the above example, a Deconstructor method has some interesting uses:
Records
A deconstruct method is automatically created only for the positional parameters of our record, for example:
public record Enemy(int MaxHp, int AttackDamage)
{
public float CurrentHp { get; set; } = MaxHp;
public void TakeDamage(int damage)
{
CurrentHp -= damage;
}
}
by using:
var goblin = new Enemy (20, 4);
var (maxHp, attackDamage) = goblin;
Console.WriteLine(maxHp + " " + attackDamage);
the output will be:
20 4
Overloading
You can overload the Deconstruct method, but each method must have a different number of parameters. For example given the following class:
public class Enemy
{
public readonly int MaxHp;
public readonly int AttackDamage;
private float _currentHp;
public Enemy (int maxHp, int attackDamage)
{
MaxHp = maxHp;
AttackDamage = attackDamage;
_currentHp = MaxHp;
}
public void TakeDamage(int damage)
{
_currentHp -= damage;
}
public void Deconstruct (out int maxHp, out float currentHp, out int attackDamage)
{
maxHp = MaxHp;
currentHp = _currentHp;
attackDamage = AttackDamage;
}
public void Deconstruct(out int maxHp, out float currentHp)
{
maxHp = MaxHp;
currentHp = _currentHp;
}
public void Deconstruct(out int maxHp, out int attackDamage)
{
maxHp = MaxHp;
attackDamage = AttackDamage;
}
}
this is valid:
var (maxHp, currentHp, attackDamage) = goblin;
but these are not:
var (maxHp2, currentHp2) = goblin;
(int maxHp3, float currentHp3) = goblin;
for both of those statements the compiler complains for ambiguous invocation, even though we have explicitly written the type of our variables in the second statement. Calling the deconstruct method using the normal syntax though is still valid:
goblin.Deconstruct(out int maxHp, out float currentHp);
Extension methods
The deconstruct method can be an extension method. This is useful if we want to use it in types that we don’t have access to, like the types that already exist in the BCL.
For more info on extension methods, you can check my blog post about 4 Ways To Use Extension Methods In C# Other Than Extending Existing Types
Discards
We can use C#’s discard symbol, if we call a deconstruct method that returns variables that we are not going to use, like this:
var (maxHp, _, attackDamage) = goblin;
Calculations
Finally the Deconstruct method can be used to return multiple calculated values. This is not very intuitive and can confuse people that don’t have access to the implementation details of our Deconstruct method. Unless someone has access to our class code or the documentation of our class, there is no way of knowing that a class has a Deconstruct method that makes calculations, just by looking at its signature, and then returns different values.
Although it can be very confusing for people not familiar with our code, the Deconstruct method can be thought as an overload of the implicit operator for multiple values. For example we can have the following classes:
public class Celsius
{
private readonly double _value;
public Celsius(double celsius) => _value = celsius;
public override string ToString() => _value.ToString(CultureInfo.InvariantCulture);
}
public class Fahrenheit
{
private readonly double _value;
public Fahrenheit(double fahrenheit) => _value = fahrenheit;
public override string ToString() => _value.ToString(CultureInfo.InvariantCulture);
}
public class Kelvin
{
private readonly double _value;
public Kelvin(double kelvin) => _value = kelvin;
public override string ToString() => _value.ToString(CultureInfo.InvariantCulture);
public void Deconstruct(out Celsius celsius, out Fahrenheit fahrenheit)
{
celsius = new Celsius(_value - 273.15);
fahrenheit = new Fahrenheit((_value - 273.15) * 9 / 5 + 32);
}
}
Here the Kelvin class has a deconstructor that returns the temperature degrees for both Celsius and Fahrenheit, so the following code is valid although confusing for people that don’t have access to the implementation or the documentation of the Kelvin class:
var input = Console.ReadLine();
if(input != null && double.TryParse(input, CultureInfo.InvariantCulture, out var temp))
{
var temperature = new Kelvin(temp);
var (celsius, fahrenheit) = temperature;
Console.WriteLine($"{temperature} degrees in Kelvin equal to {celsius} Celsius and {fahrenheit} Fahrenheit.");
}
Conclusion
I haven’t seen a lot of usage of the deconstruct method, but under certain circumstances can be useful for returning multiple values, especially when we always have a use for all of those values, by limiting the number of lines of code that we have to write. By using it as an extension method can condense our code even in types that we have not written and don’t have access to the source code.
Thank you for reading and as always if you have any questions or comments you can use the comments section or contact me directly via the contact form or email, also if you don’t want to miss any of the new articles, you can always subscribe to my newsletter or the RSS feed.