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.