Deconstruct no C#
O Deconstruct é o método que permite desconstruir um objeto no C# e obter suas propriedades em variáveis separadas, de forma simples e direta. O recurso lembra a desestruturação do JavaScript e deixa o código mais curto, principalmente quando você precisa de vários valores do mesmo objeto.
O que é o Deconstruct no C#
Afinal, o nome deconstruct significa desconstruir, que é exatamente o objetivo aqui. Em outras palavras, você pega um objeto e o desmembra em variáveis independentes em uma única linha.
Na prática, a sintaxe fica parecida com a declaração de uma tupla, porque as variáveis aparecem entre parênteses do lado esquerdo da atribuição.
var (year, month, day) = DateTime.Now;
O DateTime não oferece esse comportamento por padrão. Por isso, ao longo deste artigo, vamos criar um método Deconstruct para ele e entender cada detalhe da implementação.
Segundo a documentação oficial sobre desconstrução de tuplas e outros tipos, o C# já suporta a desconstrução de tuplas, records e DictionaryEntry sem nenhum código extra. Para os demais tipos, basta implementar um ou mais métodos Deconstruct.
Deconstruct não é destrutor
O primeiro passo é não confundir o Deconstruct com o destrutor do objeto. O destrutor, que hoje a documentação chama de finalizador, usa a sintaxe ~ClassName() e roda quando o coletor de lixo libera a instância.
Já o Deconstruct é um método comum, chamado pelo compilador quando você escreve uma atribuição de desconstrução. Ele não tem relação com memória nem com o ciclo de vida do objeto.
public class Report
{
// Finalizador: executado pelo garbage collector
~Report()
{
}
// Deconstruct: executado em uma atribuição de desconstrução
public void Deconstruct(out string title, out int pages)
{
title = "Monthly report";
pages = 10;
}
}
Portanto, os nomes são parecidos, mas os papéis são completamente diferentes.
Onde o Deconstruct já existe
Antes de criar a sua própria implementação, vale conhecer os tipos que já suportam desconstrução. Assim, você evita escrever código desnecessário.
Primeiro, as tuplas têm suporte nativo. Se você ainda não domina o assunto, veja o artigo sobre tuplas no C#, que mostra a base usada aqui.
var (city, population) = ("Sao Paulo", 11_450_000);
Em seguida, os records posicionais também já vêm com um Deconstruct pronto. Quando você declara um record com dois ou mais parâmetros posicionais, o compilador cria o método com um parâmetro out para cada um.
public record Customer(string Name, string Email);
var customer = new Customer("John Doe", "john@example.com");
var (name, email) = customer;
Por fim, o KeyValuePair<TKey, TValue> também oferece o método. Por isso, você consegue desconstruir cada item de um Dictionary dentro de um foreach.
var stock = new Dictionary<string, int>
{
["keyboard"] = 15,
["mouse"] = 42
};
foreach (var (product, quantity) in stock)
Console.WriteLine($"{product}: {quantity}");
Criando um Deconstruct com extension methods
Quando o tipo não é seu, como o DateTime, você pode adicionar o Deconstruct por meio de um método de extensão. Em artigos anteriores, falamos sobre métodos de extensão no C# e como estender tipos built-in do .NET de forma simples.
O Deconstruct funciona de forma similar a qualquer outro extension method. A única diferença está no nome, que obrigatoriamente precisa ser Deconstruct.
Criando a classe estática
Todo extension method precisa morar dentro de uma classe estática, e com o Deconstruct não é diferente. Então, vamos começar por ela.
public static class DateTimeExtensions
{
// ...
}
Definindo a assinatura do Deconstruct
Em seguida, criamos o método com o nome exigido pelo compilador. Neste ponto, ele ainda não recebe nenhum parâmetro.
public static class DateTimeExtensions
{
public static void Deconstruct()
{
// ...
}
}
Como estamos estendendo o DateTime, o método precisa de um parâmetro de entrada com a palavra this na frente. Esse é o mesmo requisito de qualquer extension method.
public static class DateTimeExtensions
{
public static void Deconstruct(this DateTime dateTime)
{
// ...
}
}
Retornando valores com out
Além do nome obrigatório, o método precisa expor os valores que serão desmembrados. Aqui vem a parte interessante, porque desconstruir um objeto significa separá-lo em variáveis.
No nosso caso, vamos desmembrar a data em três valores: ano, mês e dia. Para isso, usamos parâmetros out.
public static class DateTimeExtensions
{
public static void Deconstruct(
this DateTime dateTime,
out int year, out int month, out int day)
{
// ...
}
}
Note que o Deconstruct é do tipo void e não tem um retorno específico. Por isso, a palavra reservada out gera as três variáveis, year, month e day, todas do tipo int.
Agora, só precisamos atribuir às variáveis os valores do DateTime recebido como entrada. Assim, o compilador entrega esses valores de forma desconstruída para quem chamar o método.
public static class DateTimeExtensions
{
public static void Deconstruct(
this DateTime dateTime,
out int year, out int month, out int day)
{
year = dateTime.Year;
month = dateTime.Month;
day = dateTime.Day;
}
}
Melhorando o código com tuplas
Antes de prosseguir, podemos melhorar o Deconstruct usando tuplas. Dessa forma, criamos um conjunto com os três parâmetros out e fazemos a atribuição em grupo.
public static class DateTimeExtensions
{
public static void Deconstruct(
this DateTime dateTime,
out int year, out int month, out int day)
{
(year, month, day) = (dateTime.Year, dateTime.Month, dateTime.Day);
}
}
Como o método agora tem apenas uma linha, também podemos transformá-lo em um expression body. Para isso, usamos => e eliminamos as chaves.
public static class DateTimeExtensions
{
public static void Deconstruct(
this DateTime dateTime,
out int year, out int month, out int day)
=> (year, month, day) = (dateTime.Year, dateTime.Month, dateTime.Day);
}
O resultado é um código curto, fácil de ler e com o mesmo comportamento da versão anterior.
Utilizando o Deconstruct
Para usar o Deconstruct, basta atribuir uma data a uma tupla que espera três valores, assim como definimos nos parâmetros out.
var (y, m, d) = DateTime.Now;
Console.WriteLine(y);
Console.WriteLine(m);
Console.WriteLine(d);
Repare que o nome das variáveis não importa, tanto que usamos y, m e d. No entanto, o compilador atribui os valores conforme a ordem dos out, que no nosso caso é ano, mês e dia.
A ordem dos parâmetros importa
Não adianta inverter os nomes na hora de receber os valores. Nesse caso, você muda apenas o nome das variáveis e ainda confunde quem lê o código.
// Os nomes estão invertidos, mas a ordem continua ano, mês e dia
var (day, year, month) = DateTime.Now;
Console.WriteLine(day); // imprime o ano
Console.WriteLine(year); // imprime o mês
Console.WriteLine(month); // imprime o dia
A ordem sempre será ano, mês e dia, conforme definida nos out do método. Portanto, prefira nomes intuitivos, mesmo que maiores, e siga a mesma ordem da assinatura.
var (year, month, day) = DateTime.Now;
Console.WriteLine(year);
Console.WriteLine(month);
Console.WriteLine(day);
Ignorando valores com discards
Muitas vezes você precisa de apenas parte dos valores que o Deconstruct expõe. Nesse cenário, o C# oferece os discards, representados pelo caractere _.
// Apenas o ano interessa aqui
var (year, _, _) = DateTime.Now;
Console.WriteLine(year);
Com isso, você evita variáveis sem uso e deixa clara a intenção do código. Além disso, uma mesma desconstrução pode ter quantos discards forem necessários.
Desconstrução aninhada
O C# também permite desconstruir valores que contêm outros valores desconstruíveis. Por exemplo, um segmento formado por dois pontos pode virar quatro variáveis em uma única linha.
public record Point(int X, int Y);
public record Segment(Point Start, Point End);
var segment = new Segment(new Point(0, 0), new Point(10, 5));
// Cada record chama o seu próprio Deconstruct
var ((startX, startY), (endX, endY)) = segment;
Nesse caso, o compilador chama primeiro o Deconstruct do Segment e, em seguida, o de cada Point. Embora o recurso seja poderoso, use com moderação, porque muitos níveis de parênteses prejudicam a leitura.
Deconstruct em loops
Outro uso prático aparece dentro do foreach. Quando a coleção guarda objetos desconstruíveis, você pode separar as propriedades na própria declaração do loop.
var orders = new List<Order>
{
new("A-001", 150.00m),
new("A-002", 89.90m)
};
foreach (var (code, total) in orders)
Console.WriteLine($"{code}: {total:C}");
public record Order(string Code, decimal Total);
Dessa forma, o corpo do loop trabalha com variáveis de nomes claros em vez de repetir order.Code e order.Total. O mesmo raciocínio vale para o resultado de consultas LINQ que retornam tuplas ou records.
Formas de declarar as variáveis
Em qualquer desconstrução, o var do lado de fora dos parênteses pede para o compilador inferir todos os tipos. Contudo, você também pode declarar os tipos explicitamente ou reaproveitar variáveis existentes.
// Tipos explícitos
(int year, int month, int day) = DateTime.Now;
// Variáveis já declaradas
int y, m, d;
(y, m, d) = DateTime.Now;
A documentação também alerta para um erro comum. Você não pode escrever um tipo específico fora dos parênteses, como int (a, b, c), porque o compilador gera o erro CS8136.
Deconstruct como método de instância
O extension method é o caminho quando o tipo pertence a outra pessoa. Porém, quando a classe é sua, você pode declarar o Deconstruct diretamente nela, como um método de instância.
public class Address
{
public string Street { get; set; } = string.Empty;
public string City { get; set; } = string.Empty;
public string ZipCode { get; set; } = string.Empty;
// Desconstrói o endereço em rua, cidade e CEP
public void Deconstruct(out string street, out string city, out string zipCode)
=> (street, city, zipCode) = (Street, City, ZipCode);
}
var address = new Address
{
Street = "Main Street",
City = "Sao Paulo",
ZipCode = "01000-000"
};
var (street, city, zipCode) = address;
Nesse formato, o método não precisa de this nem de classe estática. Afinal, ele já tem acesso direto às propriedades do próprio objeto.
Essa abordagem funciona para classes, structs e até interfaces, como a documentação descreve. Além disso, ela deixa o recurso visível para qualquer pessoa que use o tipo, sem depender de um using extra.
Em geral, prefira o método de instância quando você controla o código-fonte. Desse modo, o comportamento de desconstrução fica documentado junto da própria classe, no mesmo arquivo em que estão as propriedades que ele expõe.
Sobrecargas do Deconstruct
Um mesmo tipo pode ter vários métodos de desconstrução. Por exemplo, você pode expor só a data ou a data com o horário.
public static class DateTimeExtensions
{
public static void Deconstruct(
this DateTime dateTime,
out int year, out int month, out int day)
=> (year, month, day) = (dateTime.Year, dateTime.Month, dateTime.Day);
public static void Deconstruct(
this DateTime dateTime,
out DateOnly date, out TimeOnly time)
=> (date, time) = (DateOnly.FromDateTime(dateTime), TimeOnly.FromDateTime(dateTime));
}
var (year, month, day) = DateTime.Now;
var (date, time) = DateTime.Now;
Entretanto, existe uma regra importante. Sobrecargas com a mesma quantidade de parâmetros ficam ambíguas, porque o compilador escolhe o método pelo número de variáveis.
Então, cada Deconstruct do mesmo tipo precisa ter uma aridade diferente, ou seja, um número diferente de parâmetros out.
Deconstruct com extension members do C# 14
O C# 14 trouxe uma nova sintaxe para extensões, com o bloco extension. A própria documentação já usa esse modelo para declarar um Deconstruct de extensão.
public static class DateTimeExtensions
{
extension(DateTime dateTime)
{
// O receptor é declarado no bloco, sem o this no parâmetro
public void Deconstruct(out int year, out int month, out int day)
=> (year, month, day) = (dateTime.Year, dateTime.Month, dateTime.Day);
}
}
O comportamento continua o mesmo, mas o receptor fica declarado uma única vez no bloco. Para entender o novo modelo em detalhes, leia o artigo sobre extension members no C# 14.
Vale lembrar que a sintaxe clássica com this continua válida. Portanto, você pode adotar o bloco novo apenas quando o projeto já estiver no C# 14.
Deconstruct em pattern matching
O método também aparece em um lugar menos óbvio: o padrão posicional do pattern matching. Esse padrão desconstrói o valor e testa cada parte contra outro padrão.
public record Point(int X, int Y);
static string Classify(Point point) => point switch
{
(0, 0) => "Origin",
(_, 0) => "On the X axis",
(0, _) => "On the Y axis",
_ => "Somewhere else"
};
Nesse exemplo, o record Point já tem o método gerado pelo compilador. Assim, o switch consegue comparar X e Y diretamente na sintaxe de tupla.
O mesmo vale para os seus próprios tipos. Se você implementar um Deconstruct, ganha suporte ao padrão posicional sem nenhum código adicional.
Quando usar o Deconstruct
O recurso brilha quando um objeto representa um conjunto pequeno e estável de valores. Datas, coordenadas, intervalos, pares de chave e valor e resultados de operações são bons candidatos.
Por outro lado, ele perde o sentido em entidades grandes, com dezenas de propriedades. Nesse cenário, a lista de parâmetros out fica longa e a ordem dos valores vira uma fonte de erros.
Também vale pensar no significado de cada posição. Se quem lê o código precisa abrir a assinatura do método para descobrir o que é o terceiro valor, talvez um record nomeado seja uma escolha melhor.
Além disso, lembre que a desconstrução é apenas açúcar sintático. Na prática, o compilador traduz a atribuição para uma chamada ao Deconstruct com variáveis out, então não existe mágica nem reflexão envolvida.
Erros comuns ao usar o Deconstruct
O erro mais frequente é esquecer o using do namespace onde está a classe de extensão. Sem ele, o compilador não enxerga o método e reclama que não encontrou um Deconstruct adequado para o tipo.
// Sem este using, a desconstrução do DateTime não compila
using MyProject.Extensions;
var (year, month, day) = DateTime.Now;
Outro problema comum é a quantidade de variáveis diferente da quantidade de parâmetros out. Nesse caso, o compilador procura uma sobrecarga com a aridade certa e, se não encontrar, gera um erro de compilação.
// Erro: não existe Deconstruct com quatro parâmetros out para DateTime
// var (year, month, day, hour) = DateTime.Now;
Também acontece de alguém declarar duas sobrecargas com o mesmo número de parâmetros e tipos diferentes. Como vimos, essa combinação fica ambígua, então o código que tenta desconstruir o objeto deixa de compilar.
Por último, existe o erro silencioso da ordem invertida. O compilador aceita var (day, month, year), mas cada variável recebe o valor da posição correspondente na assinatura, o que gera bugs difíceis de encontrar.
Portanto, trate a assinatura do método como um contrato público. Mudar a ordem dos parâmetros depois que outras partes do sistema já usam o recurso equivale a uma quebra de compatibilidade.
Boas práticas ao criar um Deconstruct
Em primeiro lugar, mantenha o método simples e sem efeitos colaterais. Quem escreve var (a, b) = obj espera apenas ler valores, não disparar consultas ao banco ou alterar estado.
Em segundo lugar, siga uma ordem natural para os parâmetros, como ano, mês e dia para datas ou X e Y para coordenadas.
Assim, o uso fica intuitivo mesmo sem consultar a assinatura.
Também é recomendável dar nomes descritivos aos parâmetros out. Embora o compilador ignore esses nomes na atribuição, eles aparecem no IntelliSense e orientam quem consome o tipo.
Por fim, documente as sobrecargas quando houver mais de uma. Como cada versão do Deconstruct depende apenas da quantidade de variáveis, um comentário curto evita dúvidas sobre qual método o compilador vai escolher.
Se você trabalha com records, combine o Deconstruct com a expressão with no C#. Juntos, os dois recursos facilitam ler e copiar dados imutáveis com pouco código.
Conclusão
O C# tem inúmeras funcionalidades pouco conhecidas, e o Deconstruct é uma delas. Ele agrega valor ao código, porque troca várias linhas de atribuição por uma única desconstrução legível.
Em resumo, você pode implementar o Deconstruct como método de instância nos seus tipos ou como extensão para tipos de terceiros. Além disso, records, tuplas e KeyValuePair já trazem o recurso pronto para uso.
Agora, procure no seu projeto um trecho que lê várias propriedades do mesmo objeto em sequência. Em seguida, crie um método de desconstrução para ele e compare a legibilidade.
FAQ
O Deconstruct precisa ser um extension method?
Não, ele pode ser um método de instância no próprio tipo. A extensão só é necessária quando você não controla o código da classe.
O nome das variáveis precisa ser igual ao dos parâmetros out?
Não, os valores seguem a ordem dos parâmetros out. Mesmo assim, usar os mesmos nomes evita confusão.
Posso ter mais de um Deconstruct no mesmo tipo?
Sim, desde que cada sobrecarga tenha uma quantidade diferente de parâmetros. Sobrecargas com a mesma aridade ficam ambíguas.
Records têm Deconstruct automático?
Sim, records posicionais com dois ou mais parâmetros ganham o método gerado pelo compilador.
Qual a diferença entre Deconstruct e destrutor?
O destrutor, ou finalizador, roda na coleta de lixo. Já o Deconstruct apenas separa o objeto em variáveis durante uma atribuição.