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.

Diferença entre tipos por valor e por referência

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.

Tipos por valor

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.

Tipos por referência

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.

Struct em C#

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.

Vantagens das Structs

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.

Limitações das Structs

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;
    }
}

				
			

Record

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.

Características dos Records

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.

Comparação por valor em Records

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).

Criando um Deconstructor com Record

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”.

Criando cópias de Records com a expressão with

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.

Comparação: Class vs Struct vs 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

Acelere a sua carreira conosco!

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 StartClique 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 ExpertClique aqui

Conclusão

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.