Skip to content

C# Folha de referência

Linguagem moderna orientada a objetos para .NET, web e games.

01

Introdução

Hello World

Programas em C# começam em Main(). using System; importa o namespace System (Console, Math, etc.). C# 9+ suporta top-level statements: um arquivo com apenas Console.WriteLine("Hello"); é um programa válido. O compilador gera a classe e Main automaticamente.

csharp
using System;

class Program {
    static void Main() {
        Console.WriteLine("Hello, World!");
    }
}

Variáveis & Tipos

C# é estaticamente tipado. var permite que o compilador infira o tipo (ainda type-safe em tempo de compilação). decimal (sufixo m) é para cálculos financeiros com precisão exata. const é uma constante em tempo de compilação; readonly é uma constante em tempo de runtime definida no construtor.

csharp
int age = 30;
double pi = 3.14159;
decimal price = 19.99m;
char grade = 'A';
bool isDev = true;
string name = "Alice";
var x = 42;  // implicitly typed (int)
const double TAX = 0.08;

Interpolação de Strings

Interpolação de strings ($"...") incorpora expressões em chaves. Especificadores de formato após : controlam a saída (F2 para 2 decimais, yyyy-MM-dd para datas). Verbatim strings (@"") tratam barras invertidas literalmente—úteis para caminhos de arquivo e regex. Combine ambos: $@"...".

csharp
string name = "Alice";
int age = 30;
Console.WriteLine($"Name: {name}, Age: {age}");

// Formatting
double pi = 3.14159;
Console.WriteLine($"Pi: {pi:F2}");  // Pi: 3.14
Console.WriteLine($"Date: {DateTime.Now:yyyy-MM-dd}");

// Verbatim strings (@"") preserve backslashes
string path = @"C:\Users\name";

Entrada & Saída

Console.ReadLine() lê uma linha completa como uma string. int.Parse converte, mas lança exceção em entrada inválida; int.TryParse é mais seguro—retorna bool e usa um parâmetro out. Sempre use TryParse para entrada do usuário a fim de evitar exceções de dados inválidos.

csharp
Console.Write("Enter name: ");
string name = Console.ReadLine();
Console.Write("Enter age: ");
int age = int.Parse(Console.ReadLine());

Console.WriteLine($"Hi {name}, you are {age}");

// Safe parsing with TryParse
if (int.TryParse(Console.ReadLine(), out int num)) {
    Console.WriteLine($"Valid: {num}");
} else {
    Console.WriteLine("Invalid number");
}

Tipos Nullable

int? é um tipo de valor nullable (Nullable<int>). O operador ?. (null-conditional) acessa membros com segurança—retorna null em vez de lançar exceção. O operador ?? (null-coalescing) fornece um padrão para null. C# 8+ nullable reference types avisam sobre potenciais nulls em tempo de compilação.

csharp
int? maybeAge = null;  // nullable int
if (maybeAge.HasValue) {
    Console.WriteLine(maybeAge.Value);
}

string? maybeName = null;  // nullable reference (C# 8+)
int length = maybeName?.Length ?? 0;  // null-conditional + null-coalescing

// ValueOrDefault
int age = maybeAge ?? 0;  // 0 if null
02

Strings

Métodos de String

Strings em C# são imutáveis—métodos retornam novas strings em vez de modificar a original. Métodos comuns: Length, ToUpper/ToLower, Substring, IndexOf, Replace, Contains, Split. Para manipulação intensa de strings, use StringBuilder para evitar criar muitas strings intermediárias.

csharp
string s = "Hello, World!";
Console.WriteLine(s.Length);        // 13
Console.WriteLine(s.ToUpper());     // HELLO, WORLD!
Console.WriteLine(s.Substring(0, 5)); // Hello
Console.WriteLine(s.IndexOf("World")); // 7
Console.WriteLine(s.Replace("World", "C#"));
Console.WriteLine(s.Contains("Hello")); // True
Console.WriteLine(s.Split(", "));   // ["Hello", "World!"]

StringBuilder

StringBuilder é mutável e eficiente para construir strings em loops. Concatenação de strings (+) cria uma nova string a cada vez, então concatenação repetida é O(n^2). StringBuilder amortiza para O(n). Use StringBuilder quando você tem mais que algumas concatenações.

csharp
using System.Text;
var sb = new StringBuilder();
for (int i = 0; i < 100; i++) {
    sb.Append("Line ").Append(i).Append("\n");
}
string result = sb.ToString();

// vs string concatenation (creates many temporary strings)
// string result = "";
// for (int i = 0; i < 100; i++) result += "Line " + i;

Formatação & Parsing

string.Format usa placeholders {0}, {1}. Especificadores de formato: F (fixo), N (número com separadores), P (porcentagem), X (hex). Parsing é sensível à cultura—separadores decimais diferem por locale. Use CultureInfo para controle explícito, ou TryParse para segurança.

csharp
// String formatting
string s1 = string.Format("{0} is {1} years old", "Alice", 30);
string s2 = $"{3.14159:F2}";  // "3.14"
string s3 = $"{1234567:N0}";  // "1,234,567" (thousands separator)

// Parsing
int n = int.Parse("42");
double d = double.Parse("3.14");
bool b = bool.Parse("true");

// Culture-aware parsing
double euro = double.Parse("3,14",
    System.Globalization.CultureInfo.GetCultureInfo("de-DE"));

Comparação de Strings

Use StringComparison.OrdinalIgnoreCase para comparação case-insensitive—é mais claro e mais rápido que ToLower() e depois ==. Evite == para comparações sensíveis à cultura; use string.Compare com um CultureInfo. Para chaves de dicionário, use StringComparer.OrdinalIgnoreCase.

csharp
string a = "Hello", b = "hello";

// Case-sensitive
bool equal = a == b;  // false
bool equal2 = a.Equals(b);  // false

// Case-insensitive
bool equalCI = a.Equals(b, StringComparison.OrdinalIgnoreCase);  // true
int cmp = string.Compare(a, b, StringComparison.OrdinalIgnoreCase);

// Best practice for comparison
bool same = string.Equals(a, b,
    StringComparison.OrdinalIgnoreCase);

Expressões Regulares

Regex (de System.Text.RegularExpressions) fornece correspondência de padrões. Use @"" verbatim strings para que barras invertidas não precisem de double-escaping. Para uso repetido, compile uma instância Regex uma vez e reutilize-a (RegexOptions.Compiled) para melhor desempenho.

csharp
using System.Text.RegularExpressions;

string text = "Phone: 123-456-7890";
var match = Regex.Match(text, @"\d{3}-\d{3}-\d{4}");
if (match.Success) Console.WriteLine(match.Value);  // 123-456-7890

// Replace
string clean = Regex.Replace(text, @"\d", "X");
// "Phone: XXX-XXX-XXXX"

// Extract all matches
var emails = Regex.Matches("[email protected], [email protected]", @"\S+@\S+");
03

Números & Matemática

Tipos Numéricos

C# tem tipos de tamanho fixo: int (32-bit), long (64-bit), double (64-bit float), decimal (128-bit, para financeiro). Underscores (9_000_000) melhoram a legibilidade. Use decimal para dinheiro—evita erros de arredondamento de ponto flutuante. Sufixos: L (long), f (float), m (decimal).

csharp
int i = 42;
long big = 9_000_000_000L;  // underscores for readability
double d = 3.14159265;
float f = 3.14f;
decimal price = 19.99m;     // exact decimal for money
byte b = 255;

Console.WriteLine(sizeof(int));   // 4
Console.WriteLine(int.MaxValue);  // 2147483647

Classe Math

Math fornece métodos estáticos para operações comuns. Math.Round usa banker's rounding (arredonda para par) por padrão—use MidpointRounding.AwayFromZero para arredondamento escolar. Math.BigMul lida com multiplicação long para evitar overflow.

csharp
double x = 2.5;
Math.Pow(x, 3);     // 15.625
Math.Sqrt(x);       // 1.581
Math.Abs(-5);       // 5
Math.Floor(3.7);    // 3
Math.Ceiling(3.2);  // 4
Math.Round(3.5);    // 4 (banker's rounding)
Math.Max(3, 7);     // 7
Math.Min(3, 7);     // 3
Math.PI;            // 3.14159...

Números Aleatórios

Random gera números pseudoaleatórios. Crie uma instância e reutilize-a (criar muitas em um loop pode produzir duplicatas devido à seeding baseada em tempo). Para aleatoriedade criptográfica, use System.Security.Cryptography.RandomNumberGenerator. C# 8+ fornece Random.Shared para thread safety.

csharp
var random = new Random();
int r = random.Next(1, 101);     // 1-100
double d = random.NextDouble();   // 0.0-1.0
byte[] bytes = new byte[4];
random.NextBytes(bytes);

// C# 8+: thread-safe
// var rng = Random.Shared;
// int n = rng.Next(1, 101);

// Array of random items
int[] nums = Enumerable.Range(0, 5)
    .Select(_ => random.Next(1, 100)).ToArray();

Conversão de Tipos

Conversões implícitas acontecem automaticamente quando nenhum dado é perdido (int para double). Casts explícitos (type) são necessários quando a precisão pode ser perdida. Convert.ToInt32 lida com muitos tipos e arredonda (diferente do cast que trunca). Sempre prefira TryParse para conversão de string para número a fim de evitar exceções.

csharp
// Implicit (safe, no data loss)
int i = 42;
double d = i;  // int -> double

// Explicit cast (may lose data)
double pi = 3.14;
int truncated = (int)pi;  // 3

// Convert class
string s = "42";
int n = Convert.ToInt32(s);
string str = Convert.ToString(42);

// Parse / TryParse
int parsed = int.Parse("100");
bool ok = int.TryParse("abc", out int result);  // false

Overflow de Inteiros & Checked

Por padrão, overflow de inteiros faz wrap silenciosamente (unchecked). O bloco checked lança OverflowException em overflow—use-o para código crítico de segurança. Para números verdadeiramente grandes, BigInteger (System.Numerics) lida com inteiros de precisão arbitrária sem overflow.

csharp
int a = int.MaxValue;  // 2147483647
// a++;  // overflow: wraps to -2147483648 (unchecked by default)

checked {
    a++;  // throws OverflowException!
}

// BigInteger for arbitrary precision
using System.Numerics;
var big = BigInteger.Pow(2, 100);
Console.WriteLine(big);  // 1267650...376 (31 digits)
04

Fluxo de Controle

If / Else

if/else if/else é padrão. O operador ternário (condition ? a : b) é uma expressão if/else concisa. C# exige condições booleanas—diferente de C, inteiros não são implicitamente convertidos para bool. Use chaves mesmo para instruções únicas para evitar bugs de manutenção.

csharp
int score = 85;
if (score >= 90) {
    Console.WriteLine("A");
} else if (score >= 80) {
    Console.WriteLine("B");
} else {
    Console.WriteLine("C");
}

// Ternary operator
string grade = score >= 60 ? "Pass" : "Fail";

Switch & Pattern Matching

Switch expressions do C# 8+ (=>) são concisas e retornam valores. O padrão _ é o case default. Pattern matching (is, switch) suporta type patterns, property patterns e relational patterns (> 18). Isso é mais poderoso que switch tradicional e reduz boilerplate.

csharp
// Classic switch
int day = 3;
string name = day switch {
    1 => "Mon", 2 => "Tue", 3 => "Wed",
    6 or 7 => "Weekend",
    _ => "Invalid"  // default
};

// Pattern matching with types
object obj = "hello";
if (obj is string s && s.Length > 3) {
    Console.WriteLine(s);  // "hello"
}

// Property patterns
if (person is { Age: > 18, Name: "Alice" }) {
    // matches if Age > 18 and Name == "Alice"
}

Loops

for é para iterações contadas; foreach itera coleções (arrays, listas, IEnumerable); while repete até uma condição ser false. foreach é somente leitura—você não pode modificar a coleção durante a iteração. Use for se precisar do índice ou modificar elementos.

csharp
// for loop
for (int i = 0; i < 5; i++) {
    Console.WriteLine(i);
}

// foreach
string[] fruits = { "apple", "banana", "cherry" };
foreach (string fruit in fruits) {
    Console.WriteLine(fruit);
}

// while
int n = 5;
while (n > 0) Console.WriteLine(n--);

Break, Continue & goto

break sai do loop ou switch mais próximo; continue pula para a próxima iteração. C# não tem labeled break para loops aninhados—use uma flag, extraia para um método com return, ou use LINQ. goto é válido em switch para fall-through, mas é, de resto, desencorajado.

csharp
for (int i = 0; i < 10; i++) {
    if (i == 3) continue;  // skip 3
    if (i == 7) break;     // stop at 7
    Console.WriteLine(i);  // 0 1 2 4 5 6
}

// goto in switch (rare but valid)
switch (code) {
    case 1:
        DoSomething();
        goto case 2;  // fall through
    case 2:
        DoMore();
        break;
}

Iterators & yield

yield return transforma um método em um iterator que produz valores de forma preguiçosa—um de cada vez conforme solicitado. Isso é eficiente em memória para sequências grandes ou infinitas. O compilador gera uma state machine. yield break encerra a iteração antecipadamente. LINQ usa iterators extensivamente.

csharp
// yield return creates an iterator (lazy evaluation)
IEnumerable<int> GetNumbers() {
    for (int i = 0; i < 5; i++) {
        yield return i;
    }
}

foreach (int n in GetNumbers()) {
    Console.WriteLine(n);  // 0 1 2 3 4
}

// Infinite sequence (consumed lazily)
IEnumerable<int> Naturals() {
    int n = 0;
    while (true) yield return n++;
}
05

Métodos & Delegates

Definição de Método

Métodos podem ser expression-bodied (=>) para one-liners. Parâmetros padrão tornam argumentos opcionais. Argumentos nomeados (name:) melhoram a legibilidade para chamadas com muitos parâmetros e permitem pular opcionais. C# não suporta sobrecarga de métodos apenas com tipos de retorno diferentes.

csharp
static int Add(int a, int b) {
    return a + b;
}

// Expression-bodied (C# 6+)
static int Multiply(int a, int b) => a * b;

// Default parameters
static void Greet(string name = "Guest", int times = 1) {
    for (int i = 0; i < times; i++)
        Console.WriteLine($"Hi {name}!");
}

Greet();              // Hi Guest!
Greet("Alice", 3);    // Hi Alice! x3
Greet(times: 2);      // named argument

out, ref & params

Parâmetros out devem ser atribuídos pelo método (o chamador não precisa inicializar). Parâmetros ref devem ser inicializados pelo chamador e podem ser modificados. params permite um número variável de argumentos. C# 7+ permite declaração out var inline: TryParse(s, out var result).

csharp
// out: must be assigned in method (caller doesn't init)
bool TryParse(string s, out int result) {
    result = 0;
    return int.TryParse(s, out result);
}

// ref: caller must initialize
void Increment(ref int n) { n++; }
int x = 5;
Increment(ref x);  // x is now 6

// params: variable arguments
int Sum(params int[] nums) => nums.Sum();
Sum(1, 2, 3);  // 6
Sum();         // 0

Delegates & Events

Delegates são ponteiros de função type-safe. Func<T,TResult> recebe entradas e retorna um valor; Action<T> retorna void; Predicate<T> retorna bool. Events são delegates com inscrição +=/-= e ?.Invoke para disparo seguro (null-check). Use events para o padrão observer (pub/sub).

csharp
// Delegate type
delegate int MathOp(int a, int b);

MathOp add = (a, b) => a + b;
MathOp mul = (a, b) => a * b;
Console.WriteLine(add(3, 4));  // 7

// Built-in delegate types
Func<int, int, int> divide = (a, b) => a / b;
Action<string> log = msg => Console.WriteLine(msg);
Predicate<int> isEven = n => n % 2 == 0;

// Events
event Action<string> OnMessage;
OnMessage += msg => Console.WriteLine($"Got: {msg}");
OnMessage?.Invoke("Hello");

Expressões Lambda

Lambdas são métodos anônimos usando =>. Eles capturam variáveis do escopo delimitador (closures). Variáveis capturadas são avaliadas quando o lambda executa, não quando é criado—cuidado com isso em loops. Use lambdas extensivamente com LINQ, events e callbacks.

csharp
// Lambda with explicit types
Func<int, int> square = (int x) => x * x;

// Type inference
Func<int, int, int> add = (a, b) => a + b;

// Statement lambda
Action<string> log = msg => {
    Console.WriteLine($"[{DateTime.Now}] {msg}");
};

// Capturing variables (closures)
int factor = 3;
Func<int, int> multiply = x => x * factor;
Console.WriteLine(multiply(5));  // 15

Extension Methods

Extension methods adicionam métodos a tipos existentes sem modificá-los. A palavra-chave this antes do primeiro parâmetro o marca como extension method. Devem estar em uma static class. LINQ é inteiramente implementado como extension methods em IEnumerable. Essa é uma maneira poderosa de adicionar métodos utilitários.

csharp
static class StringExtensions {
    public static int WordCount(this string s) {
        return s.Split(new[] { ' ' },
            StringSplitOptions.RemoveEmptyEntries).Length;
    }
}

string text = "Hello World C#";
Console.WriteLine(text.WordCount());  // 3

// LINQ is built on extension methods
var evens = nums.Where(n => n % 2 == 0);
06

Coleções

List<T>

List<T> é um array dinâmico (como vector em C++ ou ArrayList em Java). Add/Remove são O(1) amortizado / O(n). Count fornece o número de elementos (não a capacidade). Find/FindAll usam predicados. Para inserções/exclusões frequentes no meio, LinkedList<T> pode ser melhor.

csharp
using System.Collections.Generic;
var nums = new List<int> { 1, 2, 3 };
nums.Add(4);
nums.AddRange(new[] { 5, 6 });
nums.Remove(3);
nums.RemoveAt(0);
Console.WriteLine(nums.Count);  // 4
Console.WriteLine(nums.Contains(4));  // true

// Find
int first = nums.Find(n => n > 3);
List<int> all = nums.FindAll(n => n > 3);

Dictionary<TKey, TValue>

Dictionary é uma hash table com lookup O(1) médio. TryGetValue é mais seguro que o indexer (que lança KeyNotFoundException). Para chaves de string case-insensitive, passe StringComparer.OrdinalIgnoreCase para o construtor. Dictionary não preserva a ordem de inserção (use OrderedDictionary se necessário).

csharp
var ages = new Dictionary<string, int> {
    ["Alice"] = 30,
    ["Bob"] = 25
};
ages["Charlie"] = 35;

// Safe access
if (ages.TryGetValue("Alice", out int age)) {
    Console.WriteLine($"Alice: {age}");
}

// Iterate
foreach (var kv in ages) {
    Console.WriteLine($"{kv.Key}: {kv.Value}");
}

// Case-insensitive keys
var dict = new Dictionary<string, int>(
    StringComparer.OrdinalIgnoreCase);

HashSet<T> & SortedSet<T>

HashSet<T> armazena elementos únicos com lookup O(1)—use para deduplicação e teste de pertencimento. Suporta operações de conjunto (Union, Intersect, Except). SortedSet<T> (red-black tree) mantém elementos ordenados com operações O(log n). Use HashSet quando a ordem não importa, SortedSet quando importa.

csharp
var set = new HashSet<int> { 1, 2, 3 };
set.Add(2);  // duplicate, ignored
set.Add(4);
Console.WriteLine(set.Count);  // 4
Console.WriteLine(set.Contains(3));  // true

// Set operations
var other = new HashSet<int> { 3, 4, 5 };
set.IntersectWith(other);  // set: {3, 4}
set.UnionWith(other);      // set: {3, 4, 5}

// SortedSet keeps elements sorted
var sorted = new SortedSet<int> { 3, 1, 2 };  // {1, 2, 3}

Queue<T> & Stack<T>

Queue<T> é FIFO (Enqueue/Dequeue)—use para agendamento de tarefas, BFS. Stack<T> é LIFO (Push/Pop)—use para undo/redo, DFS, avaliação de expressões. Ambos são O(1) para suas operações principais. Para cenários concorrentes, use ConcurrentQueue e ConcurrentStack de System.Collections.Concurrent.

csharp
// Queue: FIFO (first in, first out)
var queue = new Queue<string>();
queue.Enqueue("first");
queue.Enqueue("second");
string next = queue.Dequeue();  // "first"
string peek = queue.Peek();     // "second"

// Stack: LIFO (last in, first out)
var stack = new Stack<int>();
stack.Push(1);
stack.Push(2);
int top = stack.Pop();   // 2
int peek = stack.Peek(); // 1

Arrays

Arrays são de tamanho fixo. Array.Sort ordena in-place. Arrays multidimensionais ([,]) são retangulares (colunas uniformes). Jagged arrays ([][]) são arrays de arrays (linhas podem ter comprimentos diferentes). Use List<T> para dimensionamento dinâmico; use arrays para dados de tamanho fixo e críticos de desempenho.

csharp
int[] nums = { 1, 2, 3, 4, 5 };
Console.WriteLine(nums.Length);  // 5
Array.Sort(nums);
Array.Reverse(nums);
int found = Array.IndexOf(nums, 3);
int[] copy = new int[5];
Array.Copy(nums, copy, 5);

// Multidimensional
int[,] grid = { { 1, 2 }, { 3, 4 } };
Console.WriteLine(grid[1, 0]);  // 3

// Jagged array (array of arrays)
int[][] jagged = { new[] { 1, 2 }, new[] { 3, 4, 5 } };
07

Classes & OOP

Classe & Propriedades

Auto-properties ({ get; set; }) geram backing fields automaticamente. Expression-bodied properties (=>) computam no acesso. Propriedades init-only do C# 9+ ({ get; init; }) são configuráveis apenas durante a construção, permitindo imutabilidade. Object initializers: new Person { Name = "X" }.

csharp
class Person {
    // Auto-properties (C# 3+)
    public string Name { get; set; }
    public int Age { get; set; }

    // Read-only property
    public bool IsAdult => Age >= 18;

    // Constructor
    public Person(string name, int age) {
        Name = name;
        Age = age;
    }
}

var p = new Person("Alice", 30);
Console.WriteLine(p.IsAdult);  // true

Herança & Virtual

virtual marca um método para overriding; override em subclasses fornece a nova implementação. C# exige override explícito (diferente de Java). sealed impede que uma classe seja herdada ou que um método seja sobrescrito. Todas as classes implicitamente herdam de object (System.Object).

csharp
class Animal {
    public virtual void Speak() {
        Console.WriteLine("...");
    }
}

class Dog : Animal {
    public override void Speak() {
        Console.WriteLine("Woof");
    }
}

Animal a = new Dog();
a.Speak();  // Woof (polymorphism)

// sealed prevents further inheritance
sealed class Puppy : Dog { }

Interfaces

Interfaces definem contratos sem implementação. Uma classe pode implementar múltiplas interfaces (diferente da herança única de classe). C# 8+ permite default interface methods. Use interfaces para polimorfismo e injeção de dependência. Convenção de nomenclatura: prefixe com I (IShape, ILogger).

csharp
interface IShape {
    double Area();  // interface member
    double Perimeter { get; }
}

class Circle : IShape {
    public double Radius { get; set; }
    public double Area() => Math.PI * Radius * Radius;
    public double Perimeter => 2 * Math.PI * Radius;
}

// Default interface methods (C# 8+)
interface ILogger {
    void Log(string msg);
    void Error(string msg) => Log($"ERROR: {msg}");  // default
}

Classes Abstratas

Classes abstratas não podem ser instanciadas e podem ter tanto membros abstratos (devem sobrescrever) quanto concretos (virtuais). Use classes abstratas quando há implementação compartilhada; use interfaces para contratos puros. Uma classe pode herdar apenas uma classe abstrata, mas implementar muitas interfaces.

csharp
abstract class Shape {
    public abstract double Area();  // must be overridden
    public virtual void Describe() {
        Console.WriteLine($"Area: {Area()}");
    }
}

class Rectangle : Shape {
    public double Width { get; set; }
    public double Height { get; set; }
    public override double Area() => Width * Height;
}

// Cannot instantiate abstract class
// var s = new Shape();  // ERROR
var r = new Rectangle { Width = 3, Height = 4 };

Records & Structs

Records (C# 9+) fornecem igualdade baseada em valor, imutabilidade e sintaxe concisa—ideais para DTOs e modelos de dados. A expressão with cria uma cópia com propriedades modificadas. Structs são tipos de valor (copiados na atribuição, alocados na stack)—use para dados pequenos e leves. Classes são tipos de referência (alocados no heap).

csharp
// Record (C# 9+): value-based equality, immutability
record Point(int X, int Y);

var p1 = new Point(3, 4);
var p2 = new Point(3, 4);
Console.WriteLine(p1 == p2);  // true (value equality)

// with expression (non-destructive mutation)
var p3 = p1 with { X = 10 };

// Struct: value type (stack-allocated)
struct Vector {
    public double X, Y;
    public Vector(double x, double y) { X = x; Y = y; }
}
08

LINQ

Where & Select

LINQ (Language Integrated Query) fornece consultas semelhantes a SQL em coleções. Where filtra, Select transforma (map). LINQ é preguiçoso—consultas executam apenas quando enumeradas (ex.: via ToList()). Isso permite encadeamento eficiente sem coleções intermediárias. Sintaxe de método (acima) é mais comum; sintaxe de consulta também está disponível.

csharp
using System.Linq;
int[] nums = { 1, 2, 3, 4, 5, 6 };

// Filter
var evens = nums.Where(n => n % 2 == 0);  // 2, 4, 6

// Map/Transform
var squares = nums.Select(n => n * n);  // 1, 4, 9, 16, 25, 36

// Chain operations
var result = nums
    .Where(n => n > 2)
    .Select(n => n * 10)
    .ToList();  // 30, 40, 50, 60

Ordenação & Agrupamento

OrderBy/OrderByDescending ordenam; ThenBy adiciona critérios de ordenação secundária. GroupBy agrupa elementos por uma chave, retornando grupos IGrouping<key, element>. Cada grupo tem uma Key e é ele próprio um IEnumerable de seus membros. Isso substitui loops manuais e dicionários para agrupamento.

csharp
var people = new[] {
    new { Name = "Alice", Age = 30 },
    new { Name = "Bob", Age = 25 },
    new { Name = "Charlie", Age = 30 },
};

// Order
var sorted = people.OrderBy(p => p.Age).ThenBy(p => p.Name);
var desc = people.OrderByDescending(p => p.Age);

// Group
var grouped = people.GroupBy(p => p.Age);
foreach (var g in grouped) {
    Console.WriteLine($"Age {g.Key}: {g.Count()} people");
}
// Age 25: 1 people
// Age 30: 2 people

Agregação

Métodos de agregação do LINQ (Sum, Average, Min, Max, Count) computam valores únicos a partir de coleções. Aggregate é o mais geral—é um fold/reduce que aplica uma função cumulativamente. Esses lançam em sequências vazias; use as variantes *OrDefault ou verifique a vacuidade primeiro.

csharp
int[] nums = { 1, 2, 3, 4, 5 };

Console.WriteLine(nums.Sum());    // 15
Console.WriteLine(nums.Average()); // 3
Console.WriteLine(nums.Min());    // 1
Console.WriteLine(nums.Max());    // 5
Console.WriteLine(nums.Count());  // 5

// Count with predicate
int evens = nums.Count(n => n % 2 == 0);  // 2

// Aggregate (fold/reduce)
int product = nums.Aggregate((a, b) => a * b);  // 120
string joined = nums.Aggregate("", (s, n) => s + n);  // "12345"

First, Single & ElementAt

First retorna o primeiro elemento (lança se vazio); FirstOrDefault retorna default (0 para int, null para tipos de referência) se não encontrado. Single exige exatamente um elemento (lança caso contrário)—use para validação. Any/All retornam bool sem enumerar a coleção inteira (short-circuit).

csharp
int[] nums = { 1, 2, 3, 4, 5 };

int first = nums.First();           // 1
int firstEven = nums.First(n => n % 2 == 0);  // 2
int firstOrDef = nums.FirstOrDefault(n => n > 10);  // 0 (default)

int single = new[] { 42 }.Single();  // 42
// nums.Single();  // throws! more than one element

int third = nums.ElementAt(2);  // 3 (0-indexed)

bool any = nums.Any(n => n > 3);  // true
bool all = nums.All(n => n > 0);  // true

Join & Zip

Join executa um inner join (como SQL) correspondendo elementos por chave. Zip pareia elementos de duas sequências por posição. Ambos produzem novas sequências. LINQ também suporta GroupJoin (left join com agrupamento). Esses são poderosos para combinar dados relacionados de múltiplas fontes.

csharp
var users = new[] {
    new { Id = 1, Name = "Alice" },
    new { Id = 2, Name = "Bob" },
};
var orders = new[] {
    new { UserId = 1, Item = "Book" },
    new { Id = 2, Item = "Pen" },
};

// Join (inner join)
var joined = users.Join(orders,
    u => u.Id, o => o.UserId,
    (u, o) => new { u.Name, o.Item });
// { Alice, Book }

// Zip (pair elements by position)
var names = new[] { "Alice", "Bob" };
var ages = new[] { 30, 25 };
var pairs = names.Zip(ages, (n, a) => $"{n}: {a}");
// "Alice: 30", "Bob: 25"
09

Tratamento de Erros

Try / Catch / Finally

Capture exceções específicas primeiro, depois a Exception geral por último (mais específica para menos específica). finally sempre executa—use-o para limpeza (fechar arquivos, liberar recursos). Evite catch (Exception) para fluxo de controle; capture apenas o que você pode tratar. Use throw; (não throw e;) para preservar o stack trace.

csharp
try {
    int x = 10;
    int y = 0;
    int z = x / y;  // throws DivideByZeroException
} catch (DivideByZeroException e) {
    Console.WriteLine($"Error: {e.Message}");
} catch (Exception e) {
    Console.WriteLine($"Unexpected: {e.Message}");
} finally {
    // always runs, even with return/throw
    Console.WriteLine("Cleanup");
}

Exceções Personalizadas

Derive exceções personalizadas de Exception (ou uma base mais específica). Adicione campos de contexto que ajudam na depuração. Sempre chame o construtor base com a mensagem. Por convenção, nomes de classes de exceção terminam com Exception. Use exceções personalizadas para condições de erro específicas de domínio que chamadores podem tratar.

csharp
class ValidationError : Exception {
    public string Field { get; }
    public ValidationError(string field, string msg)
        : base(msg) {
        Field = field;
    }
}

throw new ValidationError("email", "Invalid format");

try {
    // ...
} catch (ValidationError e) {
    Console.WriteLine($"{e.Field}: {e.Message}");
}

using & IDisposable

using garante que Dispose() seja chamado mesmo se uma exceção ocorrer—esse é o equivalente em C# do RAII para gerenciamento de recursos. A declaração using (C# 8+) é mais limpa—Dispose é chamado no final do escopo. Implemente IDisposable para classes que mantêm recursos não gerenciados (file handles, conexões de banco de dados).

csharp
// using statement (C# 1+)
using (var file = new StreamReader("data.txt")) {
    string content = file.ReadToEnd();
}  // file.Dispose() called automatically

// using declaration (C# 8+)
using var file2 = new StreamReader("data.txt");
string content2 = file2.ReadToEnd();
// Dispose called at end of scope

// Implementing IDisposable
class Resource : IDisposable {
    public void Dispose() {
        // release unmanaged resources
        GC.SuppressFinalize(this);
    }
}

Tratamento de Null

C# 8+ nullable reference types (string?) habilitam segurança de null em tempo de compilação. ?. retorna null em vez de lançar NullReferenceException. ?? fornece um fallback. A expressão throw (?? throw) é concisa para validação. Habilite <Nullable>enable</Nullable> no .csproj para verificação de null em todo o projeto.

csharp
string? name = null;

// Null-conditional operator
int? length = name?.Length;  // null (no exception)

// Null-coalescing operator
string display = name ?? "Unknown";  // "Unknown"

// Throw if null
string required = name ?? throw new ArgumentNullException(nameof(name));

// Pattern matching null check
if (name is null) { /* ... */ }
if (name is { Length: > 5 }) { /* not null and length > 5 */ }

Filtros de Exceção

Filtros de exceção (when) adicionam condições a blocos catch—a exceção só é capturada se o filtro for true. Isso é mais poderoso que capturar e relançar porque preserva o stack trace. O padrão de logging (when retorna false) permite observar exceções sem tratá-las.

csharp
try {
    // ...
} catch (HttpRequestException e) when (e.StatusCode == 404) {
    Console.WriteLine("Not found");
} catch (HttpRequestException e) when (e.StatusCode >= 500) {
    Console.WriteLine("Server error");
} catch (HttpRequestException e) {
    Console.WriteLine($"HTTP error: {e.Message}");
}

// Log without catching
catch (Exception e) when (LogError(e)) {
    // never enters if LogError returns false
}
private bool LogError(Exception e) { /* log */ return false; }
10

File I/O & Async

Classe File (I/O Rápido)

File fornece métodos estáticos para operações rápidas de arquivo únicas—simples, mas carrega o arquivo inteiro na memória. Para arquivos grandes, use StreamReader/StreamWriter para processar linha por linha. File.Exists verifica a existência, mas há uma race condition TOCTOU—sempre trate exceções de operações de arquivo.

csharp
using System.IO;

// Read all text at once
string content = File.ReadAllText("input.txt");
string[] lines = File.ReadAllLines("input.txt");

// Write
File.WriteAllText("output.txt", "Hello\n");
File.WriteAllLines("output.txt", new[] { "Line 1", "Line 2" });

// Append
File.AppendAllText("log.txt", $"[{DateTime.Now}] Event\n");

// File info
bool exists = File.Exists("data.txt");
var info = new FileInfo("data.txt");
Console.WriteLine(info.Length);  // bytes

Streams (StreamReader/Writer)

StreamReader/StreamWriter processam arquivos linha por linha sem carregar tudo na memória—use para arquivos grandes. A instrução using garante que o arquivo seja fechado mesmo se uma exceção ocorrer. StreamWriter faz buffer de dados; chame Flush() para escrever imediatamente, ou deixe Dispose lidar com isso.

csharp
using System.IO;

// Reading line by line
using var reader = new StreamReader("input.txt");
string? line;
while ((line = reader.ReadLine()) != null) {
    Console.WriteLine(line);
}

// Writing
using var writer = new StreamWriter("output.txt");
writer.WriteLine("First line");
writer.WriteLine("Second line");
writer.Flush();  // optional, using disposes and flushes

async / await

async/await permite operções assíncronas não bloqueantes. O método retorna Task<T> (ou Task para void). await suspende o método sem bloquear a thread—a thread é liberada para outro trabalho. Isso é essencial para operações I/O-bound (arquivos, rede, banco de dados) em aplicações UI e web.

csharp
async Task<string> ReadFileAsync(string path) {
    using var reader = new StreamReader(path);
    return await reader.ReadToEndAsync();
}

// Calling async method
string content = await ReadFileAsync("data.txt");
Console.WriteLine(content.Length);

// Async doesn't block the calling thread
// The thread is freed while waiting for I/O

Task & Parallel

Task.Run agenda trabalho no thread pool. Task.WhenAll aguarda múltiplas tarefas concorrentemente (não use async/await para trabalho CPU-bound—use Task.Run). Parallel.For é para loops paralelos CPU-bound. PLINQ (AsParallel) paraleliza consultas LINQ. Use async para I/O, Parallel/PLINQ para CPU.

csharp
// Run work on a thread pool thread
var task = Task.Run(() => HeavyComputation());
int result = await task;

// Run multiple tasks concurrently
var t1 = Task.Run(() => DownloadAsync("url1"));
var t2 = Task.Run(() => DownloadAsync("url2"));
await Task.WhenAll(t1, t2);  // wait for both

// Parallel.For for CPU-bound parallelism
Parallel.For(0, 100, i => {
    Process(i);  // runs on multiple threads
});

// PLINQ
var results = nums.AsParallel()
    .Where(n => IsPrime(n))
    .ToArray();

Serialização JSON

System.Text.Json é o serializador JSON moderno e de alto desempenho (substitui Newtonsoft.Json para a maioria dos casos). Serialize/Deserialize lidam com object<->JSON. Use JsonSerializerOptions para formatação (indented, camelCase). Para async, use os métodos baseados em stream para evitar carregar JSON grande na memória.

csharp
using System.Text.Json;

var person = new { Name = "Alice", Age = 30 };

// Serialize
string json = JsonSerializer.Serialize(person);
// {"Name":"Alice","Age":30}

// Deserialize
Person p = JsonSerializer.Deserialize<Person>(json);

// With options
var options = new JsonSerializerOptions {
    WriteIndented = true,
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
string pretty = JsonSerializer.Serialize(person, options);

// Async (for streams)
using var stream = File.OpenRead("data.json");
var data = await JsonSerializer.DeserializeAsync<Person>(stream);
11

LINQ Aprofundado

Sintaxe de Consulta vs Método

LINQ tem duas sintaxes equivalentes. Sintaxe de consulta é semelhante a SQL e mais legível para joins/grouping complexos. Sintaxe de método (fluent) é mais comum, suporta todos os operadores e encadeia naturalmente. Ambas compilam para o mesmo IL. Use sintaxe de consulta para consultas complexas com múltiplas cláusulas; sintaxe de método para encadeamentos simples. Ambas são avaliadas preguiçosamente (execução diferida).

csharp
List<int> nums = new() { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

// Query syntax (SQL-like)
var evens = from n in nums
            where n % 2 == 0
            orderby n descending
            select n * 2;

// Method syntax (fluent) — equivalent
var evens2 = nums.Where(n => n % 2 == 0)
                  .OrderByDescending(n => n)
                  .Select(n => n * 2);

// Query syntax supports: from, where, orderby, select, group, join, let
// Method syntax supports everything query does, plus more

Execução Diferida vs Imediata

LINQ usa execução diferida—Where/Select/OrderBy apenas constroem uma consulta; ela executa quando enumerada. Isso significa que os resultados refletem o estado da fonte no momento da enumeração, não no momento da criação da consulta. ToList/ToArray/Count/etc. forçam execução imediata, capturando um snapshot. Cuidado: enumerar uma consulta diferida duas vezes a executa duas vezes. Faça cache com ToList se precisar de resultados estáveis.

csharp
var nums = new List<int> { 1, 2, 3 };

// Deferred: query not run until enumerated
var query = nums.Where(n => n > 1);
nums.Add(4);  // query sees this!
Console.WriteLine(query.Count());  // 3 (2, 3, 4)

// Immediate: forces execution now
var list = nums.Where(n => n > 1).ToList();  // runs now
nums.Add(5);
Console.WriteLine(list.Count);  // still 3 (snapshot)

// Operators that force immediate execution:
// ToList, ToArray, ToDictionary, ToLookup,
// Count, Sum, Min, Max, Average, First, Last, Any, All

Agrupamento e Junção

GroupBy agrupa elementos por uma chave, retornando sequências IGrouping<key, element>. Join executa inner joins (correspondência de chaves). Group join (join...into) cria resultados hierárquicos e permite left outer joins via DefaultIfEmpty(). Essas operações são poderosas para análise de dados. Para grandes conjuntos de dados, considere ToLookup para lookups repetidos (é um agrupamento pré-computado).

csharp
var students = new[] {
    new { Name = "Alice", Grade = "A", Dept = "CS" },
    new { Name = "Bob", Grade = "B", Dept = "Math" },
    new { Name = "Carol", Grade = "A", Dept = "CS" },
};

// Group by department
var byDept = students.GroupBy(s => s.Dept);
foreach (var g in byDept) {
    Console.WriteLine(`{g.Key}: {g.Count()}`);
    // CS: 2, Math: 1
}

// Inner join
var courses = new[] { new { Dept = "CS", Course = "Algo" } };
var joined = from s in students
             join c in courses on s.Dept equals c.Dept
             select new { s.Name, c.Course };

// Group join (left outer join with DefaultIfEmpty)
var groupJoin = from s in students
                join c in courses on s.Dept equals c.Dept into sc
                from c in sc.DefaultIfEmpty()
                select new { s.Name, Course = c?.Course };

Agregação e Quantificadores

LINQ fornece agregações padrão (Sum, Min, Max, Average, Count) e um Aggregate genérico para reduções personalizadas (como reduce do JavaScript). Quantificadores (Any, All, Contains) retornam booleanos e fazem short-circuit—Any para na primeira correspondência, All para no primeiro não correspondente. Use Any() (não Count() > 0) para verificar existência—é mais eficiente e legível.

csharp
var nums = new[] { 1, 2, 3, 4, 5 };

// Aggregations
int sum = nums.Sum();              // 15
double avg = nums.Average();       // 3.0
int min = nums.Min();              // 1
int max = nums.Max();              // 5
int count = nums.Count();          // 5

// Custom aggregation (like reduce/fold)
int product = nums.Aggregate((a, b) => a * b);  // 120
int withSeed = nums.Aggregate(10, (a, b) => a + b);  // 25

// Quantifiers (return bool)
bool anyEven = nums.Any(n => n % 2 == 0);  // true
bool allPos = nums.All(n => n > 0);        // true
bool contains3 = nums.Contains(3);         // true

// LongCount for > int.MaxValue elements
long big = largeList.LongCount();

IEnumerable e IQueryable

IEnumerable<T> é para coleções em memória (LINQ to Objects)—usa delegates, executa localmente. IQueryable<T> é para fontes remotas (Entity Framework, LINQ to SQL)—usa expression trees, traduz para a linguagem de consulta da fonte (SQL). IQueryable compõe consultas do lado do servidor de forma eficiente. Mude para IEnumerable com AsEnumerable() quando precisar de lógica do lado do cliente que não pode ser traduzida.

csharp
// IEnumerable<T>: in-memory, LINQ to Objects
List<int> nums = new() { 1, 2, 3 };
var q1 = nums.Where(n => n > 1);  // executes in memory

// IQueryable<T>: translates to query language (SQL)
// Expression trees, not delegates
using var db = new MyDbContext();
var q2 = db.Users.Where(u => u.Age > 18);  // generates SQL
// SELECT * FROM Users WHERE Age > 18

// IQueryable composes queries that execute server-side
var query = db.Users
    .Where(u => u.Active)
    .OrderBy(u => u.Name)
    .Take(10);
// All translated to one SQL query

// Force client-side with AsEnumerable()
var clientSide = db.Users.AsEnumerable()
    .Where(u => ExpensiveCheck(u));
12

Async/Await Aprofundado

Task e Task<T>

Task representa uma operação async sem resultado; Task<T> retorna T. ValueTask<T> (C# 7) evita alocação no heap quando o resultado está frequentemente disponível sincronamente (cenários de cache)—mas só pode ser awaited uma vez. Use Task.FromResult para resultados síncronos em APIs async. Prefira Task a void para métodos async (void é apenas para event handlers).

csharp
// Task: represents an async operation (no result)
async Task DoWorkAsync() {
    await Task.Delay(1000);
    Console.WriteLine("Done");
}

// Task<T>: async operation returning T
async Task<int> GetCountAsync() {
    await Task.Delay(500);
    return 42;
}

// ValueTask<T>: lightweight, avoids allocation
ValueTask<int> GetCachedAsync() {
    if (_cache != null) return new ValueTask<int>(_cache);
    return new ValueTask<int>(LoadFromDbAsync());
}

// Converting to Task
Task t = Task.Run(() => Console.WriteLine("hi"));
Task<int> t2 = Task.FromResult(42);  // already complete
Task completed = Task.CompletedTask;

CancellationToken

CancellationToken permite cancelamento cooperativo. Passe-o para métodos async e verifique ThrowIfCancellationRequested() em loops. O chamador cancela via CancellationTokenSource.Cancel(); o método decide quando/como responder. Sempre aceite um parâmetro CancellationToken em APIs async (especialmente código de biblioteca). Use CancellationTokenSource com um TimeSpan para timeouts. Cancelamento lança OperationCanceledException.

csharp
async Task DownloadAsync(string url, CancellationToken ct) {
    using var client = new HttpClient();
    // Pass token to async operations
    var data = await client.GetByteArrayAsync(url, ct);
    // Check manually in loops
    foreach (var chunk in data) {
        ct.ThrowIfCancellationRequested();
        Process(chunk);
    }
}

// Usage with timeout
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
try {
    await DownloadAsync("https://example.com", cts.Token);
} catch (OperationCanceledException) {
    Console.WriteLine("Timed out or cancelled");
}

// Cooperative cancellation: caller cancels, method checks
cts.Cancel();  // request cancellation

Task.WhenAll vs WhenAny

Task.WhenAll aguarda múltiplas tarefas concorrentemente e continua quando todas completam (paralelismo). Task.WhenAny continua quando a primeira tarefa completa (racing/redundância). WhenAll lança apenas a primeira exceção por padrão; inspecione IsFaulted/Exception de cada tarefa para ver todas as falhas. Para fire-and-forget, use Task.Run e trate exceções internamente para evitar exceções não observadas.

csharp
// WhenAll: wait for ALL to complete (like Promise.all)
var tasks = new[] {
    FetchAsync("url1"),
    FetchAsync("url2"),
    FetchAsync("url3"),
};
string[] results = await Task.WhenAll(tasks);
// Continues when all 3 done; throws AggregateException if any fails

// WhenAny: wait for FIRST to complete (race)
var racing = new[] { PrimaryAsync(), FallbackAsync() };
Task<string> firstDone = await Task.WhenAny(racing);
string result = await firstDone;

// WhenAll with exception handling
try {
    await Task.WhenAll(tasks);
} catch (Exception ex) {
    // Only first exception is thrown; access all via Task.Exception
    foreach (var inner in tasks.Where(t => t.IsFaulted))
        Console.WriteLine(inner.Exception);
}

Async Streams (IAsyncEnumerable)

IAsyncEnumerable<T> (C# 8) é um stream async—produz itens conforme ficam disponíveis, com awaits entre produções. Consumido com await foreach. Perfeito para streaming de grandes conjuntos de dados ou dados em tempo real sem armazenar tudo em buffer na memória. O atributo [EnumeratorCancellation] garante que o token de cancelamento flua corretamente quando o stream é consumido com WithCancellation.

csharp
// C# 8: async iterator (like async generator)
async IAsyncEnumerable<int> GenerateAsync(
    [EnumeratorCancellation] CancellationToken ct = default)
{
    for (int i = 0; i < 10; i++) {
        await Task.Delay(100, ct);
        yield return i;  // async yield
    }
}

// Consume with await foreach
await foreach (int n in GenerateAsync().WithCancellation(ct)) {
    Console.WriteLine(n);
}

// Use case: streaming data from a database or API
async IAsyncEnumerable<Record> StreamRecordsAsync() {
    await using var reader = await cmd.ExecuteReaderAsync();
    while (await reader.ReadAsync())
        yield return MapRecord(reader);
}

Armadilhas Async (Deadlocks)

A armadilha #1 do async: chamar .Result ou .Wait() em uma Task pode causar deadlock quando um SynchronizationContext está presente (apps UI, ASP.NET legado). O await captura o contexto; .Result bloqueia a thread que a continuação precisa. Correção: torne os métodos async até o topo, ou use ConfigureAwait(false) em código de biblioteca. async void é perigoso—exceções não podem ser capturadas e não é awaitable. Use async Task em vez disso.

csharp
// BAD: .Result blocks (can deadlock in UI/ASP.NET classic)
public string BadMethod() {
    string data = GetDataAsync().Result;  // DEADLOCK risk!
    return data;
}

// GOOD: async all the way
public async Task<string> GoodMethodAsync() {
    string data = await GetDataAsync();
    return data;
}

// Why deadlock: in UI/ASP.NET context, await captures
// SynchronizationContext. .Result blocks the thread,
// but the continuation needs that same thread → deadlock.

// Fix if you MUST block (last resort):
string data = Task.Run(GetDataAsync).Result;  // offloads
// Or ConfigureAwait(false) in library code:
await GetDataAsync().ConfigureAwait(false);

// async void is EVIL (except event handlers)
async void Bad() { await Task.Delay(1); throw new Exception(); }
// Exception can't be caught by caller!
13

Events & Delegates

Básico de Delegate

Delegates são ponteiros de função type-safe. Custom delegates (delegate int MathOp(int,int)) são amplamente substituídos por Action integrado (retorno void) e Func<T> (retorna T). Delegates são multicast—podem armazenar múltiplos métodos (combinados com +). Quando invocados, todos os métodos executam. O valor de retorno de um delegate multicast é o resultado do último método. Use Action/Func para a maioria do código moderno.

csharp
// Declare a delegate type
public delegate int MathOp(int a, int b);

// Methods matching the signature
static int Add(int a, int b) => a + b;
static int Mul(int a, int b) => a * b;

MathOp op = Add;
Console.WriteLine(op(2, 3));  // 5
op = Mul;
Console.WriteLine(op(2, 3));  // 6

// Multicast: combine delegates
MathOp multi = (MathOp)Add + Mul;
// Note: returns last result (Add's), but both run
// (return value of multicast is the last one's)

// Built-in: Action (void), Func<T> (returns T)
Action<string> log = s => Console.WriteLine(s);
Func<int, int, int> add = (a, b) => a + b;
Predicate<int> isEven = n => n % 2 == 0;  // returns bool

Events (Publisher/Subscriber)

Events são uma forma restrita de delegates—apenas a classe declarante pode invocá-los, mas código externo pode inscrever/cancelar inscrição (+=/-=). Use EventHandler<TArgs> para o padrão padrão. Sempre use o operador null-conditional (Click?.Invoke) já que um event sem subscribers é null. Sempre cancele a inscrição para prevenir vazamentos de memória (o event mantém uma referência forte ao handler).

csharp
public class Button {
    // Event: restricted multicast delegate
    public event EventHandler<EventArgs> Click;

    public void SimulateClick() {
        // Null-conditional invoke pattern (thread-safe)
        Click?.Invoke(this, EventArgs.Empty);
    }
}

// Custom event args
public class SearchEventArgs : EventArgs {
    public string Query { get; init; }
}

// Subscribe
var btn = new Button();
btn.Click += (sender, e) => Console.WriteLine("Clicked!");
btn.Click += HandlerMethod;  // method group

// Unsubscribe (MUST to avoid memory leaks)
btn.Click -= HandlerMethod;

void HandlerMethod(object sender, EventArgs e) { /* ... */ }

INotifyPropertyChanged (Data Binding)

INotifyPropertyChanged é o padrão padrão para data binding em WPF, MAUI e WinForms. Quando uma propriedade muda, dispare PropertyChanged para que a UI vinculada seja atualizada. [CallerMemberName] (C# 5) passa automaticamente o nome da propriedade chamadora—sem magic strings. A verificação (if != value) previne notificações desnecessárias. ViewModels usam isso intensivamente; source generators (CommunityToolkit.Mvvm) podem auto-gerar esse boilerplate.

csharp
using System.ComponentModel;
using System.Runtime.CompilerServices;

public class ViewModel : INotifyPropertyChanged {
    private string _name;
    public string Name {
        get => _name;
        set {
            if (_name != value) {
                _name = value;
                OnPropertyChanged();  // raises event
            }
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    // CallerMemberName auto-fills the property name
    protected void OnPropertyChanged([CallerMemberName] string prop = null) {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(prop));
    }
}

// WPF/MAUI bindings automatically update UI when Name changes

Event vs Delegate

A diferença principal: um campo delegate público pode ser invocado, reatribuído ou limpo por qualquer um. Um event restringe o acesso externo apenas a += (inscrever) e -= (cancelar inscrição)—apenas a classe declarante pode invocá-lo ou limpá-lo. Esse encapsulamento é por que events são o padrão para o padrão observer. Events também geram accessores add/remove thread-safe e se integram com ferramentas de designer.

csharp
// Public delegate: anyone can invoke or clear
public delegate void Notify();
public class Bad {
    public Notify OnDone;  // field-like, public
}
// External code can do:
//   bad.OnDone();        // invoke (BAD: not the owner)
//   bad.OnDone = null;   // clear all subscribers (BAD)

// Event: only owner can invoke/clear
public class Good {
    public event Notify OnDone;  // restricted
    public void Complete() {
        OnDone?.Invoke();  // OK: owner invokes
    }
}
// External code can ONLY += or -= (subscribe/unsubscribe)
// Cannot invoke or clear from outside

// Event = delegate + access restriction
// Under the hood: event generates add/remove accessors

Weak Events (Prevenção de Vazamento de Memória)

Events mantêm referências fortes a subscribers, causando vazamentos de memória se o subscriber deveria ser coletado pelo garbage collector, mas o publisher continua vivo. Soluções: cancele a inscrição explicitamente (melhor), use WeakEventManager (WPF), ou implemente um padrão de weak event com WeakReference. Essa é uma fonte comum de vazamentos em apps de longa duração (servidores, apps desktop). Sempre cancele a inscrição em Dispose ou quando o tempo de vida do subscriber terminar.

csharp
// Problem: event holds strong ref to subscriber → leak
// if subscriber should be GC'd but publisher lives long

// Solution 1: WeakEventManager (WPF)
WeakEventManager<Publisher, EventArgs>.AddHandler(pub, "Event", Handler);

// Solution 2: WeakReference in custom pattern
public class WeakEvent<TArgs> where TArgs : EventArgs {
    private List<WeakReference<EventHandler<TArgs>>> _handlers = new();
    public void Subscribe(EventHandler<TArgs> h) =>
        _handlers.Add(new WeakReference<EventHandler<TArgs>>(h));
    public void Raise(object sender, TArgs args) {
        _handlers.RemoveAll(wr => !wr.TryGetTarget(out _));
        foreach (var wr in _handlers) {
            if (wr.TryGetTarget(out var h)) h(sender, args);
        }
    }
}

// Best practice: unsubscribe (-=) when done, or use
// IDisposable pattern with weak references for long-lived publishers
14

Reflection

Informações de Tipo

Reflection inspeciona metadados de tipo em tempo de execução. typeof(T) obtém um Type em tempo de compilação; obj.GetType() obtém em tempo de execução. Type fornece Name, IsClass, IsValueType e métodos para enumerar membros (GetMethods, GetProperties, GetFields). BindingFlags controlam o que é retornado (NonPublic, Instance, Static, Public). Reflection é poderoso, mas lento—armazene resultados em cache quando possível.

csharp
using System;

Type t = typeof(string);
// Or: Type t = "hello".GetType();

Console.WriteLine(t.Name);          // String
Console.WriteLine(t.FullName);      // System.String
Console.WriteLine(t.IsClass);       // true
Console.WriteLine(t.IsValueType);   // false
Console.WriteLine(t.IsAbstract);    // false

// Get methods
foreach (var m in t.GetMethods())
    Console.WriteLine(m.Name);

// Get properties
var props = t.GetProperties();

// Get fields
var fields = t.GetFields(BindingFlags.NonPublic | BindingFlags.Instance);

// Check if assignable
Type ienum = typeof(IEnumerable<int>);
Console.WriteLine(ienum.IsAssignableFrom(t));  // false

Instanciação e Invocação

Activator.CreateInstance cria objetos dinamicamente. MethodInfo.Invoke chama métodos via reflection (lento devido a boxing de argumentos e verificações de segurança). Para chamadas repetidas, CreateDelegate é muito mais rápido—cria um delegate fortemente tipado que ignora a sobrecarga de reflection. Reflection quebra a segurança de tipo em tempo de compilação e é mais lento que chamadas diretas; use-o para frameworks (serialização, DI, ORMs) e cenários de cache.

csharp
using System.Reflection;

// Create instance
Type t = typeof(StringBuilder);
object obj = Activator.CreateInstance(t);
// Or with args: Activator.CreateInstance(t, "initial")

// Invoke method
MethodInfo method = t.GetMethod("Append", new[] { typeof(string) });
object result = method.Invoke(obj, new object[] { "hello" });

// Get/set property
PropertyInfo prop = t.GetProperty("Length");
int len = (int)prop.GetValue(obj);

// Get/set field
FieldInfo field = t.GetField("_field", BindingFlags.NonPublic | BindingFlags.Instance);
field.SetValue(obj, 42);

// Create delegate from MethodInfo (faster than Invoke)
var appendDel = (Func<StringBuilder, string, StringBuilder>)
    method.CreateDelegate(typeof(Func<StringBuilder, string, StringBuilder>));

Attributes

Attributes anexam metadados a elementos de código (classes, métodos, propriedades). Defina com [AttributeUsage(...)] especificando alvos válidos. Recupere via GetCustomAttributes<T>() (genérico, moderno) ou Attribute.GetCustomAttribute. Attributes integrados: [Obsolete], [Serializable], [DllImport], [Conditional]. Frameworks usam attributes extensivamente: [HttpGet] (ASP.NET), [JsonProperty] (JSON), [Table] (EF Core).

csharp
// Define an attribute
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
public class AuthorAttribute : Attribute {
    public string Name { get; }
    public AuthorAttribute(string name) => Name = name;
}

// Apply
[Author("Alice")]
[Author("Bob")]
public class Widget {
    [Author("Charlie")]
    public void DoWork() { }
}

// Read via reflection
var attrs = typeof(Widget).GetCustomAttributes<AuthorAttribute>();
foreach (var a in attrs) Console.WriteLine(a.Name);  // Alice, Bob

// Check if attribute is applied
bool hasAuthor = Attribute.IsDefined(typeof(Widget), typeof(AuthorAttribute));

Emit e Geração Dinâmica de Código

Reflection.Emit gera IL em tempo de execução para máximo desempenho em cenários dinâmicos (compilação de expressões, serializadores, mock frameworks). Você constrói tipos/métodos e emite opcodes IL diretamente. Isso é avançado—use expression trees (Expression.Compile) para código dinâmico mais simples. Emit é usado por DLR (linguagens dinâmicas), ORMs e bibliotecas de serialização para gerar código tipado rápido em tempo de execução.

csharp
using System.Reflection;
using System.Reflection.Emit;

// Create a dynamic assembly/module/type
var asmName = new AssemblyName("Dynamic");
var asm = AssemblyBuilder.DefineDynamicAssembly(asmName, AssemblyBuilderAccess.Run);
var mod = asm.DefineDynamicModule("Main");
var type = mod.DefineType("Calculator", TypeAttributes.Public);

// Add a method
var method = type.DefineMethod("Add",
    MethodAttributes.Public | MethodAttributes.Static,
    typeof(int), new[] { typeof(int), typeof(int) });

// Emit IL
var il = method.GetILGenerator();
il.Emit(OpCodes.Ldarg_0);  // load first arg
il.Emit(OpCodes.Ldarg_1);  // load second arg
il.Emit(OpCodes.Add);      // add
il.Emit(OpCodes.Ret);      // return

Type created = type.CreateType();
int result = (int)created.GetMethod("Add").Invoke(null, new object[] { 3, 4 });  // 7

Expression Trees

Expression trees representam código como dados (uma árvore de nós Expression). São a base do IQueryable (provedores LINQ os traduzem para SQL, etc.). Compile() transforma uma expressão em um delegate executável. Você pode construir expressões manualmente para geração dinâmica de código (mais legível que Reflection.Emit). Expression trees permitem ORMs, rule engines e LINQ dinâmico.

csharp
using System.Linq.Expressions;

// Expression<Func<T>>: code as data (tree), not executable
Expression<Func<int, int, int>> expr = (a, b) => a + b + 1;

// Inspect the tree
Console.WriteLine(expr.Body.NodeType);  // Add
Console.WriteLine(expr.Body.Type);      // int

// Compile to a delegate (fast, runs in memory)
Func<int, int, int> compiled = expr.Compile();
int result = compiled(2, 3);  // 6

// EF Core translates expressions to SQL
Expression<Func<User, bool>> filter = u => u.Age > 18;
// → WHERE Age > 18

// Build expressions manually (advanced)
ParameterExpression x = Expression.Parameter(typeof(int), "x");
Expression body = Expression.Multiply(x, Expression.Constant(2));
var lambda = Expression.Lambda<Func<int, int>>(body, x);
Func<int, int> doubler = lambda.Compile();
15

Extension Methods & Padrões

Extension Methods

Extension methods permitem 'adicionar' métodos a tipos existentes sem modificá-los. Definidos em uma static class com 'this' no primeiro parâmetro. São syntactic sugar—o compilador reescreve email.IsEmail() para StringExtensions.IsEmail(email). LINQ é inteiramente extension methods em IEnumerable. Use extensions para adicionar métodos utilitários a sealed classes, interfaces ou tipos de terceiros. Traga-os para o escopo com using.

csharp
public static class StringExtensions {
    // Must be static, in static class, first param with 'this'
    public static bool IsEmail(this string s) =>
        s.Contains('@') && s.Contains('.');

    public static string Repeat(this string s, int n) =>
        string.Concat(Enumerable.Repeat(s, n));

    // Chainable
    public static string Truncate(this string s, int max) =>
        s.Length <= max ? s : s[..max];
}

// Usage: appears as instance method
string email = "[email protected]";
bool valid = email.IsEmail();
string repeated = "ab".Repeat(3);  // "ababab"
string short_ = "hello world".Truncate(5);  // "hello"

// LINQ is built on extension methods (Enumerable class)
var evens = nums.Where(n => n % 2 == 0);  // extension

Fluent Interfaces

Fluent interfaces retornam 'this' para permitir method chaining, produzindo código legível que se lê como uma frase. Comum em builders (QueryBuilder), configuração (ASP.NET, EF) e DSLs. Extension methods podem adicionar métodos fluentes a tipos primitivos (5.Seconds()). Mantenha métodos puros (retorna nova instância) para APIs fluentes imutáveis, ou mute e retorne this para builders.

csharp
public class QueryBuilder {
    private string _table = "";
    private string _where = "";
    private int _limit;

    public QueryBuilder From(string table) { _table = table; return this; }
    public QueryBuilder Where(string clause) { _where = clause; return this; }
    public QueryBuilder Limit(int n) { _limit = n; return this; }

    public string Build() =>
        $"SELECT * FROM {_table} WHERE {_where} LIMIT {_limit}";
}

// Fluent chaining
var sql = new QueryBuilder()
    .From("users")
    .Where("age > 18")
    .Limit(10)
    .Build();

// Extension methods enable fluent APIs on existing types
public static class IntExtensions {
    public static TimeSpan Seconds(this int n) => TimeSpan.FromSeconds(n);
    public static TimeSpan Minutes(this int n) => TimeSpan.FromMinutes(n);
}
var delay = 5.Seconds();  // TimeSpan
var timeout = 2.Minutes();

Iterator Methods (yield)

yield return transforma um método em um iterator—o compilador gera uma state machine. A execução é preguiçosa: o código roda apenas conforme os itens são puxados. Isso permite sequências infinitas e streaming eficiente (processa um item por vez sem buffer). yield break encerra a iteração. O método deve retornar IEnumerable<T> ou IEnumerator<T>. Operadores LINQ são construídos sobre yield. Re-enumerar re-executa o método.

csharp
// yield return: lazy iterator (state machine generated)
public static IEnumerable<int> Evens(int max) {
    for (int i = 0; i <= max; i += 2)
        yield return i;
}

// Lazy: nothing runs until enumerated
var evens = Evens(100);  // no execution yet
foreach (var e in evens) { /* runs here */ }

// yield break: end iteration
public static IEnumerable<T> TakeWhile<T>(IEnumerable<T> src, Func<T, bool> pred) {
    foreach (var item in src) {
        if (!pred(item)) yield break;
        yield return item;
    }
}

// Infinite sequence (lazy, safe)
public static IEnumerable<int> Naturals() {
    int n = 0;
    while (true) yield return n++;
}
var first10 = Naturals().Take(10);  // 0..9

Injeção de Dependência

Injeção de Dependência (DI) passa dependências via construtores em vez de criá-las internamente. Benefícios: testabilidade (troca implementações em testes), baixo acoplamento, responsabilidade única. O container DI integrado do .NET (Microsoft.Extensions.DependencyInjection) lida com registro e tempo de vida (AddTransient, AddScoped, AddSingleton). Sempre dependa de interfaces, não classes concretas, para flexibilidade.

csharp
// Interface for the dependency
public interface IEmailSender {
    Task SendAsync(string to, string body);
}

// Implementation
public class SmtpEmailSender : IEmailSender {
    public async Task SendAsync(string to, string body) { /* SMTP */ }
}

// Consumer: depends on abstraction, not concretion
public class UserService {
    private readonly IEmailSender _email;
    public UserService(IEmailSender email) => _email = email;  // inject

    public async Task RegisterAsync(string email) {
        // ... save user ...
        await _email.SendAsync(email, "Welcome!");
    }
}

// Register in DI container (ASP.NET Core)
builder.Services.AddScoped<IEmailSender, SmtpEmailSender>();
builder.Services.AddScoped<UserService>();

// Resolve
var userService = app.Services.GetRequiredService<UserService>();

Padrões Factory e Builder

O padrão Factory centraliza a criação de objetos, escondendo classes concretas dos chamadores (útil quando a criação é complexa ou o tipo é escolhido em tempo de execução). O padrão Builder constrói objetos complexos passo a passo com métodos fluentes, evitando 'telescoping constructors' (muitos parâmetros). Ambos melhoram a legibilidade e a manutenibilidade. Records em C# com expressões 'with' frequentemente substituem builders para dados imutáveis.

csharp
// Factory: create objects without specifying exact class
public interface IShape { void Draw(); }
public class Circle : IShape { public void Draw() => Console.WriteLine("○"); }
public class Square : IShape { public void Draw() => Console.WriteLine("□"); }

public static class ShapeFactory {
    public static IShape Create(string type) => type switch {
        "circle" => new Circle(),
        "square" => new Square(),
        _ => throw new ArgumentException()
    };
}

// Builder: step-by-step construction of complex objects
public class Pizza {
    public string Size { get; set; }
    public bool Cheese { get; set; }
    public bool Pepperoni { get; set; }
}
public class PizzaBuilder {
    private readonly Pizza _p = new();
    public PizzaBuilder WithSize(string s) { _p.Size = s; return this; }
    public PizzaBuilder AddCheese() { _p.Cheese = true; return this; }
    public PizzaBuilder AddPepperoni() { _p.Pepperoni = true; return this; }
    public Pizza Build() => _p;
}

var pizza = new PizzaBuilder()
    .WithSize("large").AddCheese().AddPepperoni().Build();
16

Pattern Matching (C# 7-10+)

Type Patterns e is

Pattern matching (C# 7+) combina verificação de tipo e vinculação de variável em uma etapa: 'o is string s' verifica e atribui. Property patterns ({ Length: > 3 }) correspondem propriedades de objeto. C# 9 adiciona combinadores 'not', 'and', 'or'. Switch expressions (C# 8) retornam valores e usam padrões—muito mais limpo que cadeias if-else. O _ é o discard pattern (case default).

csharp
object o = "hello";

// Type pattern (C# 7)
if (o is string s) {
    Console.WriteLine(s.Length);  // s is string here
}

// Combined with null check
if (o is string { Length: > 3 } longStr) {
    Console.WriteLine(longStr);  // string with length > 3
}

// Negation and conjunction (C# 9)
if (o is not null) { /* ... */ }
if (o is string and not null) { /* ... */ }

// Switch on type
string Describe(object o) => o switch {
    int i => $"Integer: {i}",
    string s => $"String: {s}",
    double d => $"Double: {d}",
    null => "null",
    _ => "Unknown"
};

Property e Positional Patterns

Property patterns correspondem propriedades de objeto: { Prop: pattern }. Positional patterns desconstruem via o método Deconstruct (records têm isso automaticamente). Padrões compõem: você pode aninhá-los e combinar com operadores relacionais (<, >, etc.). C# 10 permite abreviação de propriedade aninhada (Name.Length em vez de Name: { Length: }). Pattern matching torna lógica condicional complexa declarativa e legível.

csharp
public record Point(int X, int Y);
public record Person(string Name, int Age);

// Property pattern
string Describe(Person p) => p switch {
    { Age: < 18 } => "Minor",
    { Age: >= 65 } => "Senior",
    { Name.Length: > 10 } => "Long name",
    { } => "Adult"  // non-null Person
};

// Positional pattern (deconstruction)
string Quadrant(Point p) => p switch {
    (0, 0) => "Origin",
    ( > 0, > 0) => "Q1",
    ( < 0, > 0) => "Q2",
    ( < 0, < 0) => "Q3",
    ( > 0, < 0) => "Q4",
    _ => "On axis"
};

// Nested patterns
bool IsValid(Person p) => p is { Name: { Length: > 0 }, Age: > 0 };
// C# 10: nested property shorthand
bool IsValid2(Person p) => p is { Name.Length: > 0, Age: > 0 };

Relational e Logical Patterns (C# 9)

C# 9 introduziu relational patterns (<, >, <=, >=) e combinadores lógicos (and, or, not) para padrões. Esses tornam switch expressions expressivos para verificações de intervalo e lógica de categoria. 'or' combina padrões (corresponde se qualquer um); 'and' exige ambos; 'not' nega. Parênteses agrupam para precedência. Isso substitui cadeias if-else verbosas por lógica declarativa e exaustiva (verificada pelo compilador).

csharp
// Relational patterns: <, >, <=, >=
string Classify(int n) => n switch {
    < 0 => "Negative",
    0 => "Zero",
    > 0 and < 10 => "Small positive",
    >= 10 and < 100 => "Medium",
    >= 100 => "Large"
};

// Logical patterns: and, or, not
bool IsWeekend(DayOfWeek d) => d is DayOfWeek.Saturday or DayOfWeek.Sunday;

// Combined with type patterns
decimal GetDiscount(object customer) => customer switch {
    Student => 0.2m,
    Senior or Veteran => 0.15m,
    not null => 0.0m,
    null => throw new ArgumentNullException()
};

// Parenthesized patterns for grouping
bool IsValid(int n) => n is (> 0 and < 10) or (> 100 and < 200);

List Patterns (C# 11)

List patterns (C# 11) correspondem à forma de arrays e listas: vazio [], elemento único [x], exato [a, b], ou com slices [first, .., last]. O padrão slice (..) captura zero ou mais elementos do meio em uma variável. Combinado com var e guards (when), isso permite correspondência expressiva em sequências. Útil para parsing, validação e implementação de algoritmos.

csharp
// List patterns match array/list shapes
int[] nums = { 1, 2, 3, 4, 5 };

string Describe(int[] arr) => arr switch {
    [] => "Empty",
    [single] => $"Single: {single}",
    [first, second] => $"Pair: {first}, {second}",
    [first, .., last] => $"First: {first}, Last: {last}",  // slice
    [.., var last] => $"Last: {last}",
    _ => "Other"
};

// Slice pattern (..) captures middle elements
if (nums is [var first, .. var middle, var last]) {
    Console.WriteLine(`First {first}, middle count {middle.Length}, last {last}`);
}

// With guards
string Classify(int[] arr) => arr switch {
    [var a, var b] when a == b => "Equal pair",
    [var a, var b] => "Unequal pair",
    _ => "Other"
};

Switch Expressions

Switch expressions (C# 8) são expressões que retornam valores, diferentemente de switch statements. Elas usam pattern matching (=>) e são mais concisas. Tuple switching ((a, b) switch) lida com múltiplos valores. O compilador verifica a exaustividade para enums (avisa se um case está faltando). Sempre inclua um default (_) a menos que queira uma exceção de runtime. Switch expressions são preferidas a statements para lógica que retorna valor.

csharp
// Traditional switch statement (verbose)
string Old(int n) {
    switch (n) {
        case 1: return "one";
        case 2: return "two";
        default: return "many";
    }
}

// Switch expression (C# 8): expression, returns value
string New(int n) => n switch {
    1 => "one",
    2 => "two",
    _ => "many"
};

// With multiple values per arm
string Describe(int day, bool holiday) => (day, holiday) switch {
    (6 or 7, _) => "Weekend",
    (_, true) => "Holiday",
    (1, false) => "Monday",
    _ => "Weekday"
};

// Exhaustiveness: compiler warns if not all cases covered
// (especially with enums)
17

Records & Imutabilidade

Básico de Record (C# 9)

Records (C# 9) são tipos de referência com igualdade baseada em valor—dois records são iguais se seus dados forem iguais (diferente de classes, que usam igualdade de referência). O compilador gera Equals, GetHashCode, ToString e Deconstruct automaticamente. A expressão 'with' cria uma cópia com propriedades modificadas (mutação não destrutiva). Records são ideais para DTOs, value objects e modelos de dados imutáveis.

csharp
// Record: value-equality, immutable, concise
public record Person(string Name, int Age);

// Create
var p1 = new Person("Alice", 30);
var p2 = new Person("Alice", 30);

// Value equality (auto-generated)
Console.WriteLine(p1 == p2);  // true (compares properties)
Console.WriteLine(p1.Equals(p2));  // true

// ToString (auto-generated, readable)
Console.WriteLine(p1);  // Person { Name = Alice, Age = 30 }

// Deconstruction
var (name, age) = p1;

// Non-destructive mutation (with-expression)
var older = p1 with { Age = 31 };
// p1 is unchanged, older is a copy with Age = 31

Record Structs (C# 10)

C# 10 adicionou record structs (tipos de valor com recursos de record) e readonly record structs (tipos de valor imutáveis). Escolha: record class (tipo de referência, para dados maiores/compartilhados), record struct (tipo de valor, para dados pequenos, evita alocação no heap), readonly record struct (tipo de valor imutável, mais seguro). Record structs têm igualdade de valor e suporte a 'with' como record classes. Use readonly record struct para pequenos valores imutáveis como Point, Money.

csharp
// record class (default, reference type)
public record PersonRef(string Name, int Age);

// record struct (C# 10): value type with record features
public record struct Point(int X, int Y);

// readonly record struct: fully immutable value type
public readonly record struct Money(decimal Amount, string Currency);

// Mutable record struct (allowed but discouraged)
public record struct Counter(int Value) {
    public int Value { get; set; } = Value;
}

var p = new Point(1, 2);
var p2 = p with { X = 5 };  // copy with X=5
Console.WriteLine(p == p2);  // false

// Records can have additional members
public record Person(string Name) {
    public int Age { get; init; }  // extra init-only prop
    public string DisplayName => Name.ToUpper();
}

Init-only Setters

Init-only setters (C# 9) permitem configuração durante a inicialização do objeto, mas não depois—imutabilidade após construção sem boilerplate de construtor. 'required' (C# 11) força o chamador a definir uma propriedade no inicializador (verificação em tempo de compilação). Records usam init setters por padrão. Isso permite padrões de objeto imutáveis com sintaxe de object-initializer, que é mais legível que parâmetros de construtor.

csharp
public class Config {
    // init: settable only during construction (object initializer)
    public string Name { get; init; } = "";
    public int Timeout { get; init; } = 30;
}

var c = new Config { Name = "MyApp", Timeout = 60 };
// c.Name = "Other";  // ERROR: init-only after construction

// Required properties (C# 11): must be set in initializer
public class User {
    public required string Email { get; init; }
    public string? DisplayName { get; init; }
}

// Must set Email, DisplayName optional
var u = new User { Email = "[email protected]" };

// Records use init by default (positional parameters)
public record Point(int X, int Y);
// Equivalent to: public int X { get; init; }
//                public int Y { get; init; }

Herança de Record

Records suportam herança: um record derivado inclui parâmetros de record base em seu construtor. A expressão 'with' preserva o tipo de runtime (criando um Dog a partir de um Dog, não um Animal). Verificações de igualdade verificam o tipo de runtime—dois records são iguais apenas se forem do mesmo tipo com dados iguais. Isso faz records funcionarem corretamente em coleções polimórficas, diferentemente de igualdade de valor ingênua.

csharp
// Base record
public record Animal(string Name);

// Derived record: must include base parameters
public record Dog(string Name, string Breed) : Animal(Name);

// Derived can add to base
public record Teacher(string Name, string Subject) : Person(Name);

var d = new Dog("Rex", "Labrador");
Console.WriteLine(d);  // Dog { Name = Rex, Breed = Labrador }

// with preserves the runtime type
var d2 = d with { Name = "Buddy" };
Console.WriteLine(d2.GetType().Name);  // Dog (not Animal)

// Value equality across hierarchy
Animal a = new Dog("Rex", "Lab");
Animal b = new Dog("Rex", "Lab");
Console.WriteLine(a == b);  // true

// Different types are not equal even with same data
Animal c = new Animal("Rex");
Console.WriteLine(a == c);  // false (different runtime types)

Records vs Classes vs Structs

Classes usam igualdade de referência (== compara referências); records usam igualdade de valor (compara dados); structs usam igualdade de valor por padrão, mas são tipos de valor (copiados na atribuição). Use records para dados imutáveis com semântica de valor (DTOs, value objects, mensagens). Use classes para entidades mutáveis com identidade (User, Order) e hierarquias de herança. Use structs para valores pequenos (≤16 bytes) que devem ser copiados (Point, Color, Money).

csharp
// Class: reference type, reference equality
public class PersonClass {
    public string Name { get; set; }
    public PersonClass(string name) => Name = name;
}
var c1 = new PersonClass("Alice");
var c2 = new PersonClass("Alice");
Console.WriteLine(c1 == c2);  // false (different references)

// Record: reference type, value equality
public record PersonRec(string Name);
var r1 = new PersonRec("Alice");
var r2 = new PersonRec("Alice");
Console.WriteLine(r1 == r2);  // true (value equality)

// Struct: value type, value equality (manual)
public struct PointStruct {
    public int X, Y;
    public PointStruct(int x, int y) { X = x; Y = y; }
}
var s1 = new PointStruct(1, 2);
var s2 = new PointStruct(1, 2);
Console.WriteLine(s1.Equals(s2));  // true (default struct equality)

// Choose:
// - record: immutable data with value equality (DTOs, value objects)
// - class: mutable, reference semantics, inheritance (entities, services)
// - struct: small, value type, avoid heap allocation (Point, Color)
18

Pattern Matching Aprofundado

Property Patterns

Property patterns correspondem a propriedades de objeto inline. A switch expression retorna um valor diretamente. Padrões combinados (and/or) criam condições flexíveis. null e _ (discard) tratam casos extremos. Isso substitui cadeias if-else verbosas por código declarativo e legível.

csharp
static decimal CalcDiscount(Customer c) => c switch
{
    { IsPremium: true, Orders.Count: > 100 } => 0.20m,
    { IsPremium: true } => 0.10m,
    { Orders.Count: > 50 } => 0.05m,
    { Country: "US" or "CA" } => 0.03m,
    null => 0m,
    _ => 0m
};

List Patterns

List patterns (C# 11) correspondem a arrays e sequências indexáveis. [] corresponde a vazio, [single] corresponde a um elemento, [first, .. rest] usa o padrão slice para capturar elementos restantes. Útil para parsing de comandos, validação de sequências e algoritmos recursivos.

csharp
int Sum(int[] n) => n switch
{
    [] => 0,
    [int single] => single,
    [int first, int second] => first + second,
    [int first, .. int[] rest] => first + Sum(rest),
};

// Matching array shapes
var arr = new[] { 1, 2, 3, 4 };
// [1, ..] matches array starting with 1
// [.., 4] matches array ending with 4
// [1, 2, .., 4] matches 1,2,anything,4

Tuple Patterns

Tuple patterns correspondem múltiplos valores simultaneamente. Cada case testa uma tupla de valores. Combinado com relational patterns (>, <, >=) e logical patterns (and, or, not), isso cria correspondência multidimensional poderosa sem instruções if aninhadas.

csharp
static string Classify(int t, bool rain) => (t, rain) switch
{
    (< 0, _) => "Freezing",
    (>= 0 and < 15, true) => "Cold and wet",
    (>= 0 and < 15, false) => "Cold",
    (>= 15 and < 25, _) => "Mild",
    (>= 25, true) => "Hot and wet",
    (>= 25, false) => "Hot",
};

Type Patterns & Guards

Type patterns correspondem tipos de runtime. A cláusula when adiciona guards adicionais. var corresponde a qualquer coisa (incluindo null). A ordem importa: padrões mais específicos devem vir antes dos gerais. Essa é a maneira idiomática de fazer dispatch baseado em tipo em C# moderno.

csharp
object Process(object input) => input switch
{
    int i when i > 0 => $"Positive: {i}",
    int i => $"Non-positive: {i}",
    string s when s.Length > 10 => s.Substring(0, 10),
    string s => s,
    IList list => $"List with {list.Count} items",
    var x => x?.ToString() ?? "null"
};

Padrões Aninhados

Padrões aninham arbitrariamente profundo. var dentro de um padrão captura o valor correspondido para uso em guards ou no corpo da expressão. Este exemplo corresponde propriedades de Point dentro de records Line. Padrões aninhados são poderosos para validar grafos de objetos complexos de forma concisa.

csharp
record Point(int X, int Y);
record Line(Point Start, Point End);

string Describe(Line line) => line switch
{
    Line({ X: 0, Y: 0 }, { X: 0, Y: 0 }) => "Point at origin",
    Line({ X: var x1, Y: var y1 }, { X: var x2, Y: var y2 })
        when x1 == x2 && y1 == y2 => "Degenerate line",
    Line({ X: var x1 }, { X: var x2 }) when x1 == x2 => "Vertical",
    Line({ Y: var y1 }, { Y: var y2 }) when y1 == y2 => "Horizontal",
    _ => "Diagonal line"
};
19

Records & With Expressions

Igualdade de Valor em Records

Records fornecem igualdade baseada em valor por padrão: duas instâncias com os mesmos dados são iguais. O compilador gera Equals, GetHashCode, ToString e o operador ==. Records são tipos de referência, mas projetados para imutabilidade. Use-os para DTOs, value objects e dados que devem ser comparados por conteúdo.

csharp
public record Person(string Name, int Age);

var p1 = new Person("Alice", 30);
var p2 = new Person("Alice", 30);
Console.WriteLine(p1 == p2);  // True (value equality)
Console.WriteLine(p1.GetHashCode() == p2.GetHashCode()); // True
Console.WriteLine(p1);  // Person { Name = Alice, Age = 30 }

With Expressions

A expressão with cria uma cópia de um record com propriedades modificadas. O original permanece inalterado (mutação não destrutiva). Essa é a maneira idiomática de atualizar dados imutáveis. Nos bastidores, o compilador usa um copy constructor protected e init-only setters.

csharp
var alice = new Person("Alice", 30);
var alice31 = alice with { Age = 31 };
var aliceMarried = alice with { Name = "Alice Smith" };

// Original is unchanged (immutable)
Console.WriteLine(alice.Age);      // 30
Console.WriteLine(alice31.Age);    // 31
Console.WriteLine(aliceMarried.Name); // Alice Smith

Positional vs Init-Only

Records posicionais usam sintaxe de primary constructor e geram métodos deconstruct. Records init-only usam sintaxe de object initializer, permitindo valores padrão e modificadores required. Escolha positional para tipos de valor simples; init-only para records complexos com propriedades opcionais ou computadas.

csharp
// Positional record (primary constructor)
public record Point(int X, int Y);

// Init-only record (more flexible properties)
public record Employee
{
    public required string Name { get; init; }
    public decimal Salary { get; init; }
    public string Dept { get; init; } = "General";
}

var emp = new Employee { Name = "Bob", Salary = 50000 };
var promoted = emp with { Salary = 60000 };

Record Structs & Herança

Record structs são tipos de valor (copiados na atribuição) e não podem herdar de outros record structs. Records de tipo de referência suportam herança. readonly record struct previne mutação. Use record struct para pequenos valores imutáveis; record class para objetos maiores que se beneficiam de semântica de referência e herança.

csharp
// Record struct (value type)
public readonly record struct Money(decimal Amount, string Currency);

// Record inheritance (reference type only)
public record Animal(string Name);
public record Dog(string Name, string Breed) : Animal(Name);

var dog = new Dog("Rex", "Labrador");
Console.WriteLine(dog is Animal);  // True
var copied = dog with { Breed = "Poodle" };

Deconstruct & Matching

Records posicionais geram automaticamente um método Deconstruct, permitindo desconstrução de tupla. Isso se integra perfeitamente com pattern matching: você pode corresponder propriedades de record posicionalmente ou por nome. A desconstrução funciona em var patterns, tuple patterns e switch expressions.

csharp
var person = new Person("Alice", 30);
var (name, age) = person;  // Deconstruct
Console.WriteLine($"{name} is {age}");

// Works with pattern matching
string Greet(Person p) => p switch
{
    ("Alice", _) => "Hi Alice!",
    (_, < 18) => "Hello young one",
    var (n, a) => $"Hello {n}, age {a}"
};
20

Source Generators

Source Generator Básico

Source generators executam em tempo de compilação e adicionam arquivos de código-fonte C# à compilação. IIncrementalGenerator é a API moderna (C# 9+). RegisterPostInitializationOutput adiciona código estático sem analisar o código do usuário. Generators são somente leitura: podem adicionar arquivos, mas não modificar código existente.

csharp
[Generator]
public class HelloGenerator : IIncrementalGenerator
{
    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
        context.RegisterPostInitializationOutput(ctx =>
        {
            ctx.AddSource("Hello.g.cs", """
                namespace Generated;
                public static class Hello
                {
                    public static string World => "Hello!";
                }
                """);
        });
    }
}

Gerando a partir de Attributes

ForAttributeWithMetadataName encontra tipos marcados com um attribute específico. O pipeline: syntax provider filtra nós, transform extrai dados, Collect agrupa resultados, RegisterSourceOutput emite código. Esse pipeline incremental armazena resultados em cache e só regenera quando as entradas mudam, mantendo os builds rápidos.

csharp
// Find types marked with an attribute
context.SyntaxProvider.ForAttributeWithMetadataName(
    "MyApp.AutoToStringAttribute",
    predicate: (node, _) => node is ClassDeclarationSyntax,
    transform: (ctx, _) => (ClassDeclarationSyntax)ctx.TargetNode)
    .Collect()
    .RegisterSourceOutput((spc, classes) =>
    {
        foreach (var cls in classes)
            GenerateToString(spc, cls);
    });

Saída StringBuilder

Generators produzem código-fonte como strings. StringBuilder é eficiente para saída multi-linha. A classe alvo deve ser partial para que o generator adicione membros. Arquivos gerados aparecem na IDE sob Dependencies > Analyzers. Use o sufixo .g.cs por convenção para distinguir arquivos gerados.

csharp
void GenToString(SourceProductionContext ctx, ClassDeclarationSyntax cls)
{
    var sb = new StringBuilder();
    sb.AppendLine($"namespace {cls.ContainingNamespace};");
    sb.AppendLine($"public partial class {cls.Identifier}");
    sb.AppendLine("{");
    sb.AppendLine("  public override string ToString()");
    sb.AppendLine("  {");
    var props = cls.Members.OfType<PropertyDeclarationSyntax>();
    sb.AppendLine($"    return $"{cls.Identifier}: " +
      string.Join(", ", props.Select(p => $"{p.Identifier}={{{p.Identifier}}}")) + "";");
    sb.AppendLine("  }");
    sb.AppendLine("}");
    ctx.AddSource($"{cls.Identifier}_ToString.g.cs", sb.ToString());
}

JsonSerializerContext

System.Text.Json inclui um source generator integrado que pré-compila lógica de serialização, eliminando reflection em tempo de execução. Isso melhora o desempenho e suporta compilação AOT (Native AOT). Declare tipos com JsonSerializable, depois use o contexto gerado para chamadas de serialize/deserialize.

csharp
[JsonSourceGenerationOptions(
    PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
    WriteIndented = true)]
[JsonSerializable(typeof(List<Person>))]
[JsonSerializable(typeof(Dictionary<string, object>))]
public partial class AppJsonContext : JsonSerializerContext { }

// Usage (no runtime reflection):
var json = JsonSerializer.Serialize(people, AppJsonContext.Default.ListPerson);
var data = JsonSerializer.Deserialize(json, AppJsonContext.Default.ListPerson);

Casos de Uso Comuns

Casos de uso populares de generator: geração de DTO, registro de DI, configuração fortemente tipada, profiles do AutoMapper, templates de logging e serialização JSON. Bibliotecas como MediatR, AutoMapper e Microsoft.Extensions.Logging usam generators para reduzir boilerplate e melhorar a compatibilidade com AOT.

csharp
// 1. DTO generation from interfaces
public interface IUserDto { string Name { get; } int Age { get; } }
// Generator creates: public class UserDto : IUserDto { ... }

// 2. Dependency injection registration
[RegisterService(ServiceLifetime.Scoped)]
public class MyService : IService { }
// Generator creates: services.AddScoped<MyService>();

// 3. Strongly-typed configuration
[ConfigurationSection("App:Db")]
public class DbSettings { public string Conn { get; set; } }
// Generator creates: services.Configure<DbSettings>(...)
21

Span & Memory

Básico de Span

Span<T> fornece uma visão type-safe e memory-safe sobre memória contígua sem cópia. Pode envolver arrays, memória stackalloc, memória não gerenciada ou strings. Span é um ref struct (somente stack), então não pode ser boxed, armazenado em campos ou capturado por lambdas. Use-o para slicing e parsing de alto desempenho.

csharp
// Span<T> is a stack-only view over contiguous memory
Span<int> numbers = stackalloc int[] { 1, 2, 3, 4, 5 };

// From array
int[] arr = { 10, 20, 30, 40, 50 };
Span<int> slice = arr.AsSpan(1, 3);  // [20, 30, 40]
slice[0] = 99;  // Modifies original array

// From string
ReadOnlySpan<char> text = "Hello, World".AsSpan();
ReadOnlySpan<char> hello = text.Slice(0, 5);  // "Hello"

Memory para Async

Memory<T> é a contraparte heap-safe do Span<T>. Pode ser armazenado em campos, capturado por lambdas e usado através de boundaries de await. Converta para Span<T> via .Span ao fazer trabalho real. Use Memory para APIs async e Span para hot paths síncronos.

csharp
// Memory<T> can live on the heap and cross async boundaries
async Task ProcessAsync(Memory<byte> buffer)
{
    await Task.Delay(100);
    Span<byte> span = buffer.Span;
    for (int i = 0; i < span.Length; i++)
        span[i] = (byte)(span[i] * 2);
}

byte[] data = new byte[1024];
await ProcessAsync(data);  // Implicit conversion to Memory<byte>

Evitando Alocações

Span elimina alocações de string intermediárias durante parsing e slicing. Para hot paths que processam grandes entradas (parsing de HTTP, JSON, arquivos de log), isso pode reduzir a pressão do GC drasticamente. int.TryParse e outros primitivos têm overloads de Span. Só chame ToString() quando realmente precisar de uma string.

csharp
// Traditional (allocates new string)
string Extract(string input, int start, int len)
    => input.Substring(start, len);

// Zero-allocation with Span
ReadOnlySpan<char> ExtractFast(ReadOnlySpan<char> input, int start, int len)
    => input.Slice(start, len);

// Parse without allocating substrings
bool TryParseInt(ReadOnlySpan<char> span, out int result)
    => int.TryParse(span, out result);

stackalloc & Não Gerenciado

stackalloc aloca na call stack (auto-liberação, sem pressão no GC), mas é limitado a tamanhos pequenos. Para buffers grandes, use NativeMemory.Alloc/Free (C# 7+) ou Marshal.AllocHGlobal. Sempre envolva memória não gerenciada em um Span e libere-a em um bloco finally para evitar vazamentos.

csharp
// Stack allocation (small, fast, auto-freed)
Span<int> buffer = stackalloc int[128];
for (int i = 0; i < buffer.Length; i++)
    buffer[i] = i;

// Unmanaged memory (large buffers, manual lifetime)
unsafe
{
    int* ptr = (int*)NativeMemory.Alloc(1024, sizeof(int));
    try
    {
        Span<int> span = new Span<int>(ptr, 1024);
        span.Fill(42);
    }
    finally
    {
        NativeMemory.Free(ptr);
    }
}

APIs Amigáveis a Span

Projete APIs para aceitar Span<T> ou ReadOnlySpan<T> para buffers, evitando alocações de array. O chamador decide se deve usar stackalloc, um array ou memória não gerenciada. Span.TryWrite (C# 10+) formata diretamente em um span sem alocar uma string, útil para logging e serialização.

csharp
// Writing to a Span buffer
int WriteValues(Span<int> dest)
{
    int count = 0;
    foreach (var v in new[] { 1, 2, 3 })
    {
        if (count >= dest.Length) break;
        dest[count++] = v;
    }
    return count;
}

// Span-based string formatting (C# 10+)
Span<char> dest = stackalloc char[64];
bool ok = dest.TryWrite($"Value: {42:N2}", out int charsWritten);
Console.WriteLine(dest.Slice(0, charsWritten).ToString());
22

Interpolated Strings Aprofundado

Especificadores de Formato

Interpolação de strings ($"...") incorpora expressões em chaves. Especificadores de formato após um caractere de dois pontos controlam a saída: C para moeda, F para decimais fixos, X para hex, D para dígitos com padding, P para porcentagem, yyyy-MM-dd para datas. O compilador traduz isso para string.Format ou FormattableString.

csharp
string name = "Alice";
int age = 30;
decimal price = 19.99m;
DateTime now = DateTime.Now;

Console.WriteLine($"Name: {name}, Age: {age}");
Console.WriteLine($"Price: {price:C}");  // $19.99
Console.WriteLine($"Date: {now:yyyy-MM-dd HH:mm:ss}");
Console.WriteLine($"Hex: {255:X}");  // FF
Console.WriteLine($"Padded: {age:D5}");  // 00030
Console.WriteLine($"Percent: {0.85:P0}");  // 85%

Raw String Interpolation

Verbatim interpolated strings ($@) preservam barras invertidas literalmente e permitem conteúdo multi-linha. Raw string interpolation (C# 11) usa aspas triplas e elimina todo escaping: sem necessidade de escapar chaves ou aspas. O número de sinais $ controla quantas chaves são necessárias para interpolação.

csharp
// Verbatim interpolated ($@)
string path = $@"C:\Users\{name}\Documents";
string json = $@"{{
  ""name"": ""{name}"",
  ""age"": {age}
}}";

// Raw string interpolation (C# 11)
string query = $$"""
    SELECT * FROM Users
    WHERE Name = '{{name}}'
    AND Age > {{age - 5}}
    """;
// $$ means {{ }} for interpolation, no escaping needed

FormattableString & Cultura

FormattableString preserva a estrutura de interpolação, permitindo formatação diferida com culturas específicas. Isso é crítico para bibliotecas que não devem depender da cultura atual. FormattableString.Invariant força cultura invariante, prevenindo problemas específicos de locale em serialização e logging.

csharp
// FormattableString captures the format for deferred execution
FormattableString msg = $"Hello {name}, you are {age} years old";

// Explicit culture (avoid CurrentCulture issues in libraries)
string german = msg.ToString(CultureInfo.GetCultureInfo("de-DE"));
string invariant = msg.ToString(CultureInfo.InvariantCulture);

// Invariant by default
string formatted = FormattableString.Invariant($"Total: {price}");

Interpolated String Handlers

Interpolated string handlers (C# 10) permitem que o compilador passe componentes de interpolação diretamente para um método, evitando alocação de string quando o resultado não é necessário. É assim que funciona logging de alto desempenho: se o nível de log estiver desativado, nenhuma string é construída. O handler recebe literais e valores formatados separadamente.

csharp
// Custom handler (C# 10+) for conditional allocation
[InterpolatedStringHandler]
public ref struct LogStringHandler
{
    public LogStringHandler(int literal, int hole, Logger logger, out bool enabled)
    {
        enabled = logger.IsEnabled;
        // Skip allocation if logging is disabled
    }
    public void AppendLiteral(string s) { /* ... */ }
    public void AppendFormatted<T>(T value) { /* ... */ }
}

// Usage: no string allocation if log level is off
logger.Info($"Processing {item} with {count} items");

Formatação Baseada em Span

Span.TryWrite formata interpolated strings diretamente em um buffer alocado na stack, evitando alocação no heap inteiramente. string.Create com uma interpolated string e culture fornece formatação culture-aware sem alocações intermediárias. Essas são técnicas essenciais para hot paths em aplicações de alto throughput.

csharp
// Format directly into a buffer (C# 10+)
Span<char> buffer = stackalloc char[256];

if (buffer.TryWrite($"User: {name}, Age: {age}", out int chars))
{
    var result = buffer[..chars];
    Console.WriteLine(result);
}

// Concatenation with DefaultInterpolatedStringHandler
string built = string.Create(CultureInfo.InvariantCulture, $"Total={price}");
23

File-Scoped Namespaces

Sintaxe File-Scoped

File-scoped namespaces (C# 10) declaram o namespace para todo o arquivo com um ponto e vírgula em vez de chaves. Isso remove um nível de indentação, tornando o código mais plano e fácil de ler. Apenas um file-scoped namespace por arquivo é permitido. Todos os tipos no arquivo pertencem a esse namespace.

csharp
// Traditional (adds a level of indentation)
namespace MyApp.Services
{
    public class UserService
    {
        public void Process() { }
    }
}

// File-scoped (C# 10+, no indentation)
namespace MyApp.Services;

public class UserService
{
    public void Process() { }
}

Global Usings

Global usings (C# 10) declarados em um arquivo se aplicam a todos os arquivos do projeto. Combine com file-scoped namespaces para boilerplate mínimo e limpo. Projetos .NET SDK auto-geram global usings para namespaces comuns (ImplicitUsings). Ordem: global usings, depois file usings, depois namespace.

csharp
// Global usings (apply to all files in the project)
global using System.Collections.Generic;
global using System.Linq;

// File-scoped namespace with usings
using System.Text.Json;
using MyApp.Models;

namespace MyApp.Api;

public class UserController
{
    // Can use List, LINQ, Json, Models without per-file usings
}

Nested Namespaces

File-scoped namespaces usam nomes pontilhados para hierarquia: MyApp.Services.Authentication é equivalente a blocos de namespace aninhados. Múltiplos arquivos podem compartilhar o mesmo namespace. Você não pode aninhar tipos em um namespace diferente dentro do mesmo arquivo; use arquivos separados em vez disso.

csharp
// File-scoped: flat name with dots
namespace MyApp.Services.Authentication;

public class TokenService { }

// Multiple files can use the same namespace
// File 1: namespace MyApp.Services;
// File 2: namespace MyApp.Services;  // Same namespace

Configuração de Projeto

ImplicitUsings no arquivo de projeto auto-gera global usings para namespaces comuns do System. Combinado com file-scoped namespaces, isso elimina a maior parte do boilerplate. Nullable habilita nullable reference types. Esses são o padrão para novos projetos .NET 6+.

csharp
<!-- .csproj -->
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>
</Project>

<!-- ImplicitUsings generates:
     global using System;
     global using System.Collections.Generic;
     global using System.IO;
     global using System.Linq;
     global using System.Net.Http;
     global using System.Threading;
     global using System.Threading.Tasks;
-->

Migração & Melhores Práticas

Use .editorconfig para impor file-scoped namespaces (IDE0161). A IDE fornece migração com um clique de tradicional para file-scoped. Melhor prática: um tipo público por arquivo, com o nome do arquivo correspondendo ao nome do tipo. Isso mantém os arquivos pequenos e a navegação fácil.

csharp
// .editorconfig to enforce file-scoped namespaces
[*.{cs,vb}]
dotnet_diagnostic.IDE0161.severity = error  // Prefer file-scoped

// Migration: use the IDE fixer to convert
// Right-click > Quick Actions > Convert to file-scoped namespace

// Best practice: one type per file
// UserService.cs
namespace MyApp.Services;
public class UserService { }
24

Required Members

Required Properties

O modificador required (C# 11) força chamadores a inicializar propriedades durante a criação do objeto. O compilador emite CS9035 se um membro required não for definido. Isso substitui validação de construtor para propriedades obrigatórias. Funciona com init-only setters e object initializers, fornecendo garantias em tempo de compilação.

csharp
public class User
{
    public required Guid Id { get; init; }
    public required string Name { get; init; }
    public required string Email { get; init; }
    public string? Phone { get; init; }  // Optional
}

// Compiler enforces initialization
var user = new User
{
    Id = Guid.NewGuid(),
    Name = "Alice",
    Email = "[email protected]"
};

// Error CS9035: Required member 'Name' must be set
// var bad = new User { Id = Guid.NewGuid() };

SetsRequiredMembers

O attribute SetsRequiredMembers diz ao compilador que um construtor inicializa todos os membros required, permitindo que chamadores usem o construtor sem um object initializer. Isso faz a ponte entre propriedades required e construtores tradicionais. O attribute não verifica em tempo de execução; é uma promessa em tempo de compilação.

csharp
public class Configuration
{
    public required string ConnectionString { get; init; }
    public required int Port { get; init; }

    [SetsRequiredMembers]
    public Configuration(string conn, int port)
    {
        ConnectionString = conn;
        Port = port;
    }
}

// Both work: constructor or initializer
var c1 = new Configuration("localhost", 5432);
var c2 = new Configuration
{
    ConnectionString = "localhost",
    Port = 5432
};

Required com Records

Membros required funcionam com records. Para records posicionais, propriedades required são declaradas no corpo, não no primary constructor. A expressão with preserva valores required automaticamente. Valores padrão (como OpenedAt) tornam propriedades opcionais sem o modificador required.

csharp
public record Account
{
    public required string AccountNumber { get; init; }
    public required decimal Balance { get; init; }
    public DateTime OpenedAt { get; init; } = DateTime.UtcNow;
}

// Positional record with required
public record Customer(string Name)
{
    public required string Email { get; init; }
}

var cust = new Customer("Alice") { Email = "[email protected]" };

Init vs Required vs Set

init torna uma propriedade configurável apenas durante a construção (imutável depois). required força a inicialização. Juntos (required + init) eles criam propriedades imutáveis obrigatórias. required + set permite modificação pós-construção. Escolha com base em se o valor deve mudar após a criação.

csharp
// Init-only: can be set in initializer, but not required
public class Settings
{
    public string Name { get; init; } = "Default";
}

// Required + init: must be set, immutable after
public class SecureSettings
{
    public required string ApiKey { get; init; }
}

// Required + set: must be set, but can be modified later
public class MutableConfig
{
    public required string Environment { get; set; }
}

Serialização JSON

System.Text.Json respeita o modificador required: a desserialização lança JsonException se uma propriedade required estiver faltando no JSON. Isso fornece validação em tempo de execução correspondente ao contrato em tempo de compilação. Combine com init para DTOs imutáveis que são garantidos de ter todos os campos obrigatórios preenchidos.

csharp
using System.Text.Json;

public class ApiResponse
{
    public required string Status { get; init; }
    public required int Code { get; init; }
    public string? Message { get; init; }
}

// Deserialization enforces required at runtime
var json = """{"Status":"OK","Code":200}""";
var response = JsonSerializer.Deserialize<ApiResponse>(json);

// Throws JsonException if required member is missing
// var bad = JsonSerializer.Deserialize<ApiResponse>("""{"Code":200}""");
25

Collection Expressions

Sintaxe de Collection Expression

Collection expressions (C# 12) fornecem uma sintaxe uniforme [..] para inicializar qualquer tipo de coleção. O compilador infere o tipo e usa o builder apropriado. Isso substitui a sintaxe verbosa new List<int> { 1, 2, 3 }. Funciona com arrays, List<T>, Span<T>, IEnumerable<T> e coleções personalizadas com um attribute CollectionBuilder.

csharp
// C# 12 collection expressions
int[] numbers = [1, 2, 3, 4, 5];
List<string> names = ["Alice", "Bob", "Charlie"];
Span<int> span = [10, 20, 30];
IEnumerable<int> seq = [1, 2, 3];

// Empty collections
int[] empty = [];
List<int> emptyList = [];

// Works with any collection type that has a builder
HashSet<string> set = ["a", "b", "c"];

Spread Operator

O spread operator (..) achata uma coleção na collection expression delimitadora. Isso substitui Concat, AddRange e loops manuais. Você pode misturar spreads com elementos individuais. O spread funciona com qualquer IEnumerable<T>, facilitando combinar múltiplas fontes de dados de forma concisa.

csharp
int[] a = [1, 2, 3];
int[] b = [4, 5, 6];

// Spread elements with ..
int[] combined = [..a, ..b, 7, 8];  // [1,2,3,4,5,6,7,8]

// Mix spreads and literals
List<int> result = [0, ..a, 100, ..b];

// Spread any IEnumerable
List<int> evens = [.. Enumerable.Range(0, 10).Where(x => x % 2 == 0)];

Padrões de Retorno & Parâmetro

Collection expressions funcionam em instruções de retorno, argumentos e atribuições. O compilador converte para o tipo alvo automaticamente. Spreads condicionais (ternário com ..) permitem incluir elementos opcionalmente. Isso torna a construção de coleções a partir de múltiplas fontes opcionais limpa e legível.

csharp
// Return collection expressions
List<int> GetNumbers() => [1, 2, 3];
int[] GetArray() => [10, 20];

// Pass as arguments
void Process(IEnumerable<int> data) { }
Process([1, 2, 3]);

// Conditional spread
bool includeExtra = true;
var items = [
    1, 2, 3,
    .. includeExtra ? [4, 5] : []
];

Custom Collection Builders

O attribute CollectionBuilder conecta um tipo de coleção a um método factory, permitindo suporte a collection expression. O método Create recebe um ReadOnlySpan<T> de elementos. Isso permite que coleções de terceiros participem da sintaxe uniforme [..]. O padrão builder mantém a inicialização eficiente.

csharp
[CollectionBuilder(typeof(MyCollection), "Create")]
public class MyCollection<T> : IEnumerable<T>
{
    private readonly List<T> _items = new();
    public IEnumerator<T> GetEnumerator() => _items.GetEnumerator();

    public static MyCollection<T> Create(ReadOnlySpan<T> items)
    {
        var col = new MyCollection<T>();
        foreach (var item in items) col._items.Add(item);
        return col;
    }
}

// Now usable with collection expressions
MyCollection<int> custom = [1, 2, 3, 4];

Params Collections

C# 12 permite params com qualquer tipo de coleção, não apenas arrays. Collection expressions podem ser passadas para métodos params. Quando o alvo é Span<T>, o compilador pode usar stack allocation para coleções pequenas, evitando alocação no heap inteiramente. Isso torna métodos variádicos tanto ergonômicos quanto eficientes.

csharp
// C# 12: params with collection expressions
void Log(params string[] messages)
{
    foreach (var m in messages) Console.WriteLine(m);
}

// Traditional params call
Log("Error", "Warning", "Info");

// Collection expression call (C# 12)
Log(["Error", "Warning", "Info"]);

// params with Span for zero-allocation
void ProcessFast(params Span<int> values) { }
ProcessFast([1, 2, 3]);  // May use stackalloc internally

Was this helpful?

Learning path

Learn from scratch

Learn this language from the ground up with structured lessons.