Oii, Mateus!
Ótima pergunta! A resposta curta é: structs fazem sentido quando você quer representar um “valor pequeno” (comportamento mínimo, foco em dados), que não vai circular como object
/interface e que aparece em grande volume (coleções, hot paths). Em uma arquitetura típica com Services, DTOs e Exceptions, dá para usar sim, com alguns cuidados.
Quando usar struct
na prática:
Value Objects pequenos (sem identidade própria)
- Exemplos clássicos:
Coordinate
(Latitude/Longitude), DateRange
(Início/Fim), RgbColor
(R,G,B), Money
(se for compacto). - Por quê? São dados “atômicos”, passados por valor, ótimos para evitar alocações na heap e reduzir GC em loops/coleções.
IDs fortemente tipados
- Em vez de espalhar
Guid
/int
cru, crie um tipo de ID.
public readonly record struct UserId(Guid Value);
- Vantagens: segurança de tipo (você não mistura
OrderId
com UserId
), sem overhead de alocação se mantiver pequeno. - Uso: Services recebem/retornam
UserId
; DTOs carregam UserId
; repositórios mapeiam via conversor (EF Core) para Guid
.
Chaves de dicionário e estruturas de dados
- Um
struct
pequeno com IEquatable<T>
e GetHashCode
bem implementado é excelente como chave:
public readonly struct CustomerOrderKey : IEquatable<CustomerOrderKey>
{
public CustomerOrderKey(int customerId, int orderId)
=> (CustomerId, OrderId) = (customerId, orderId);
public int CustomerId { get; }
public int OrderId { get; }
public bool Equals(CustomerOrderKey other)
=> CustomerId == other.CustomerId && OrderId == other.OrderId;
public override bool Equals(object? obj) => obj is CustomerOrderKey other && Equals(other);
public override int GetHashCode() => HashCode.Combine(CustomerId, OrderId);
}
- Diminui alocações e melhora tempo de lookup em caches locais, dicionários, etc.
Interop/parsing de alto desempenho
ref struct
como Span<T>
/ReadOnlySpan<T>
evitam alocações em parsing, serialização, manipulação de texto.- Úteis em bibliotecas e pontos de “hot path” (por exemplo, normalizar entradas antes de chegar aos Services).
Regras de bolso (bem práticas):
- Pequeno é lindo: mantenha o
struct
“compacto” (ex.: até ~16 a 32 bytes é um guia comum). Dois double
(lat/long) cabem bem. - Imutabilidade ajuda: prefira
readonly struct
ou record struct
para semântica de valor e menos bugs de cópia. - Evite boxing: não exponha o
struct
via interfaces/object
. Se precisar de polimorfismo, provavelmente é classe. - Implemente igualdade:
IEquatable<T>
, Equals
, GetHashCode
coerentes — especialmente se for usar como chave. - Cuidado com campos de referência: strings/listas dentro de
struct
tornam o tipo “pesado” e podem anular ganhos.
Exemplos práticos na arquitetura proposta:
- Value Object em domínio + uso em Service/DTO
// Domínio
public readonly struct Coordinate : IEquatable<Coordinate>
{
public Coordinate(double latitude, double longitude)
=> (Latitude, Longitude) = (latitude, longitude);
public double Latitude { get; }
public double Longitude { get; }
public override string ToString()
=> string.Create(CultureInfo.InvariantCulture, $"{Latitude},{Longitude}");
public bool Equals(Coordinate other)
=> Latitude == other.Latitude && Longitude == other.Longitude;
public override bool Equals(object? obj) => obj is Coordinate c && Equals(c);
public override int GetHashCode() => HashCode.Combine(Latitude, Longitude);
}
// DTO (classe), contendo o struct
public class AccessLogDto
{
public required string UserEmail { get; init; }
public required Coordinate Coordinate { get; init; } // sem alocação extra
public DateTime TimestampUtc { get; init; }
}
// Service usa o struct sem boxing
public sealed class AccessService
{
public void RegisterAccess(UserId userId, Coordinate coordinate, DateTime timestampUtc)
{
// validações, persistência, etc.
}
}
- ID forte como
record struct
(Services/Repos/DTOs)
public readonly record struct UserId(Guid Value);
// Service
public User GetUser(UserId id) { /*...*/ }
// DTO
public sealed class UserDto
{
public UserId Id { get; init; }
public required string Email { get; init; }
}
No banco (EF Core), mapeie com ValueConverter<UserId, Guid>
, você segue usando Guid
na coluna e UserId
no código, sem custo de alocação no caminho feliz.
Espero ter ajudado.
Um abraço e bons estudos.