O que Design de Tipos para Performance faz?
Quando Usar Esta Skill
Use esta skill quando:
- Projetando novos tipos e APIs
- Revisando código em busca de problemas de performance
- Escolhendo entre classe, struct e record
- Trabalhando com coleções e enumeráveis
Princípios Fundamentais
- Sele seus tipos - A menos que explicitamente projetados para herança
- Prefira structs readonly - Para tipos de valor pequenos e imutáveis
- Prefira funções estáticas puras - Melhor performance e testabilidade
- Adie a enumeração - Não materialize até que seja necessário
- Retorne coleções imutáveis - Dos limites da API
Sele Classes por Padrão
Selar classes permite a desvirtualização JIT e comunica a intenção da API.
// FAÇA: Sele classes não projetadas para herança
public sealed class OrderProcessor
{
public void Process(Order order) { }
}
// FAÇA: Sele records (eles são classes)
public sealed record OrderCreated(OrderId Id, CustomerId CustomerId);
// NÃO FAÇA: Deixar sem selar sem motivo
public class OrderProcessor // Pode ser subclassificado - intencional?
{
public virtual void Process(Order order) { } // Virtual = mais lento
}
Benefícios:
- JIT pode desvirtualizar chamadas de método
- Comunica "este não é um ponto de extensão"
- Previne alterações de quebra acidentais
Structs Readonly para Tipos de Valor
Structs devem ser readonly quando imutáveis. Isso previne cópias defensivas.
// FAÇA: Struct readonly para tipos de valor imutáveis
public readonly record struct OrderId(Guid Value)
{
public static OrderId New() => new(Guid.NewGuid());
public override string ToString() => Value.ToString();
}
// FAÇA: Struct readonly para dados pequenos e de curta duração
public readonly struct Money
{
public decimal Amount { get; }
public string Currency { get; }
public Money(decimal amount, string currency)
{
Amount = amount;
Currency = currency;
}
}
// NÃO FAÇA: Struct mutável (causa cópias defensivas)
public struct Point // Não é readonly!
{
public int X { get; set; } // Mutável!
public int Y { get; set; }
}
Quando Usar Structs
| Use Struct Quando | Use Classe Quando |
|---|---|
| Pequeno (≤16 bytes tipicamente) | Objetos maiores |
| Curta duração | Longa duração |
| Frequentemente alocado | Referências compartilhadas necessárias |
| Semântica de valor necessária | Semântica de identidade necessária |
| Imutável | Estado mutável |
Prefira Funções Estáticas Puras
Métodos estáticos sem efeitos colaterais são mais rápidos e mais testáveis.
// FAÇA: Função estática pura
public static class OrderCalculator
{
public static Money CalculateTotal(IReadOnlyList<OrderItem> items)
{
var total = items.Sum(i => i.Price * i.Quantity);
return new Money(total, "USD");
}
}
// Uso - previsível, testável
var total = OrderCalculator.CalculateTotal(items);
Benefícios:
- Sem consulta à vtable (mais rápido)
- Sem estado oculto
- Mais fácil de testar (entrada pura → saída)
- Thread-safe por design
- Força dependências explícitas
// NÃO FAÇA: Método de instância escondendo dependências
public class OrderCalculator
{
private readonly ITaxService _taxService; // Dependência oculta
private readonly IDiscountService _discountService; // Dependência oculta
public Money CalculateTotal(IReadOnlyList<OrderItem> items)
{
// Do que isso realmente depende?
}
}
// MELHOR: Dependências explícitas via parâmetros
public static class OrderCalculator
{
public static Money CalculateTotal(
IReadOnlyList<OrderItem> items,
decimal taxRate,
decimal discountPercent)
{
// Todas as entradas visíveis
}
}
Não exagere - Use métodos de instância quando você realmente precisar de estado ou polimorfismo.
Adie a Enumeração
Não materialize enumeráveis até que seja necessário. Evite encadeamentos excessivos de LINQ.
// RUIM: Materialização prematura
public IReadOnlyList<Order> GetActiveOrders()
{
return _orders
.Where(o => o.IsActive)
.ToList() // Materializado!
.OrderBy(o => o.CreatedAt) // Outra iteração
.ToList(); // Materializado novamente!
}
// BOM: Adie até o final
public IReadOnlyList<Order> GetActiveOrders()
{
return _orders
.Where(o => o.IsActive)
.OrderBy(o => o.CreatedAt)
.ToList(); // Materialização única
}
// BOM: Retorne IEnumerable se o chamador pode não precisar de todos os itens
public IEnumerable<Order> GetActiveOrders()
{
return _orders
.Where(o => o.IsActive)
.OrderBy(o => o.CreatedAt);
// O chamador decide quando materializar
}
Enumeração Assíncrona
Tenha cuidado com async e IEnumerable:
// RUIM: Async em LINQ - alocações ocultas
var results = orders
.Select(async o => await ProcessOrderAsync(o)) // Task por item!
.ToList();
await Task.WhenAll(results);
// BOM: Use IAsyncEnumerable para streaming
public async IAsyncEnumerable<OrderResult> ProcessOrdersAsync(
IEnumerable<Order> orders,
[EnumeratorCancellation] CancellationToken ct = default)
{
foreach (var order in orders)
{
ct.ThrowIfCancellationRequested();
yield return await ProcessOrderAsync(order, ct);
}
}
// BOM: Processamento em lote para paralelismo
var results = await Task.WhenAll(
orders.Select(o => ProcessOrderAsync(o)));
ValueTask vs Task
Use ValueTask para caminhos quentes que frequentemente completam de forma síncrona. Para I/O real, use apenas Task.
// FAÇA: ValueTask para caminhos em cache/síncronos
public ValueTask<User?> GetUserAsync(UserId id)
{
if (_cache.TryGetValue(id, out var user))
{
return ValueTask.FromResult<User?>(user); // Sem alocação
}
return new ValueTask<User?>(FetchUserAsync(id));
}
// FAÇA: Task para I/O real (mais simples, sem armadilhas)
public Task<Order> CreateOrderAsync(CreateOrderCommand cmd)
{
// Isso sempre acessa o banco de dados
return _repository.CreateAsync(cmd);
}
Regras do ValueTask:
- Nunca aguarde um ValueTask mais de uma vez
- Nunca use
.Resultou.GetAwaiter().GetResult()antes da conclusão - Em caso de dúvida, use Task
Span e Memory para Bytes
Use Span<T> e Memory<T> em vez de byte[] para operações de baixo nível.
// FAÇA: Aceite Span para operações síncronas
public static int ParseInt(ReadOnlySpan<char> text)
{
return int.Parse(text);
}
// FAÇA: Aceite Memory para operações assíncronas
public async Task WriteAsync(ReadOnlyMemory<byte> data)
{
await _stream.WriteAsync(data);
}
// NÃO FAÇA: Force alocação de array
public static int ParseInt(string text) // String alocada
{
return int.Parse(text);
}
Padrões Comuns com Span
// Fatie sem alocação
ReadOnlySpan<char> span = "Hello, World!".AsSpan();
var hello = span[..5]; // Sem alocação
// Alocação na pilha para buffers pequenos
Span<byte> buffer = stackalloc byte[256];
// Use ArrayPool para buffers maiores
var buffer = ArrayPool<byte>.Shared.Rent(4096);
try
{
// Use o buffer...
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
Tipos de Retorno de Coleções
Retorne Coleções Imutáveis de APIs
// FAÇA: Retorne coleção imutável
public IReadOnlyList<Order> GetOrders()
{
return _orders.ToList(); // O chamador não pode modificar o estado interno
}
// FAÇA: Use coleções congeladas para dados estáticos (.NET 8+)
private static readonly FrozenDictionary<string, Handler> _handlers =
new Dictionary<string, Handler>
{
["create"] = new CreateHandler(),
["update"] = new UpdateHandler(),
}.ToFrozenDictionary();
// NÃO FAÇA: Retorne coleção mutável
public List<Order> GetOrders()
{
return _orders; // O chamador pode modificar!
}
Mutação Interna é Aceitável
public IReadOnlyList<OrderItem> BuildOrderItems(Cart cart)
{
var items = new List<OrderItem>(); // Mutável internamente
foreach (var cartItem in cart.Items)
{
items.Add(CreateOrderItem(cartItem));
}
return items; // Retorne como IReadOnlyList
}
Diretrizes para Coleções
| Cenário | Tipo de Retorno |
|---|---|
| Limite de API | IReadOnlyList<T>, IReadOnlyCollection<T> |
| Dados de consulta estática | FrozenDictionary<K,V>, FrozenSet<T> |
| Construção interna | List<T>, depois retorne como readonly |
| Item único ou nenhum | T? (nullable) |
| Zero ou mais, lazy | IEnumerable<T> |
Referência Rápida
| Padrão | Benefício |
|---|---|
sealed class | Desvirtualização, API clara |
readonly record struct | Sem cópias defensivas, semântica de valor |
| Funções estáticas puras | Sem vtable, testável, thread-safe |
Adiar .ToList() | Materialização única |
ValueTask para caminhos quentes | Evitar alocação de Task |
Span<T> para bytes | Alocação na pilha, sem cópia |
Retorno IReadOnlyList<T> | Contrato imutável da API |
FrozenDictionary | Consulta mais rápida para dados estáticos |
Anti-Padrões
// NÃO FAÇA: Classe sem selar sem motivo
public class OrderService { } // Sele-a!
// NÃO FAÇA: Struct mutável
public struct Point { public int X; public int Y; } // Torne readonly
// NÃO FAÇA: Método de instância que poderia ser estático
public int Add(int a, int b) => a + b; // Torne estático
// NÃO FAÇA: Múltiplas chamadas ToList()
items.Where(...).ToList().OrderBy(...).ToList(); // Um ToList no final
// NÃO FAÇA: Retornar List<T> de uma API pública
public List<Order> GetOrders(); // Retorne IReadOnlyList<T>
// NÃO FAÇA: ValueTask para operações sempre assíncronas
public ValueTask<Order> CreateOrderAsync(); // Apenas use Task
Recursos
- Melhores Práticas de Performance: https://learn.microsoft.com/pt-br/dotnet/standard/performance/
- Diretrizes de Span: https://learn.microsoft.com/pt-br/dotnet/standard/memory-and-spans/
- Coleções Congeladas: https://learn.microsoft.com/pt-br/dotnet/api/system.collections.frozen