Menos código, mais resultado: 10 técnicas em C# para código mais rápido e elegante

O C# tem evoluído rapidamente nos últimos anos, introduzindo recursos que tornam o código mais limpo, performático e expressivo. Veja estas 10 dicas práticas baseadas nas versões mais recentes da linguagem, com exemplos comparativos que mostram como escrever um código mais limpo ou modernizar seu código.

1. Switch expressions vs dicionários: quando usar cada um

❌ Abordagem tradicional

public static string GetDayNameOldWay(int day)
{
    switch (day)
    {
        case 1:
            return "Monday";
        case 2:
            return "Tuesday";
        case 3:
            return "Wednesday";
        case 4:
            return "Thursday";
        case 5:
            return "Friday";
        case 6:
            return "Saturday";
        case 7:
            return "Sunday";
        default:
            return "Invalid day";
    }
}

✅ Switch expression moderno

public static string GetDayNameSwitch(int day)
{
    return day switch
    {
        1 => "Monday",
        2 => "Tuesday",
        3 => "Wednesday",
        4 => "Thursday",
        5 => "Friday",
        6 => "Saturday",
        7 => "Sunday",
        _ => "Invalid day"
    };
}

✅ Alternativa com dicionário

private static readonly Dictionary<int, string> DayNames = new()
{
    { 1, "Monday" },
    { 2, "Tuesday" },
    { 3, "Wednesday" },
    { 4, "Thursday" },
    { 5, "Friday" },
    { 6, "Saturday" },
    { 7, "Sunday" }
};

public static string GetDayNameDictionary(int day)
{
    return DayNames.TryGetValue(day, out var name) ? name : "Invalid day";
}

💡 Dica Prática: Use switch expressions para lógica simples e imutável. Prefira dicionários quando os dados podem mudar em runtime ou quando há muitas opções (>10).

2. Method groups: simplifique suas lambdas

❌ Lambda desnecessária

var evenNumbers = numbers.Where(x => IsEven(x));
var upperCaseWords = words.Select(w => w.ToUpper());

✅ Method group (mais limpo)

var evenNumbers = numbers.Where(IsEven);
var upperCaseWords = words.Select(string.ToUpper);

Sempre que o tipo de retorno e os parâmetros do método batem com o predicado da expressão lambda, você pode passar somente o nome do método, como nos exemplos acima.

💡 Dica Prática: Method groups são mais eficientes em termos de alocação de memória e tornam o código mais legível. Use sempre que a lambda apenas chama um método existente.

3. Collection expressions: a nova sintaxe do C# 12

❌ Inicialização tradicional

List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
int[] array = new int[] { 1, 2, 3, 4, 5 };
string[] words = new string[] { "apple", "banana", "cherry" };

// Verificação de nulo verbosa
List<int> safeList = maybeNull != null ? maybeNull : new List<int>();

✅ Collection Expressions (C# 12)

List<int> numbers = [1, 2, 3, 4, 5];
int[] array = [1, 2, 3, 4, 5];
string[] words = ["apple", "banana", "cherry"];

// Coalesce com collection expression
var safeList = maybeNull ?? [];

// Funciona com tuplas também
(int, string)[] tuples = [(1, "one"), (2, "two"), (3, "three")];

💡 Dica Prática: Collection expressions reduzem boilerplate (código padrão e repetitivo) e funcionam muito bem entre diferentes tipos de coleção. Especialmente úteis com o operador ??.

4. Simplifique o new: elimine redundâncias

❌ Redundância de tipo

DateTime date1 = new DateTime(2030, 1, 30);
Dictionary<string, List<int>> complexDict = new Dictionary<string, List<int>>();

✅ Expressões new com tipo de destino (target-typed new expressions)

DateTime date1 = new(2030, 1, 30);
Dictionary<string, List<int>> complexDict = new();

Neste exemplo, a expressão new obtém seu tipo a partir do contexto em que é usada, eliminando a necessidade de repetir o tipo explicitamente ao instanciar objetos quando o tipo já está claro pela declaração da variável.

💡 Dica Prática: Funciona melhor com tipos complexos e genéricos. Cuidado: não funciona com var pois o compilador não consegue inferir o tipo. Baseado no exemplo acima:

var date1 = new(2030, 1, 30); // não funciona, o compilador não sabe qual é o tipo

5. Construtores primários: reduza boilerplate

Este recusto do C# permite declarar parâmetros diretamente na definição de uma classe ou struct, simplificando a criação e inicialização de objetos. Diferentemente das versões anteriores, onde o construtor precisava ser declarado dentro do corpo da classe, agora é possível definir esses parâmetros logo na assinatura da classe, o que torna o código mais enxuto e legível.

❌ Constructor tradicional

public class Client
{
    public string Name { get; }
    public int Age { get; }

    public Client(string name, int age)
    {
        Name = name;
        Age = age;
    }
}

✅ Construtor primário (C# 12)

public class Person(string Name, int Age)
{
    public string Name { get; } = Name;
    public int Age { get; } = Age;
}

// Ainda mais conciso com records
public record PersonRecord(string Name, int Age);

💡 Dica Prática: Primary constructors são ideais para DTOs e classes simples. Para lógica complexa no construtor, ainda prefira o construtor tradicional.

6. Pattern matching: simplifique condições complexas

❌ Verificações verbosas

Neste exemplo temos várias condições dentro do mesmo if e verificação de tipo, algo muito comum do dia a dia:

// Verificação do estado do objeto
if (order != null && order.Items > 2 && order.TotalValue > 50)
{
    Console.WriteLine("Order qualifies for discount.");
}

// Verificação de tipo tradicional
if (obj is string)
{
    string str = (string)obj;
    if (str.Length > 5)
    {
        Console.WriteLine($"Long string: {str}");
    }
}

✅ Pattern matching moderno

// Property patterns
if (order is { Items: > 2, TotalValue: > 50 })
{
    Console.WriteLine("Order qualifies for discount.");
}

// Pattern matching com declaração
if (obj is string { Length: > 5 } str)
{
    Console.WriteLine($"Long string: {str}");
}

// Switch expressions com patterns
string result = order switch
{
    { Items: > 10, TotalValue: > 100 } => "Premium discount",
    { Items: > 5, TotalValue: > 50 } => "Standard discount",
    { Items: > 2 } => "Small discount",
    _ => "No discount"
};

💡 Dica Prática: Pattern matching reduz significativamente a complexidade do código e o torna mais legível. Use especialmente para validações complexas (com mais de uma condição, como no exemplo).

7. With expressions: simplifique tipos imutáveis

Ideal para clonar objetos facilmente, aproveitando os valores de uma ou mais propriedades de um objeto existente.

❌ Clonagem manual

public class PersonOld
{
    public string Name { get; set; }
    public int Age { get; set; }
    
    public PersonOld Clone()
    {
        return new PersonOld 
        { 
            Name = this.Name, 
            Age = this.Age 
        };
    }
}

// Uso
var person1 = new PersonOld { Name = "Bob", Age = 25 };
var person2 = person1.Clone();
person2.Age = 26;

✅ With expressions

public record Person(string Name, int Age);

// Com records
Person person1 = new("Bob", 25);
Person person2 = person1 with { Age = 26 }; // Name = "Bob", Age = 26

// Funciona também com structs
public struct Point
{
    public int X { get; init; }
    public int Y { get; init; }
}

var point1 = new Point { X = 10, Y = 20 };
var point2 = point1 with { Y = 30 }; // X = 10, Y = 30

💡 Dica Prática: With expressions promovem imutabilidade e são thread-safe por design. Ideais para estado da aplicação e DTOs.

8. Otimização de performance: evite Any() desnecessário

Você pode encontrar essa sugestão no VS Code ou Visual Studio, de acordo com as regras de performance CA1860 (Avoid using ‘Enumerable.Any()’ extension method) e CA1827 (Do not use Count()/LongCount() when Any() can be used).

❌ Uso Ineficiente do Any()

// Regra CA1860 - ineficiente
bool hasElements = collection.Any();
bool hasItems = list.Any();
bool hasChars = text.Any();

✅ Use propriedades específicas

// Mais eficiente
bool hasElements = collection.Count > 0;
bool hasItems = list.Count > 0;  // ou list.Length para arrays
bool hasChars = text.Length > 0;

// Para IEnumerable sem Count, Any() ainda é necessário
bool hasItems = someEnumerable.Any();

💡 Dica Prática: Any() força enumeração completa, ou seja, percorrer a coleção inteira. Use Count ou Length quando disponível. Esta otimização pode ter impacto significativo em loops ou operações frequentes.

9. Null coalescing: trate nulos elegantemente

❌ Verificações verbosas

// Operador ternário verboso
int result = nullable.HasValue ? nullable.Value : defaultValue;

// Verificação de nulo tradicional
var person = GetPerson();
if (person == null)
    throw new InvalidOperationException("Person not found");

✅ Operadores de Coalescência

// Null coalescing operator
int result = nullable ?? defaultValue;

// Null coalescing assignment (C# 8+)
someVariable ??= GetDefaultValue();

// Throw expression
var person = GetPerson() ?? throw new InvalidOperationException("Person not found");

// Null propagation
string upperName = person?.Name?.ToUpper();

💡 Dica Prática: Combine ?? , ??= e ?. para código defensivo conciso. Especialmente útil em construtores e validações de entrada.

10. Serialização polimórfica: tratamento de JSON moderno

Suponha que você queira gravar eventos como um novo cadastro, ou uma notificação que precisa ser enviada. Esse eventos podem ser enviados via mensageria ou gravados num banco de dados. Aqui temos dois eventos específicos LoginEvent and ErrorEvent.

Aqui a propriedade EventType é usada como discrimidador para identificar o tipo do evento em tempo de execução.

❌ Serialização manual com discriminadores

public class Event
{
    public string EventType { get; set; }
    public DateTime Timestamp { get; set; }

    public Event()
    {
        EventType = GetType().Name.Replace("Event", "").ToLower();
    }
}

public class LoginEvent : Event
{
    public string User { get; set; }
}

public class ErrorEvent : Event
{
   public string ErrorMessage { get; set; }
}

// Serialização complexa
string json = JsonSerializer.Serialize((object)eventMessage, options);

// Deserialização com switch manual
Event deserialized = JsonSerializer.Deserialize<Event>(json);
switch (deserialized?.EventType)
{
    case "login":
        return JsonSerializer.Deserialize<LoginEvent>(json);
    case "error":
        return JsonSerializer.Deserialize<ErrorEvent>(json);
    // ...
}

✅ Serialização polimórfica com JsonPolymorphic (C# 11+)

A serialização polimórfica em C# com System.Text.Json permite tratar hierarquias de classes complexas durante a conversão para JSON. Assim, é possível saber qual tipo de classe derivada deve ser usada durante a serialização e desserialização, usando os atributos JsonPolymorphic e JsonDerivedType.

JsonPolymorphic define a nome da propriedade “virtual” que vai existir somente no JSON. JsonDerivedType indica quais valores essa propriedade vai ter para cada subclasse ao serializar e desserializar os objetos.

O objeto desserializado vai ser automaticamente criado com o tipo correto em tempo de execução (polimorfismo). Daí o nome serialização polimórfica.

[JsonPolymorphic(TypeDiscriminatorPropertyName = "$baseType")]
[JsonDerivedType(typeof(LoginEvent), "login")]
[JsonDerivedType(typeof(ErrorEvent), "error")]
public class Event
{
    public DateTime Timestamp { get; set; }
}

public class LoginEvent : Event
{
    public string User { get; set; }
}

// Serialização automática
string json = JsonSerializer.Serialize(eventMessage, options);

// Deserialização automática
Event deserialized = JsonSerializer.Deserialize<Event>(json);

// Pattern matching direto
string message = deserialized switch
{
    LoginEvent login => $"User {login.User} logged in",
    ErrorEvent error => $"Error: {error.ErrorMessage}",
    _ => "Unknown event"
};

💡 Dica Prática: JsonPolymorphic elimina boilerplate e reduz erros. O discriminador é automaticamente gerenciado pelo System.Text.Json.

Essas 10 dicas representam a evolução do C# moderno, focando em:

  • Expressividade: Código mais limpo e legível
  • Performance: Otimizações automáticas e manuais
  • Segurança: Null safety e pattern matching
  • Produtividade: Menos boilerplate, mais funcionalidade

Checklist de Implementação

✔️ Migre switch statements para switch expressions

✔️ Substitua lambdas simples por method groups

✔️ Adote collection expressions

✔️ Use target-typed new para reduzir redundância

✔️ Implemente primary constructors em DTOs

✔️Aplique pattern matching em validações complexas

✔️ Prefira with expressions para imutabilidade

✔️ Otimize verificações de coleção

✔️ Standardize tratamento de null com coalescing

✔️Modernize serialização com JsonPolymorphic

Lembre-se: A migração deve ser gradual. Implemente essas práticas em código novo e refatore código legado conforme necessário.


Este artigo foi baseado em exemplos práticos do repositório CSharpSnippets. Para mais exemplos e código completo, consulte o repositório original.

Deixe um comentário

Este site utiliza o Akismet para reduzir spam. Saiba como seus dados em comentários são processados.