O C# oferece uma variedade de tipos de dados, entre eles as classes, structs e records. Cada um desses tipos tem um comportamento distinto, com objetivos específicos e cenários ideais de uso. Neste artigo, exploraremos as diferenças entre eles, como usá-los corretamente e como escolher o tipo adequado para diferentes situações.
No C#, a diferença entre tipos por valor e tipos por referência está na forma como eles são armazenados e manipulados na memória.
Os tipos por valor, como structs, são armazenados diretamente na stack. Quando você copia um tipo por valor, uma nova instância é criada, e alterações na cópia não afetam o original. Isso torna os tipos por valor eficientes em termos de memória e desempenho, principalmente para dados pequenos e temporários. Para entender melhor como a stack impacta o desempenho e a alocação de memória, consulte o artigo sobre memória stack e heap.
Os tipos por referência, como classes e records, armazenam uma referência para os dados na heap. Ao copiar uma variável de tipo por referência, estamos copiando apenas o endereço onde os dados estão armazenados, não os dados em si. Isso significa que alterações feitas em uma cópia afetam o objeto original. A alocação na heap é mais flexível, mas também mais lenta, sendo gerenciada pelo garbage collector.
Uma struct é um tipo por valor que armazena seus dados diretamente na memória stack. Os Structs são ideais para representar pequenas unidades de dados, como coordenadas, pontos em um gráfico ou valores simples que não precisam de herança.
Desempenho: como são salvos na stack a sua alocação e desalocação é mais rápida em comparação com objetos alocados na memória heap.
Imutabilidade: Structs geralmente são imutáveis, o que as torna seguras para uso em ambientes concorrentes.
Ideal para pequenos valores: como são mais eficientes para armazenar pequenos valores, structs são úteis quando o desempenho e a economia de memória são importantes.
Imutabilidade: uma vez que structs são imutáveis por design, elas não oferecem suporte a herança, o que limita sua flexibilidade.
Tamanho ideal: Structs devem ser usados para armazenar dados pequenos e simples, como números ou pares de valores. Usá-las para armazenar grandes quantidades de dados pode prejudicar a performance.
Podemos criar uma struct da seguinte forma:
public struct Ponto
{
public int X { get; }
public int Y { get; }
public Ponto(int x, int y)
{
X = x;
Y = y;
}
}
Introduzidos no C# 9, os records são tipos imutáveis e focados em dados. Eles são perfeitos para representar informações que não mudam, como objetos que precisam ser transferidos entre diferentes partes de um sistema, como DTOs (Data Transfer Objects). Diferente das classes, os records têm recursos que facilitam o trabalho com dados, como comparação por valor, imutabilidade e a possibilidade de criar deconstructors.
Imutabilidade: Por padrão, records são imutáveis. Isso significa que seus dados não podem ser modificados após a criação.
Comparação por valor: Ao contrário das classes, que comparam objetos por referência, os records comparam suas propriedades por valor.
Facilidade de transferência de dados: Eles são perfeitos para serem usados como objetos que são transferidos entre sistemas, como APIs REST.
Uma das principais características dos records é a comparação por valor. Isso significa que, quando você compara duas instâncias de um record, o C# compara os valores de suas propriedades, e não a referência do objeto na memória. Isso é diferente das classes, que por padrão, fazem comparação por referência, ou seja, elas só são iguais se apontarem para o mesmo objeto na memória.
public record Produto(int Id, string Nome);
var produto1 = new Produto(1, "Televisão");
var produto2 = new Produto(1, "Televisão");
var produto3 = new Produto(2, "Geladeira");
Console.WriteLine(produto1 == produto2); // True, pois os valores são iguais
Console.WriteLine(produto1 == produto3); // False, pois os valores são diferentes
O C# compara os valores das propriedades de “produto1” e “produto2” (não a referência de memória). Como ambos têm “Id” igual a “1” e “Nome” igual a “Televisão”, a comparação retorna true. Já para “produto3”, os valores são diferentes, então a comparação retorna false.
Esse comportamento é possível porque os records em C# são comparados por valor e não por referência, o que os torna diferentes das classes, que por padrão comparam objetos por referência (ou seja, se apontam para o mesmo local na memória).
Os records em C# também possuem suporte para deconstructors. Isso permite que você extraia as propriedades de um record de forma prática, facilitando o trabalho com os dados armazenados.
Quando você define um record, o C# já cria automaticamente um deconstructor que permite “desestruturar” o objeto e acessar suas propriedades. O deconstructor é útil quando você precisa acessar os valores das propriedades diretamente.
public record Produto(int Id, string Nome);
var produto = new Produto(1, "Produto A");
var (id, nome) = produto;
Console.WriteLine($"Id: {id}, Nome: {nome}");
No exemplo acima, o deconstructor do record “Produto” permite que você extraia facilmente os valores de “Id” e “Nome” diretamente para as variáveis “id” e “nome”.
A expressão “with” permite criar uma cópia de um record com base em um objeto existente, permitindo modificar algumas de suas propriedades enquanto mantém as demais inalteradas.
public record Produto(int Id, string Nome, decimal Preco);
var produtoOriginal = new Produto(1, "Televisão", 2000.00m);
var produtoComDesconto = produtoOriginal with { Preco = 1800.00m };
Console.WriteLine(produtoOriginal);
Console.WriteLine(produtoComDesconto);
No código acima o “produtoOriginal” é copiado para “produtoComDesconto”, mas a propriedade “Preco” é alterada. O produto original permanece imutável, garantindo a segurança de dados.
A expressão “with” facilita a criação de novos objetos com base em instâncias existentes, mantendo a imutabilidade do record.
A seguir, uma tabela comparativa entre classes, structs e records:
Característica | Class | Struct | Record |
|---|---|---|---|
Tipo | Por referência | Por valor | Por referência |
Imutabilidade | Mutável | Geralmente imutável | Imutável |
Herança | Suporta herança | Não suporta herança | Suporta herança |
Comparação | Por referência | Por valor | Por valor |
Cenário Ideal | Objetos complexos, mutáveis | Tipos pequenos e simples | Tipos de dados imutáveis e orientados a dados |
Se você é Desenvolvedor .NET Júnior e quer acelerar sua carreira até nível Pleno com salário de R$7k+, ou mesmo busca a primeira vaga, conheça a Mentoria .NET Start: Clique aqui
Se é Desenvolvedor .NET Pleno ou Sênior e quer virar referência técnica em sua equipe e mercado, com salário de R$10k+, conheça a Mentoria .NET Expert: Clique aqui
Records e structs são dois recursos que ajudam a escrever código mais eficiente, expressivo e seguro no C#. As structs, por serem tipos por valor, são ideais para representar dados pequenos e imutáveis. Já os records oferecem uma maneira fácil e elegante de trabalhar com dados imutáveis e comparar objetos por valor, o que os torna bastante úteis para transferências de dados. Com essas ferramentas, você pode escrever código mais claro, performático e seguro, alinhado com as necessidades do ecossistema .NET moderno.