1
resposta

[Dúvida] Onde é aplicado Structs num projeto real ?

Tenho estudado sobre o uso de struct em C# e entendi que pode trazer ganhos de desempenho em determinados cenários. No entanto, ainda tenho dúvidas sobre onde exatamente ele se encaixa na prática. Em um projeto típico com Services, DTOs e Exceptions, existe algum caso real onde o uso de struct seria apropriado ou até preferível em relação a classes? Você poderia me dar um exemplo prático de onde usou ou usaria struct nesse tipo de arquitetura?

1 resposta

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:

  1. 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.
    }
}
  1. 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.