[Apostila(Linguagem = "C#", Nivel = "Básico → Muito Avançado")]

C# do zero ao nível sênior, com foco no mercado

Uma trilha completa para quem já conhece lógica de programação e quer dominar C# com profundidade — cobrindo backend com ASP.NET Core, aplicações desktop e web, e desenvolvimento de jogos com Unity. Com teoria de verdade, exercícios, projetos reais e preparação para entrevistas técnicas.

23 capítulos6 partes progressivas
3 áreas de mercadoBackend · Desktop/Web · Jogos
+80 exemploscódigo comentado em PT-BR
[ComoUsar]

Como usar esta apostila

A apostila foi montada como uma trilha: cada capítulo assume que você domina os anteriores. Se você já conhece lógica de programação (seu caso), pode ler a Parte I em ritmo acelerado, mas não a pule — é nela que ficam detalhes de C# que caem em entrevista, como tipos de valor vs. referência.

Ao longo do texto você encontrará quatro tipos de caixas:

Boa prática

Convenções e hábitos que times profissionais esperam de você desde o primeiro dia.

Cai na entrevista

Perguntas clássicas de processo seletivo relacionadas ao tema do capítulo — com a resposta que o entrevistador quer ouvir.

No mercado

Como o conceito aparece no dia a dia real de empresas: em code review, em sistemas legados, em decisões de arquitetura.

Armadilha

Erros comuns que derrubam iniciantes (e às vezes plenos) em produção.

No fim de cada capítulo há exercícios com soluções ocultas — tente antes de abrir. Os capítulos 19 a 21 aplicam tudo em cada área do mercado, e os capítulos 22 e 23 fecham com entrevistas, projetos de portfólio e roadmap de carreira.

Boa prática

Digite os exemplos, não copie. A memória muscular de sintaxe importa em entrevistas com live coding. Crie um projeto dotnet new console só para rodar os exemplos da apostila.

Parte I

Fundamentos

A base da linguagem e da plataforma. Mesmo vindo de outra linguagem, leia com atenção: o sistema de tipos do C# tem detalhes que definem quem passa em entrevista técnica.

[Capitulo(1, Nivel.Fundamentos)]

O ecossistema C# e .NET

C# é a linguagem; .NET é a plataforma que a executa. Entender essa separação é o primeiro passo — e uma pergunta frequente de entrevista para júnior. Quando você compila C#, o compilador (Roslyn) não gera código de máquina diretamente: ele gera IL (Intermediate Language), que é executado pelo CLR (Common Language Runtime). Em tempo de execução, o JIT (Just-In-Time compiler) converte IL em código nativo da máquina.

// O caminho do seu código:
// arquivo .cs  →  Roslyn (compilador)  →  IL (.dll)  →  CLR + JIT  →  código nativo

Isso traz consequências práticas: o mesmo binário roda em Windows, Linux e macOS; o CLR gerencia memória por você (Garbage Collector, capítulo 14); e outras linguagens (F#, VB.NET) compilam para o mesmo IL e interoperam com seu código.

O .NET de hoje (e o legado que você vai encontrar)

NomeO que éOnde você encontra
.NET (5, 6, 7, 8, 9+)A plataforma moderna, multiplataforma e open source. Versões pares (6, 8) são LTS — suporte de 3 anos.Todo projeto novo. Empresas sérias usam a LTS mais recente.
.NET Framework (4.x)A plataforma clássica, só Windows, congelada na 4.8.Sistemas legados de bancos, indústrias, governo. Muito comum em vagas de manutenção.
.NET StandardEspecificação de compatibilidade entre os dois mundos.Bibliotecas que precisam rodar em ambos.
Mono / UnityRuntime alternativo usado pelo Unity (com scripting backend IL2CPP para builds).Desenvolvimento de jogos (capítulo 21).
No mercado

Muitas vagas no Brasil pedem "migração de .NET Framework para .NET 8". Saber que ASP.NET WebForms e WCF não existem no .NET moderno (e conhecer os substitutos: ASP.NET Core e gRPC/REST) é diferencial real em entrevistas para essas posições.

Instalando e criando o primeiro projeto

Instale o SDK do .NET (dotnet.microsoft.com) e um editor: Visual Studio (Windows, completo), VS Code + extensão C# Dev Kit (leve, multiplataforma) ou Rider (pago, muito usado em times de jogos). Depois, no terminal:

dotnet --version          # confirma a instalação
dotnet new console -n MeuPrimeiroApp
cd MeuPrimeiroApp
dotnet run                # compila e executa

Anatomia de um programa

Desde o .NET 6, o template usa top-level statements — o Main fica implícito:

Program.cs (estilo moderno)
Console.WriteLine("Olá, mercado de trabalho!");

Mas você precisa conhecer a forma completa, porque é o que existe em 99% dos códigos de empresa:

Program.cs (forma clássica)
using System;                     // importa um namespace

namespace MeuPrimeiroApp          // organiza o código em "pacotes" lógicos
{
    internal class Program        // toda lógica vive dentro de tipos
    {
        static void Main(string[] args)   // ponto de entrada do programa
        {
            Console.WriteLine("Olá, mercado de trabalho!");
        }
    }
}
  • using importa namespaces (como import/include em outras linguagens).
  • namespace agrupa tipos e evita conflito de nomes. Desde o C# 10 pode ser file-scoped: namespace MeuPrimeiroApp; (sem chaves, menos indentação).
  • Main é o ponto de entrada; args são os argumentos de linha de comando.

O arquivo .csproj e o NuGet

Todo projeto tem um .csproj — o "manifesto" que define versão do .NET, dependências e configurações:

MeuPrimeiroApp.csproj
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>
</Project>

Dependências externas vêm do NuGet, o gerenciador de pacotes do .NET (equivalente ao npm/pip):

dotnet add package Newtonsoft.Json   # adiciona uma biblioteca ao projeto
dotnet restore                       # restaura dependências
dotnet build                         # compila
dotnet test                          # roda os testes
Cai na entrevista

"Qual a diferença entre .NET Framework e .NET?" — Resposta ideal: o Framework é a versão clássica, exclusiva do Windows e congelada na 4.8; o .NET moderno (5+) é multiplataforma, open source, com melhor performance e é onde acontece toda evolução. Cite que versões pares são LTS. "O que é o CLR?" — É o runtime que executa o IL: faz JIT, gerencia memória (GC), segurança de tipos e exceções.

Exercícios · Capítulo 1
  1. Instale o SDK e crie um projeto console chamado Curriculo que imprime seu nome, sua stack-alvo (backend, jogos...) e o ano em que você quer estar empregado como dev C#.
  2. Descubra (via dotnet --list-sdks) quais SDKs estão instalados na sua máquina.
  3. Converta o template moderno (top-level) para a forma clássica com Main, e faça o programa imprimir cada argumento recebido em args.
Ver solução do exercício 3
using System;

namespace Curriculo
{
    internal class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine($"Recebi {args.Length} argumento(s):");
            foreach (string arg in args)
            {
                Console.WriteLine($" - {arg}");
            }
        }
    }
}
// Execute com: dotnet run -- primeiro segundo terceiro
[Capitulo(2, Nivel.Fundamentos)]

Sintaxe, variáveis e tipos

C# é estaticamente tipado e fortemente tipado: toda variável tem um tipo conhecido em tempo de compilação, e conversões perigosas não acontecem silenciosamente. Isso é uma vantagem enorme em sistemas grandes — o compilador vira seu primeiro revisor de código.

Declaração e inferência com var

int idade = 28;                    // tipo explícito
var salario = 5500.50m;            // tipo inferido: decimal (por causa do sufixo m)
var nome = "Ana";                  // inferido: string

// var NÃO é tipo dinâmico! O tipo é fixado na compilação:
var x = 10;
// x = "texto";                    // ERRO de compilação: x é int para sempre
Boa prática

Times costumam usar var quando o tipo é óbvio à direita (var lista = new List<int>();) e tipo explícito quando não é (decimal total = CalcularTotal();). Siga a convenção do time — consistência vale mais que preferência pessoal.

Tipos primitivos que você usará todo dia

TipoTamanhoUso típicoExemplo
int32 bitsContadores, IDs, quantidadesint qtd = 42;
long64 bitsIDs de banco grandes, timestampslong id = 9_000_000_000L;
double64 bitsCálculo científico, física de jogosdouble vel = 9.81;
float32 bitsUnity usa float em tudofloat dt = 0.016f;
decimal128 bitsDinheiro — sempre!decimal preco = 19.90m;
boolVerdadeiro/falsobool ativo = true;
char16 bitsUm caractere Unicodechar letra = 'A';
stringTexto (tipo de referência!)string s = "olá";
byte8 bitsDados binários, buffersbyte b = 255;
Armadilha

Nunca use double ou float para dinheiro. Eles são binários de ponto flutuante: 0.1 + 0.2 == 0.3false! Use decimal, que representa valores em base 10 com exatidão. Sistemas financeiros com double geram bugs de centavos que viram incidentes reais.

Tipos de valor vs. tipos de referência

Esta é a distinção mais importante do capítulo — e uma das perguntas mais frequentes em entrevistas de qualquer nível.

  • Tipos de valor (int, double, bool, char, struct, enum): a variável contém o dado. Atribuir copia o valor inteiro. Vivem, em geral, na stack (ou embutidos dentro de objetos).
  • Tipos de referência (class, string, arrays, interface, delegate): a variável contém um endereço para o objeto no heap. Atribuir copia só a referência — os dois nomes apontam para o mesmo objeto.
// TIPO DE VALOR: cópia independente
int a = 10;
int b = a;        // b recebe uma CÓPIA
b = 99;
Console.WriteLine(a);   // 10 — a não mudou

// TIPO DE REFERÊNCIA: mesmo objeto
int[] v1 = { 1, 2, 3 };
int[] v2 = v1;    // v2 aponta para o MESMO array
v2[0] = 99;
Console.WriteLine(v1[0]);   // 99 — mudou "nos dois"
Cai na entrevista

"string é tipo de valor ou de referência?" — É tipo de referência, mas imutável: toda "modificação" cria uma nova string. Por isso ela se comporta como valor em comparações (== compara conteúdo, pois é sobrecarregado). Complemente: concatenar strings em loop cria muitos objetos — use StringBuilder. Responder isso completo diferencia candidatos.

Strings na prática

string nome = "Maria";
int anos = 3;

// Interpolação (forma preferida no mercado)
string msg = $"{nome} tem {anos} anos de experiência";

// Métodos essenciais (lembre: retornam NOVAS strings)
string s = "  C# Backend  ";
Console.WriteLine(s.Trim());              // "C# Backend"
Console.WriteLine(s.ToUpper());           // "  C# BACKEND  "
Console.WriteLine(s.Contains("Back"));    // True
Console.WriteLine(s.Replace("#", "Sharp"));
Console.WriteLine(string.IsNullOrWhiteSpace(s));  // False

var partes = "a,b,c".Split(',');          // ["a","b","c"]
var junto  = string.Join(" | ", partes);  // "a | b | c"

// Concatenação em loop: use StringBuilder
var sb = new System.Text.StringBuilder();
for (int i = 0; i < 1000; i++)
    sb.Append(i).Append(';');
string resultado = sb.ToString();

Conversões de tipo

// Implícita: sem perda de dados, automática
int i = 100;
long l = i;            // int cabe em long
double d = i;          // int cabe em double

// Explícita (cast): pode perder dados — você assume o risco
double pi = 3.99;
int truncado = (int)pi;         // 3 (trunca, não arredonda!)

// String → número: o jeito profissional é TryParse
string entrada = Console.ReadLine() ?? "";
if (int.TryParse(entrada, out int numero))
    Console.WriteLine($"Dobro: {numero * 2}");
else
    Console.WriteLine("Entrada inválida.");

// Convert e Parse lançam exceção se falhar — evite para entrada de usuário
int x = int.Parse("123");        // ok, mas explode com "abc"
int y = Convert.ToInt32("456");
Boa prática

Para entrada externa (usuário, arquivo, API), sempre TryParse. Parse/Convert só quando você controla o dado. Usar exceção para fluxo normal é lento e considerado má prática em code review.

Constantes, readonly e operadores

const double Pi = 3.14159;        // fixado em compilação (só primitivos/string)
// readonly: fixado no construtor — veremos no capítulo 5

int a = 17, b = 5;
Console.WriteLine(a / b);    // 3   (divisão inteira!)
Console.WriteLine(a % b);    // 2   (resto — útil para paridade, ciclos)
Console.WriteLine(a / (double)b);  // 3.4

bool dentro = a > 0 && a < 100;   // && e || têm curto-circuito
int? talvez = null;
int valor = talvez ?? -1;         // ?? : valor padrão se null
string? nome2 = null;
int? tam = nome2?.Length;         // ?. : navegação segura (null se nome2 for null)
Exercícios · Capítulo 2
  1. Leia dois números do console com TryParse e mostre soma, divisão real e resto, tratando entradas inválidas e divisão por zero.
  2. Prove no console que 0.1 + 0.2 != 0.3 com double, e que com decimal a igualdade vale.
  3. Dado string frase = "aprender csharp para o mercado", imprima a frase com iniciais maiúsculas em cada palavra, sem usar bibliotecas prontas de capitalização.
  4. Explique (em comentário no código) por que modificar um array passado a outra variável afeta "os dois", mas o mesmo não acontece com int.
Ver solução do exercício 3
string frase = "aprender csharp para o mercado";
var palavras = frase.Split(' ', StringSplitOptions.RemoveEmptyEntries);
var sb = new System.Text.StringBuilder();

foreach (var p in palavras)
{
    sb.Append(char.ToUpper(p[0]))   // primeira letra maiúscula
      .Append(p.Substring(1))       // resto da palavra
      .Append(' ');
}
Console.WriteLine(sb.ToString().TrimEnd());
// Aprender Csharp Para O Mercado
[Capitulo(3, Nivel.Fundamentos)]

Controle de fluxo e métodos

Decisões: if, operador ternário e switch

decimal salario = 4200m;

if (salario < 2000m)
    Console.WriteLine("Faixa júnior inicial");
else if (salario < 7000m)
    Console.WriteLine("Faixa júnior/pleno");
else
    Console.WriteLine("Faixa pleno/sênior");

// Ternário: para atribuições curtas
string nivel = salario >= 7000m ? "senior" : "junior-pleno";

// switch clássico
int dia = 3;
switch (dia)
{
    case 1:
    case 7:
        Console.WriteLine("Fim de semana");
        break;                       // break é obrigatório
    default:
        Console.WriteLine("Dia útil");
        break;
}

// switch EXPRESSION (C# 8+) — muito usado em código moderno
string turno = DateTime.Now.Hour switch
{
    >= 6 and < 12  => "manhã",
    >= 12 and < 18 => "tarde",
    _              => "noite"      // _ = caso padrão
};

Repetições

for (int i = 0; i < 5; i++)          // quando você controla o índice
    Console.WriteLine($"Tentativa {i}");

int[] notas = { 8, 7, 10, 6 };
foreach (int nota in notas)          // quando você percorre uma coleção
    Console.WriteLine(nota);

int tentativas = 0;
while (tentativas < 3)               // condição antes
    tentativas++;

string? senha;
do                                    // executa ao menos 1 vez
{
    Console.Write("Senha: ");
    senha = Console.ReadLine();
} while (string.IsNullOrEmpty(senha));

// break sai do loop; continue pula para a próxima iteração
for (int i = 0; i < 100; i++)
{
    if (i % 2 == 0) continue;   // ignora pares
    if (i > 9) break;           // para no 9
    Console.Write(i + " ");     // 1 3 5 7 9
}
Boa prática

Prefira foreach sempre que não precisar do índice — comunica intenção e evita erros de off-by-one. Em Unity, porém, for em listas quentes (chamadas a cada frame) pode ser preferido por performance; veremos no capítulo 21.

Métodos

// [modificador] [retorno] Nome(parâmetros)
static decimal CalcularSalarioLiquido(decimal bruto, decimal descontoPct)
{
    return bruto * (1 - descontoPct / 100m);
}

// Expression-bodied: para métodos de uma linha
static bool EhPar(int n) => n % 2 == 0;

// Parâmetros opcionais e nomeados
static void Log(string msg, string nivel = "INFO", bool comData = true)
{
    var prefixo = comData ? $"[{DateTime.Now:HH:mm}] " : "";
    Console.WriteLine($"{prefixo}{nivel}: {msg}");
}
Log("Sistema iniciado");
Log("Falhou!", nivel: "ERRO");          // argumento nomeado
Log("Sem data", comData: false);

// params: quantidade variável de argumentos
static int Somar(params int[] numeros)
{
    int total = 0;
    foreach (var n in numeros) total += n;
    return total;
}
Somar(1, 2, 3, 4);   // 10

ref, out e in — passagem por referência

Por padrão, argumentos são passados por valor (tipos de valor copiam o dado; tipos de referência copiam a referência). Os modificadores mudam isso — e são pergunta clássica de entrevista:

// ref: a variável DEVE estar inicializada; o método pode ler e alterar
static void Dobrar(ref int x) => x *= 2;
int n = 5;
Dobrar(ref n);          // n agora é 10 (ref também no chamador!)

// out: NÃO precisa estar inicializada; o método DEVE atribuir
static bool TentarDividir(int a, int b, out int resultado)
{
    if (b == 0) { resultado = 0; return false; }
    resultado = a / b;
    return true;
}
if (TentarDividir(10, 2, out int r))
    Console.WriteLine(r);           // 5 — padrão TryXxx do .NET

// in: passa por referência SOMENTE leitura (performance com structs grandes)
static double Modulo(in Vector3D v) => Math.Sqrt(v.X*v.X + v.Y*v.Y + v.Z*v.Z);
Cai na entrevista

"Diferença entre ref e out?" — Ambos passam por referência. ref exige variável inicializada e o método pode ou não alterar; out dispensa inicialização e o método é obrigado a atribuir antes de retornar. Cite o padrão TryParse como uso real de out.

Sobrecarga e recursão

// Sobrecarga: mesmo nome, assinaturas diferentes
static double Area(double raio) => Math.PI * raio * raio;
static double Area(double baseRet, double altura) => baseRet * altura;

// Recursão: método que chama a si mesmo (sempre com caso de parada!)
static long Fatorial(int n) => n <= 1 ? 1 : n * Fatorial(n - 1);
Armadilha

Recursão sem caso de parada (ou muito profunda) causa StackOverflowException — que não pode ser capturada e derruba o processo. Para profundidades grandes, converta para loop ou use uma pilha explícita.

Exercícios · Capítulo 3
  1. Escreva FizzBuzz de 1 a 100 (múltiplo de 3 → "Fizz", de 5 → "Buzz", de ambos → "FizzBuzz"). É o teste de triagem mais comum do mundo — precisa sair no reflexo.
  2. Crie bool TentarCalcularImc(double peso, double altura, out double imc) seguindo o padrão TryXxx, validando entradas.
  3. Implemente Fibonacci(int n) de forma recursiva e depois iterativa. Meça grosseiramente a diferença de tempo para n = 40 (dica: System.Diagnostics.Stopwatch).
  4. Reescreva com switch expression: um método que recebe um código de status HTTP (200, 201, 400, 401, 404, 500) e retorna a descrição.
Ver solução do exercício 4
static string DescricaoHttp(int status) => status switch
{
    200 => "OK",
    201 => "Created",
    400 => "Bad Request",
    401 => "Unauthorized",
    404 => "Not Found",
    500 => "Internal Server Error",
    _   => $"Status {status} não mapeado"
};
[Capitulo(4, Nivel.Fundamentos)]

Coleções essenciais

Escolher a coleção certa é decisão diária de trabalho e tema recorrente em entrevistas (com perguntas de complexidade Big-O). As quatro que resolvem 95% dos problemas:

Array — tamanho fixo, máxima performance

int[] notas = new int[3];         // [0, 0, 0]
int[] primos = { 2, 3, 5, 7 };    // inicialização direta
primos[0] = 11;
Console.WriteLine(primos.Length); // 4

// Matriz (2D) — comum em jogos (grids, tabuleiros)
int[,] tabuleiro = new int[8, 8];
tabuleiro[0, 0] = 1;

// Array de arrays (jagged) — linhas de tamanhos diferentes
int[][] triangulo = { new[]{1}, new[]{1,1}, new[]{1,2,1} };

List<T> — a coleção padrão do dia a dia

var tarefas = new List<string> { "Estudar C#", "Fazer portfólio" };
tarefas.Add("Aplicar para vagas");
tarefas.Insert(0, "Café");             // insere na posição 0
tarefas.Remove("Café");                // remove por valor
tarefas.RemoveAt(0);                   // remove por índice
bool tem = tarefas.Contains("Fazer portfólio");
Console.WriteLine(tarefas.Count);      // Count, não Length
tarefas.Sort();

// Busca com predicado (antecipa lambdas do cap. 9)
string? achada = tarefas.Find(t => t.StartsWith("Aplicar"));

Dictionary<TKey,TValue> — busca por chave em O(1)

var estoque = new Dictionary<string, int>
{
    ["teclado"] = 15,
    ["mouse"]   = 40
};
estoque["monitor"] = 8;               // adiciona ou substitui
estoque["mouse"]++;                   // atualiza

// Acesso seguro: TryGetValue (indexer lança exceção se a chave não existe!)
if (estoque.TryGetValue("webcam", out int qtd))
    Console.WriteLine(qtd);
else
    Console.WriteLine("Sem webcam no estoque");

foreach (var (produto, quantidade) in estoque)
    Console.WriteLine($"{produto}: {quantidade}");

HashSet<T>, Queue<T> e Stack<T>

// HashSet: valores únicos, Contains em O(1)
var emails = new HashSet<string>();
emails.Add("a@x.com");
emails.Add("a@x.com");           // ignorado — já existe
Console.WriteLine(emails.Count); // 1

// Queue: FIFO — filas de processamento, mensageria
var fila = new Queue<string>();
fila.Enqueue("pedido-1");
fila.Enqueue("pedido-2");
Console.WriteLine(fila.Dequeue());   // pedido-1

// Stack: LIFO — undo, navegação, parsing
var pilha = new Stack<string>();
pilha.Push("tela-inicial");
pilha.Push("tela-detalhe");
Console.WriteLine(pilha.Pop());      // tela-detalhe

Complexidade: o que a entrevista espera

OperaçãoArray / ListDictionary / HashSet
Acesso por índice/chaveO(1)O(1) médio
Busca por valor (Contains)O(n)O(1) médio
Inserir no fimO(1) amortizadoO(1) médio
Inserir no início/meioO(n) — desloca elementos
Remover por valorO(n)O(1) médio
Cai na entrevista

"Como você removeria duplicatas de uma lista e por quê?" — Resposta forte: converter para HashSet (O(n)) ou usar lista.Distinct() do LINQ; explicar que verificar duplicata com List.Contains dentro de um loop vira O(n²) e não escala. Mostrar consciência de complexidade vale mais que decorar sintaxe.

Armadilha

Nunca modifique uma coleção dentro de um foreach sobre ela — lança InvalidOperationException. Para remover itens em massa use lista.RemoveAll(x => condicao), ou itere com for de trás para frente.

Exercícios · Capítulo 4
  1. Leia palavras do usuário até ele digitar "fim" e mostre quantas palavras distintas foram digitadas.
  2. Conte a frequência de cada caractere de uma string usando Dictionary<char,int> (pergunta real de triagem técnica).
  3. Simule uma fila de atendimento: comandos "chegar <nome>", "atender" e "listar" no console, usando Queue<string>.
  4. Verifique se uma expressão com parênteses/colchetes/chaves está balanceada — ex.: "{[()]}" é válida, "([)]" não — usando Stack<char>. Clássico absoluto de entrevista.
Ver solução do exercício 4 (balanceamento)
static bool EstaBalanceada(string expr)
{
    var pares = new Dictionary<char, char> { [')']='(', [']']='[', ['}']='{' };
    var pilha = new Stack<char>();

    foreach (char c in expr)
    {
        if (c is '(' or '[' or '{')
            pilha.Push(c);
        else if (pares.TryGetValue(c, out char abertura))
        {
            if (pilha.Count == 0 || pilha.Pop() != abertura)
                return false;
        }
    }
    return pilha.Count == 0;
}
Parte II

Orientação a Objetos

O coração do C#. Frameworks inteiros (ASP.NET, Unity, WPF) são construídos sobre estes conceitos — e entrevistas para qualquer nível cobram POO com profundidade.

[Capitulo(5, Nivel.POO)]

Classes, objetos e propriedades

Uma classe é o molde; um objeto é uma instância viva desse molde no heap. O objetivo da POO é juntar dados e comportamento que pertencem à mesma coisa, escondendo detalhes internos (encapsulamento).

public class ContaBancaria
{
    // Campo privado: estado interno, invisível de fora
    private decimal _saldo;

    // Propriedades: a "porta de entrada" controlada para os dados
    public string Titular { get; }                 // somente leitura pós-construção
    public decimal Saldo => _saldo;                // propriedade calculada (get puro)
    public int Transacoes { get; private set; }    // leitura pública, escrita interna

    // Construtor: garante que o objeto já nasce válido
    public ContaBancaria(string titular, decimal saldoInicial = 0)
    {
        if (string.IsNullOrWhiteSpace(titular))
            throw new ArgumentException("Titular é obrigatório.", nameof(titular));

        Titular = titular;
        _saldo = saldoInicial;
    }

    // Comportamento: métodos que protegem as regras de negócio
    public void Depositar(decimal valor)
    {
        if (valor <= 0)
            throw new ArgumentOutOfRangeException(nameof(valor), "Depósito deve ser positivo.");
        _saldo += valor;
        Transacoes++;
    }

    public bool TentarSacar(decimal valor)
    {
        if (valor <= 0 || valor > _saldo) return false;
        _saldo -= valor;
        Transacoes++;
        return true;
    }
}

// Uso:
var conta = new ContaBancaria("Ana", 100m);
conta.Depositar(50m);
// conta._saldo = 1_000_000m;   // ERRO: campo privado — encapsulamento em ação
Console.WriteLine($"{conta.Titular}: {conta.Saldo:C}");   // Ana: R$ 150,00
Boa prática

O ponto do encapsulamento não é "esconder por esconder": é garantir que não existe estado inválido. Se o saldo só muda por Depositar/TentarSacar, nenhum lugar do sistema consegue corrompê-lo. Em code review, classes com tudo public set são apontadas como "modelos anêmicos".

Propriedades: as formas que você verá

public class Produto
{
    public string Nome { get; set; }              // auto-propriedade comum
    public decimal Preco { get; private set; }    // set restrito à classe
    public string Sku { get; init; }              // init: só na criação do objeto
    public decimal PrecoComImposto => Preco * 1.1m;  // calculada

    private string _descricao = "";
    public string Descricao                        // propriedade "completa" com validação
    {
        get => _descricao;
        set => _descricao = value?.Trim() ?? "";
    }
}

var p = new Produto { Nome = "Teclado", Sku = "TEC-01" };  // inicializador de objeto
// p.Sku = "OUTRO";   // ERRO: init só permite atribuir na inicialização

static: membros da classe, não do objeto

public class GeradorDeId
{
    private static int _proximo = 1;                  // compartilhado por TODAS as instâncias
    public static int Proximo() => _proximo++;
    public static readonly DateTime Inicio = DateTime.Now;  // readonly: fixado 1x
}

int a = GeradorDeId.Proximo();   // chama pela CLASSE, sem new
int b = GeradorDeId.Proximo();   // 2
Armadilha

Estado static mutável é global compartilhado: em aplicações web (muitas requisições simultâneas) causa condições de corrida, e em testes causa vazamento de estado entre casos. Use para constantes, utilitários puros e caches conscientes — não como atalho para "acessar de qualquer lugar".

this, sobrecarga de construtores e desalocação

public class Retangulo
{
    public double Largura { get; }
    public double Altura { get; }

    public Retangulo(double lado) : this(lado, lado) { }   // encadeia construtores

    public Retangulo(double largura, double altura)
    {
        Largura = largura;
        Altura = altura;
    }
}

Você não destrói objetos manualmente: quando nada mais referencia um objeto, o Garbage Collector o coleta (capítulo 14). Para recursos externos (arquivos, conexões) existe IDisposable e using — capítulo 11.

Cai na entrevista

"O que é encapsulamento e por que importa?" — Não responda só "esconder dados". Resposta forte: é expor operações em vez de estado, de modo que as invariantes do objeto (ex.: saldo nunca negativo) sejam garantidas pela própria classe, reduzindo a superfície de bugs e permitindo mudar a implementação interna sem quebrar quem usa.

Exercícios · Capítulo 5
  1. Modele Jogador para um jogo: propriedades Nome, Vida (0–100, nunca fora da faixa), método ReceberDano(int) e Curar(int), e propriedade calculada EstaVivo. Nenhum código externo pode colocar a vida em valor inválido.
  2. Crie CarrinhoDeCompras com lista interna privada de itens, métodos Adicionar, Remover e propriedade Total em decimal. Exponha os itens só como IReadOnlyList<Item>.
  3. Implemente um contador static de instâncias criadas de uma classe qualquer (incrementado no construtor).
Ver solução do exercício 1
public class Jogador
{
    public string Nome { get; }
    public int Vida { get; private set; } = 100;
    public bool EstaVivo => Vida > 0;

    public Jogador(string nome) => Nome = nome;

    public void ReceberDano(int dano)
    {
        if (dano < 0) throw new ArgumentOutOfRangeException(nameof(dano));
        Vida = Math.Max(0, Vida - dano);
    }

    public void Curar(int pontos)
    {
        if (pontos < 0) throw new ArgumentOutOfRangeException(nameof(pontos));
        if (!EstaVivo) return;                 // regra: morto não cura
        Vida = Math.Min(100, Vida + pontos);
    }
}
[Capitulo(6, Nivel.POO)]

Herança, interfaces e polimorfismo

Herança e virtual/override

public class Funcionario
{
    public string Nome { get; }
    public decimal SalarioBase { get; }

    public Funcionario(string nome, decimal salarioBase)
    {
        Nome = nome;
        SalarioBase = salarioBase;
    }

    // virtual: PERMITE que classes filhas substituam
    public virtual decimal CalcularSalario() => SalarioBase;
}

public class Gerente : Funcionario           // Gerente É UM Funcionario
{
    public decimal Bonus { get; }

    public Gerente(string nome, decimal salarioBase, decimal bonus)
        : base(nome, salarioBase)            // chama o construtor do pai
    {
        Bonus = bonus;
    }

    public override decimal CalcularSalario()
        => base.CalcularSalario() + Bonus;   // reaproveita e estende
}

Polimorfismo: o mesmo código, comportamentos diferentes

List<Funcionario> folha = new()
{
    new Funcionario("Bia", 3000m),
    new Gerente("Caio", 8000m, 2000m)
};

foreach (var f in folha)
    Console.WriteLine($"{f.Nome}: {f.CalcularSalario():C}");
// Bia: R$ 3.000,00
// Caio: R$ 10.000,00  ← chamou o override do Gerente!

Isso é despacho dinâmico: a chamada CalcularSalario() decide em tempo de execução qual implementação rodar, com base no tipo real do objeto. É o mecanismo que permite escrever código genérico que funciona para tipos que nem existem ainda.

Classes abstratas

public abstract class Forma                    // não pode ser instanciada
{
    public abstract double Area();             // SEM corpo: filhas SÃO OBRIGADAS a implementar

    public void Descrever()                    // método concreto compartilhado
        => Console.WriteLine($"{GetType().Name}: área {Area():F2}");
}

public class Circulo : Forma
{
    public double Raio { get; init; }
    public override double Area() => Math.PI * Raio * Raio;
}

public class Quadrado : Forma
{
    public double Lado { get; init; }
    public override double Area() => Lado * Lado;
}

Interfaces: contratos sem implementação

public interface INotificador
{
    void Enviar(string destinatario, string mensagem);
}

public class NotificadorEmail : INotificador
{
    public void Enviar(string destinatario, string mensagem)
        => Console.WriteLine($"[EMAIL para {destinatario}] {mensagem}");
}

public class NotificadorSms : INotificador
{
    public void Enviar(string destinatario, string mensagem)
        => Console.WriteLine($"[SMS para {destinatario}] {mensagem}");
}

// Quem usa depende do CONTRATO, não da implementação:
public class ServicoDePedidos
{
    private readonly INotificador _notificador;
    public ServicoDePedidos(INotificador notificador) => _notificador = notificador;

    public void Confirmar(string cliente)
        => _notificador.Enviar(cliente, "Pedido confirmado!");
}

var servico = new ServicoDePedidos(new NotificadorSms());   // trocável sem tocar na classe
servico.Confirmar("11-99999-0000");

Esse padrão — depender de interface e receber a implementação pelo construtor — é a semente da injeção de dependência (capítulo 17) e está em todo projeto ASP.NET Core profissional. Uma classe pode implementar várias interfaces, mas herdar de uma única classe.

Cai na entrevista

"Classe abstrata ou interface — quando usar cada uma?" — Classe abstrata quando existe código e estado compartilhado entre as filhas e uma relação forte "é um" (Circulo é uma Forma). Interface quando você quer só um contrato de capacidade ("sabe notificar", "sabe persistir"), possivelmente implementado por tipos sem parentesco. No design moderno, a preferência é por interfaces + composição, reservando herança para hierarquias realmente naturais. Mencione: interfaces permitem múltipla implementação; C# 8+ permite default interface methods, mas o uso é raro.

sealed, is, as e casting

public sealed class ConfiguracaoFinal { }   // ninguém pode herdar dela

Funcionario f = new Gerente("Dani", 9000m, 1500m);   // upcast: implícito e seguro

// Downcast: precisa de verificação
if (f is Gerente g)                          // pattern matching: testa E converte
    Console.WriteLine($"Bônus: {g.Bonus}");

Gerente? talvez = f as Gerente;              // as: retorna null se não for (sem exceção)
Gerente forcado = (Gerente)f;                // cast direto: InvalidCastException se falhar
Armadilha

Se seu código está cheio de if (obj is TipoX), provavelmente falta polimorfismo: o comportamento deveria estar em um método virtual/de interface que cada tipo implementa. Cadeias de type-check são apontadas em review como violação de OCP (capítulo 16).

object, ToString e Equals

// Todo tipo herda de object: ToString, Equals, GetHashCode, GetType
public class Ponto
{
    public int X { get; init; }
    public int Y { get; init; }

    public override string ToString() => $"({X}, {Y})";

    public override bool Equals(object? obj)
        => obj is Ponto p && p.X == X && p.Y == Y;

    public override int GetHashCode() => HashCode.Combine(X, Y);
}
// Regra de ouro: sobrescreveu Equals? Sobrescreva GetHashCode junto,
// ou Dictionary/HashSet quebram silenciosamente.
// (No capítulo 7, records fazem isso automaticamente.)
Exercícios · Capítulo 6
  1. Modele um sistema de formas com Forma abstrata (com Area() e Perimetro()), implemente Circulo, Retangulo e Triangulo, e escreva um método que recebe List<Forma> e retorna a soma das áreas.
  2. Crie IRepositorio<T>-like simplificado: interface IArmazenamento com Salvar(string) e duas implementações — em memória (lista) e em arquivo (File.AppendAllText). Escreva uma classe que usa a interface sem saber qual implementação recebeu.
  3. Explique em comentários por que List<Gerente> não pode ser atribuída a List<Funcionario> (pesquise: variância). Guarde a pergunta — o capítulo 8 responde.
Ver solução do exercício 2
public interface IArmazenamento
{
    void Salvar(string dado);
}

public class ArmazenamentoMemoria : IArmazenamento
{
    public List<string> Dados { get; } = new();
    public void Salvar(string dado) => Dados.Add(dado);
}

public class ArmazenamentoArquivo : IArmazenamento
{
    private readonly string _caminho;
    public ArmazenamentoArquivo(string caminho) => _caminho = caminho;
    public void Salvar(string dado)
        => File.AppendAllText(_caminho, dado + Environment.NewLine);
}

public class RegistradorDeEventos
{
    private readonly IArmazenamento _armazenamento;   // depende do contrato
    public RegistradorDeEventos(IArmazenamento a) => _armazenamento = a;

    public void Registrar(string evento)
        => _armazenamento.Salvar($"{DateTime.Now:O} | {evento}");
}
[Capitulo(7, Nivel.POO)]

Structs, enums e records

struct: tipo de valor definido por você

public struct Vector2D          // em jogos, vetores são structs (Unity: Vector2/Vector3)
{
    public float X;
    public float Y;

    public Vector2D(float x, float y) { X = x; Y = y; }

    public float Magnitude => MathF.Sqrt(X * X + Y * Y);

    public static Vector2D operator +(Vector2D a, Vector2D b)   // sobrecarga de operador
        => new(a.X + b.X, a.Y + b.Y);
}

Vector2D v1 = new(3, 4);
Vector2D v2 = v1;        // CÓPIA COMPLETA — são independentes
v2.X = 100;
Console.WriteLine(v1.X); // 3

Structs são copiados a cada atribuição/passagem e não geram alocação no heap quando usados localmente — por isso dominam código de alta performance e engines de jogos. Regras práticas: use struct para dados pequenos (≤ ~16 bytes), imutáveis, que representam um valor (coordenada, dinheiro, intervalo). Para o resto, classe.

Armadilha

Structs mutáveis causam bugs sutis: você altera uma cópia achando que alterou o original (clássico: modificar um struct retornado por uma propriedade — a mudança se perde). Prefira readonly struct com propriedades init/somente leitura.

enum: conjunto fechado de opções

public enum StatusPedido
{
    Criado = 0,
    Pago = 1,
    Enviado = 2,
    Entregue = 3,
    Cancelado = 99
}

var status = StatusPedido.Pago;
if (status == StatusPedido.Pago)
    Console.WriteLine("Liberar envio");

// Conversões e parsing
int codigo = (int)status;                            // 1
var lido = (StatusPedido)2;                          // Enviado
Enum.TryParse<StatusPedido>("Entregue", out var s);  // parse seguro de texto

// [Flags]: combinações com bits — comum em permissões e em jogos (layers)
[Flags]
public enum Permissoes
{
    Nenhuma = 0,
    Ler     = 1,
    Editar  = 2,
    Excluir = 4,
    Admin   = Ler | Editar | Excluir
}
var p = Permissoes.Ler | Permissoes.Editar;
bool podeEditar = p.HasFlag(Permissoes.Editar);      // true
Boa prática

Defina os valores numéricos explicitamente quando o enum é persistido (banco, API): se alguém reordenar os membros, os números não podem mudar. E trate sempre o caso "valor desconhecido" ao converter de int — (StatusPedido)42 compila e não lança exceção!

record: dados imutáveis com igualdade por valor

Records (C# 9+) são o jeito moderno de modelar dados — DTOs de APIs, mensagens, resultados. Eles geram automaticamente construtor, propriedades init, Equals/GetHashCode por valor, ToString legível e suporte a cópia com with:

public record ClienteDto(int Id, string Nome, string Email);

var c1 = new ClienteDto(1, "Ana", "ana@x.com");
var c2 = new ClienteDto(1, "Ana", "ana@x.com");

Console.WriteLine(c1 == c2);        // True! Igualdade por VALOR (classe daria False)
Console.WriteLine(c1);             // ClienteDto { Id = 1, Nome = Ana, Email = ana@x.com }

// Imutável: para "alterar", crie uma cópia modificada
var c3 = c1 with { Email = "novo@x.com" };

// Record com corpo, validação e membros extras:
public record Dinheiro(decimal Valor, string Moeda)
{
    public decimal Valor { get; init; } =
        Valor >= 0 ? Valor : throw new ArgumentOutOfRangeException(nameof(Valor));

    public Dinheiro Somar(Dinheiro outro) =>
        outro.Moeda == Moeda
            ? this with { Valor = Valor + outro.Valor }
            : throw new InvalidOperationException("Moedas diferentes.");
}
// Existe também record struct (tipo de valor) para dados pequenos:
public readonly record struct Coordenada(double Lat, double Lon);
Cai na entrevista

"Class, struct ou record — como escolher?"class: entidades com identidade e comportamento, tempo de vida longo, mutáveis. struct: valores pequenos e imutáveis onde performance/semântica de cópia importam. record: dados imutáveis comparados por conteúdo (DTOs, eventos, configurações). Bônus: cite que record por padrão é classe (referência) e que existe record struct.

No mercado

Em APIs ASP.NET Core modernas, requests e responses são quase sempre records — imutabilidade elimina uma família inteira de bugs. Em Unity, você viverá de structs (Vector3, Quaternion, Color) e precisa saber que alterá-los exige reatribuir: transform.position += delta; em vez de mexer no .x de uma cópia.

Exercícios · Capítulo 7
  1. Crie readonly struct Temperatura com valor em Celsius e propriedades calculadas para Fahrenheit e Kelvin, mais operadores + e -.
  2. Modele enum DiaSemana e escreva um método que, dado um dia, retorna se é útil ou fim de semana com switch expression.
  3. Crie o record Endereco(string Rua, string Cidade, string Uf) e demonstre: igualdade por valor entre duas instâncias idênticas e cópia com with mudando só a cidade.
  4. Implemente [Flags] enum EstadoInimigo (Patrulhando, Alerta, Atacando, Atordoado) e mostre como ativar, testar e desativar um estado com operadores bit a bit (|, &, ~).
Ver solução do exercício 4
[Flags]
public enum EstadoInimigo
{
    Nenhum      = 0,
    Patrulhando = 1 << 0,   // 1
    Alerta      = 1 << 1,   // 2
    Atacando    = 1 << 2,   // 4
    Atordoado   = 1 << 3    // 8
}

var estado = EstadoInimigo.Patrulhando;

estado |= EstadoInimigo.Alerta;                 // ativa Alerta
bool emAlerta = (estado & EstadoInimigo.Alerta) != 0;   // testa
estado &= ~EstadoInimigo.Patrulhando;           // desativa Patrulhando

Console.WriteLine(estado);      // Alerta
Parte III

C# Intermediário

Os recursos que separam quem "sabe a sintaxe" de quem escreve C# idiomático: genéricos, delegates, LINQ, exceções e o sistema de null safety moderno.

[Capitulo(8, Nivel.Intermediario)]

Genéricos

Genéricos permitem escrever código que funciona para qualquer tipo, mantendo segurança de tipos e sem custo de conversões. Você já usa desde o capítulo 4 (List<T>, Dictionary<K,V>); agora vai criar os seus.

// Sem genéricos (o passado): object + casts + boxing = lento e inseguro
// Com genéricos: T é um "parâmetro de tipo" resolvido em compilação

public class Caixa<T>
{
    private T _conteudo;
    public void Guardar(T item) => _conteudo = item;
    public T Abrir() => _conteudo;
}

var caixaInt = new Caixa<int>();
caixaInt.Guardar(42);
int n = caixaInt.Abrir();          // sem cast, sem risco

var caixaStr = new Caixa<string>();
caixaStr.Guardar("currículo.pdf");
// caixaStr.Guardar(123);          // ERRO de compilação — segurança de tipos

Métodos genéricos e inferência

static void Trocar<T>(ref T a, ref T b)
{
    (a, b) = (b, a);               // tuplas tornam swap elegante
}

int x = 1, y = 2;
Trocar(ref x, ref y);              // T inferido como int — não precisa escrever <int>

static T PrimeiroOuPadrao<T>(IReadOnlyList<T> lista, T padrao)
    => lista.Count > 0 ? lista[0] : padrao;

Constraints: exigindo capacidades de T

// where restringe quais tipos podem ser usados — e libera membros de T
public interface IEntidade
{
    int Id { get; }
}

public class RepositorioMemoria<T> where T : class, IEntidade, new()
//                                        │       │            └ tem construtor vazio
//                                        │       └ implementa IEntidade (posso usar .Id)
//                                        └ é tipo de referência
{
    private readonly Dictionary<int, T> _dados = new();

    public void Salvar(T entidade) => _dados[entidade.Id] = entidade;
    public T? BuscarPorId(int id) => _dados.GetValueOrDefault(id);
    public IReadOnlyCollection<T> Todos() => _dados.Values;
}

// Constraints comuns:
// where T : struct          → tipo de valor
// where T : class           → tipo de referência
// where T : new()           → construtor público sem parâmetros
// where T : MinhaBase       → herda de MinhaBase
// where T : IComparable<T>  → implementa a interface (permite CompareTo)
// Exemplo clássico com IComparable — "maior elemento" para qualquer tipo comparável
static T Maior<T>(IEnumerable<T> itens) where T : IComparable<T>
{
    T maior = itens.First();
    foreach (var item in itens)
        if (item.CompareTo(maior) > 0)
            maior = item;
    return maior;
}
Console.WriteLine(Maior(new[] { 3, 9, 4 }));          // 9
Console.WriteLine(Maior(new[] { "ana", "zoe" }));      // zoe

Variância: out e in em interfaces (a resposta do cap. 6)

// Por que List<Gerente> não é List<Funcionario>?
// Porque List permite ESCREVER: você poderia adicionar um Estagiario
// numa lista que na verdade é de Gerentes. O compilador impede.

// IEnumerable<out T> é COVARIANTE (só "sai" T — leitura):
IEnumerable<Gerente> gerentes = new List<Gerente>();
IEnumerable<Funcionario> funcs = gerentes;      // OK! Ler Gerente como Funcionario é seguro

// Action<in T> é CONTRAVARIANTE (só "entra" T):
Action<Funcionario> processar = f => Console.WriteLine(f.Nome);
Action<Gerente> processarGerente = processar;   // OK! Quem processa qualquer Funcionario
                                                 // processa um Gerente
Cai na entrevista

"Por que genéricos são melhores que usar object?" — Três pontos: (1) segurança em compilação, sem casts em runtime; (2) sem boxing de tipos de valor (colocar um int em object aloca no heap — capítulo 14); (3) melhor legibilidade e IntelliSense. Perguntas de covariância/contravariância aparecem em vagas plenas/sêniores — saber o exemplo do IEnumerable<out T> já resolve a maioria.

Exercícios · Capítulo 8
  1. Implemente Pilha<T> do zero (com array interno que dobra de tamanho quando enche): Push, Pop, Peek, Count.
  2. Crie static T? MaiorOuNull<T>(IEnumerable<T> itens) que retorna null/default para coleção vazia, com constraint adequada.
  3. Estenda o RepositorioMemoria<T> com Remover(int id) e bool Existe(int id), e use-o com duas entidades diferentes (ex.: Cliente e Produto) para sentir o reuso.
Ver solução do exercício 1
public class Pilha<T>
{
    private T[] _itens = new T[4];
    public int Count { get; private set; }

    public void Push(T item)
    {
        if (Count == _itens.Length)
            Array.Resize(ref _itens, _itens.Length * 2);
        _itens[Count++] = item;
    }

    public T Pop()
    {
        if (Count == 0) throw new InvalidOperationException("Pilha vazia.");
        T topo = _itens[--Count];
        _itens[Count] = default!;   // libera referência p/ o GC
        return topo;
    }

    public T Peek() => Count > 0 ? _itens[Count - 1]
        : throw new InvalidOperationException("Pilha vazia.");
}
[Capitulo(9, Nivel.Intermediario)]

Delegates, eventos e lambdas

Um delegate é um tipo que representa referências a métodos — funções viram valores que você guarda em variáveis, passa como parâmetro e chama depois. É a fundação do LINQ, dos eventos de UI e de metade da API do Unity.

// Declarando um delegate (a "assinatura" que os métodos devem ter)
public delegate decimal CalculoDesconto(decimal preco);

static decimal DescontoBlackFriday(decimal preco) => preco * 0.5m;
static decimal DescontoCliente(decimal preco) => preco * 0.9m;

CalculoDesconto calculo = DescontoBlackFriday;   // método vira valor
Console.WriteLine(calculo(100m));                // 50 — chamando pela variável
calculo = DescontoCliente;
Console.WriteLine(calculo(100m));                // 90

Action, Func e Predicate — os delegates prontos

Na prática, ninguém declara delegates próprios para casos comuns; o .NET fornece genéricos prontos:

Action<string> log = msg => Console.WriteLine($"[LOG] {msg}");
//     └ recebe string, não retorna nada

Func<decimal, decimal, decimal> somar = (a, b) => a + b;
//    └ últimos tipo é o RETORNO: recebe 2 decimals, retorna decimal

Predicate<int> ehPar = n => n % 2 == 0;   // recebe T, retorna bool

log("iniciando");
Console.WriteLine(somar(10m, 5m));         // 15
Console.WriteLine(ehPar(4));               // True

Lambdas e closures

// Lambda: função anônima escrita inline
Func<int, int> quadrado = x => x * x;
Func<int, int, int> mult = (x, y) => x * y;
Action semParametro = () => Console.WriteLine("oi");
Func<int, int> comCorpo = x => { var d = x * 2; return d + 1; };

// CLOSURE: a lambda "captura" variáveis do escopo em que nasceu
int contador = 0;
Action incrementar = () => contador++;   // captura 'contador' por referência
incrementar();
incrementar();
Console.WriteLine(contador);             // 2 — a variável externa mudou!
Armadilha

Closure em loop é pegadinha clássica: capturar a variável do for faz todas as lambdas verem o valor final. Solução: copie para uma variável local dentro do loop (foreach moderno já cria uma variável por iteração). Em Unity, closures em código quente também geram alocações — atenção no capítulo 21.

Eventos: o padrão publisher/subscriber

public class Pedido
{
    // event: só a classe dona pode DISPARAR; externos só assinam (+=) e cancelam (-=)
    public event EventHandler<decimal>? PedidoPago;

    public void ConfirmarPagamento(decimal valor)
    {
        Console.WriteLine("Pagamento processado.");
        PedidoPago?.Invoke(this, valor);   // ?. evita NullReference se ninguém assinou
    }
}

var pedido = new Pedido();

// Vários assinantes reagem ao mesmo evento, desacoplados entre si:
pedido.PedidoPago += (sender, valor) => Console.WriteLine($"Emitir NF de {valor:C}");
pedido.PedidoPago += (sender, valor) => Console.WriteLine("Enviar e-mail ao cliente");

pedido.ConfirmarPagamento(250m);
// Pagamento processado.
// Emitir NF de R$ 250,00
// Enviar e-mail ao cliente
No mercado

Eventos estão em todo lugar: cliques em WPF/WinForms (botao.Click += ...), callbacks no Unity (Button.onClick, eventos de animação), e o padrão inspira mensageria em backend. Bug real de produção: esquecer de desassinar (-=) eventos de objetos de vida longa causa vazamento de memória — o publisher mantém o assinante vivo. Em Unity, desassine em OnDestroy/OnDisable.

Cai na entrevista

"Qual a diferença entre delegate e event?" — Um event é um delegate com encapsulamento: consumidores externos só podem +=/-=; apenas a classe declarante pode invocar ou sobrescrever a lista de assinantes. Sem event, qualquer um poderia fazer obj.Callback = null e apagar os assinantes dos outros.

Exercícios · Capítulo 9
  1. Escreva List<T> Filtrar<T>(List<T> itens, Func<T, bool> criterio) — você acabou de reimplementar o Where do LINQ.
  2. Crie uma classe SensorDeTemperatura com evento TemperaturaCritica disparado quando a leitura passa de 80°C; assine dois handlers (alarme e log).
  3. Demonstre a pegadinha da closure: crie 3 Actions em um for capturando i, execute-as e observe; depois corrija com variável local.
Ver solução do exercício 3
var acoes = new List<Action>();

for (int i = 0; i < 3; i++)
    acoes.Add(() => Console.Write(i + " "));
foreach (var a in acoes) a();     // 3 3 3  ← todas veem o i final!

acoes.Clear();
for (int i = 0; i < 3; i++)
{
    int copia = i;                 // uma variável NOVA por iteração
    acoes.Add(() => Console.Write(copia + " "));
}
foreach (var a in acoes) a();     // 0 1 2  ✓
[Capitulo(10, Nivel.Intermediario)]

LINQ

LINQ (Language Integrated Query) permite consultar coleções — e depois bancos de dados, via EF Core — com uma sintaxe declarativa. É provavelmente a habilidade de C# mais usada no dia a dia de backend, e entrevistas quase sempre incluem "resolva com LINQ".

using System.Linq;   // (implícito nos templates modernos)

var devs = new List<Dev>
{
    new("Ana",   "Backend", 5, 9500m),
    new("Bruno", "Games",   2, 4800m),
    new("Carla", "Backend", 8, 14000m),
    new("Diego", "Web",     1, 3500m),
    new("Elisa", "Games",   6, 11000m),
};
public record Dev(string Nome, string Area, int AnosXp, decimal Salario);

Os operadores essenciais

// Where: filtrar
var seniores = devs.Where(d => d.AnosXp >= 5);

// Select: projetar/transformar
var nomes = devs.Select(d => d.Nome);
var resumo = devs.Select(d => new { d.Nome, d.Area });   // tipo anônimo

// OrderBy / ThenBy / Descending: ordenar
var porSalario = devs.OrderByDescending(d => d.Salario)
                     .ThenBy(d => d.Nome);

// First / FirstOrDefault / Single / Any / All / Count
var primeiraBackend = devs.First(d => d.Area == "Backend");        // exceção se não achar
var talvezMobile    = devs.FirstOrDefault(d => d.Area == "Mobile"); // null se não achar
bool temJunior      = devs.Any(d => d.AnosXp < 2);
bool todosAcima3k   = devs.All(d => d.Salario > 3000m);
int qtdGames        = devs.Count(d => d.Area == "Games");

// Agregações
decimal folha = devs.Sum(d => d.Salario);
decimal media = devs.Average(d => d.Salario);
decimal maior = devs.Max(d => d.Salario);

// Encadeando tudo (pipeline típico de trabalho):
var top2BackendNomes = devs
    .Where(d => d.Area == "Backend")
    .OrderByDescending(d => d.Salario)
    .Take(2)
    .Select(d => d.Nome)
    .ToList();

GroupBy: relatórios em poucas linhas

var porArea = devs
    .GroupBy(d => d.Area)
    .Select(g => new
    {
        Area = g.Key,
        Quantidade = g.Count(),
        MediaSalarial = g.Average(d => d.Salario)
    })
    .OrderByDescending(x => x.MediaSalarial);

foreach (var grupo in porArea)
    Console.WriteLine($"{grupo.Area}: {grupo.Quantidade} dev(s), média {grupo.MediaSalarial:C}");

Execução adiada (deferred execution) — o conceito que reprova candidatos

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

var pares = numeros.Where(n => n % 2 == 0);   // NADA executa aqui — só define a consulta

numeros.Add(4);                                // modifica a fonte DEPOIS

foreach (var n in pares)                       // a consulta roda AGORA, na enumeração
    Console.Write(n + " ");                    // 2 4  ← pegou o 4!

// ToList()/ToArray() MATERIALIZAM: executam já e congelam o resultado
var paresAgora = numeros.Where(n => n % 2 == 0).ToList();
Armadilha

Enumerar a mesma query adiada várias vezes re-executa o pipeline inteiro (e, com EF Core, refaz a consulta no banco!). Se vai usar o resultado mais de uma vez, materialize com .ToList(). Por outro lado, materializar cedo demais uma consulta de banco traz dados demais para a memória — o equilíbrio é a marca do dev pleno.

Cai na entrevista

"Diferença entre IEnumerable<T> e IQueryable<T>?"IEnumerable executa em memória (LINQ to Objects, delegates compilados); IQueryable constrói uma árvore de expressão que um provedor (EF Core) traduz para SQL e executa no banco. Consequência prática: filtrar com Where num IQueryable vira WHERE no SQL; converter para IEnumerable cedo demais (ex.: .AsEnumerable() ou .ToList() antes do filtro) traz a tabela inteira e filtra na aplicação — bug de performance gravíssimo e muito comum.

Sintaxe de query e outros operadores úteis

// A mesma consulta em "query syntax" (menos comum, mas você verá em legado):
var consulta = from d in devs
               where d.AnosXp >= 5
               orderby d.Salario descending
               select d.Nome;

// Outros que aparecem no dia a dia:
var distintas  = devs.Select(d => d.Area).Distinct();
var paginacao  = devs.Skip(10).Take(10);                    // página 2 de 10 itens
var dicionario = devs.ToDictionary(d => d.Nome);            // índice por chave
var achatado   = new[] { new[]{1,2}, new[]{3,4} }
                   .SelectMany(x => x);                      // 1,2,3,4 (achata listas)
var zip        = new[]{"a","b"}.Zip(new[]{1,2}, (s,n) => $"{s}{n}");  // a1, b2
Exercícios · Capítulo 10
  1. Com a lista devs: encontre o dev mais bem pago de cada área (dica: GroupBy + MaxBy).
  2. Dada uma List<string> de palavras, retorne as 3 mais frequentes, em ordem decrescente de frequência (pergunta real de entrevista — resolva com GroupBy, OrderByDescending, Take).
  3. Demonstre a execução adiada: crie uma query Where com um Console.WriteLine dentro da lambda e observe quando ele imprime.
  4. Dado int[] numeros, retorne a soma dos quadrados dos pares em uma linha.
Ver soluções (2 e 4)
// 2 — top 3 palavras mais frequentes
var top3 = palavras
    .GroupBy(p => p.ToLower())
    .OrderByDescending(g => g.Count())
    .Take(3)
    .Select(g => new { Palavra = g.Key, Vezes = g.Count() })
    .ToList();

// 4 — soma dos quadrados dos pares
int soma = numeros.Where(n => n % 2 == 0).Sum(n => n * n);
[Capitulo(11, Nivel.Intermediario)]

Exceções e tratamento de erros

Exceções sinalizam condições excepcionais: algo que impede o método de cumprir o que promete. Saber quando lançar, quando capturar e — principalmente — quando não capturar é um dos maiores diferenciadores entre código júnior e profissional.

try
{
    string conteudo = File.ReadAllText("config.json");
    var config = System.Text.Json.JsonSerializer.Deserialize<Config>(conteudo);
}
catch (FileNotFoundException ex)             // do mais ESPECÍFICO...
{
    Console.WriteLine($"Arquivo ausente: {ex.FileName}");
}
catch (System.Text.Json.JsonException)
{
    Console.WriteLine("JSON inválido no arquivo de configuração.");
}
catch (Exception ex)                          // ...para o mais GENÉRICO (último recurso)
{
    Console.WriteLine($"Erro inesperado: {ex.Message}");
    throw;                                    // relança preservando o stack trace!
}
finally
{
    Console.WriteLine("Sempre executa — com ou sem exceção.");
}
Armadilha

throw;throw ex; — relançar com throw ex; apaga o stack trace original, destruindo a informação de onde o erro nasceu. Em produção isso transforma um bug de 10 minutos em uma investigação de dias. Use throw; sozinho, ou embrulhe: throw new AppException("contexto", ex); preservando a InnerException.

Lançando exceções e criando as suas

public void Transferir(Conta origem, Conta destino, decimal valor)
{
    // Validações de argumento: exceções padrão do .NET
    ArgumentNullException.ThrowIfNull(origem);
    ArgumentNullException.ThrowIfNull(destino);
    if (valor <= 0)
        throw new ArgumentOutOfRangeException(nameof(valor), "Valor deve ser positivo.");

    // Regra de negócio: exceção CUSTOMIZADA
    if (origem.Saldo < valor)
        throw new SaldoInsuficienteException(origem.Id, valor, origem.Saldo);

    origem.Debitar(valor);
    destino.Creditar(valor);
}

// Exceção customizada: herde de Exception, nome terminando em Exception,
// carregue dados úteis para diagnóstico
public class SaldoInsuficienteException : Exception
{
    public int ContaId { get; }
    public decimal ValorSolicitado { get; }
    public decimal SaldoAtual { get; }

    public SaldoInsuficienteException(int contaId, decimal solicitado, decimal saldo)
        : base($"Conta {contaId}: solicitado {solicitado:C}, saldo {saldo:C}.")
    {
        ContaId = contaId;
        ValorSolicitado = solicitado;
        SaldoAtual = saldo;
    }
}

As regras profissionais

  • Capture só o que você sabe tratar. catch (Exception) { } vazio ("engolir exceção") esconde bugs e é o erro mais criticado em code review.
  • Exceção não é fluxo de controle. Para casos esperados (entrada inválida do usuário), use validação/TryParse. Exceções são caras (captura de stack trace) e poluem a semântica.
  • Trate na borda. Em APIs, um middleware global captura, loga e converte para resposta HTTP adequada; o domínio só lança. Em apps desktop/jogos, um handler global evita crash e loga.
  • Log com contexto. Logar ex.ToString() (mensagem + stack + inner), nunca só ex.Message.

IDisposable e using: liberando recursos com garantia

// Recursos NÃO gerenciados pelo GC (arquivos, conexões, sockets) precisam
// de liberação determinística: Dispose(). O using garante isso mesmo com exceção.

using (var leitor = new StreamReader("dados.txt"))
{
    Console.WriteLine(leitor.ReadLine());
}   // Dispose() chamado aqui, SEMPRE

// Forma moderna (C# 8+): using declaration — descarta no fim do escopo
using var conexao = new System.Net.Http.HttpClient();

// Implementando na sua classe:
public class ArquivoDeLog : IDisposable
{
    private readonly StreamWriter _writer = new("app.log", append: true);
    public void Escrever(string msg) => _writer.WriteLine($"{DateTime.Now:O} {msg}");
    public void Dispose() => _writer.Dispose();
}
Cai na entrevista

"O que o bloco finally garante e para que serve using?"finally executa sempre (exceção ou não) e serve para limpeza; using é açúcar sintático para try/finally chamando Dispose(). Bônus sênior: exceções em .NET são unchecked (diferente do Java — nada obriga capturar), e StackOverflowException/corrupções de processo não são capturáveis.

Exercícios · Capítulo 11
  1. Crie EstoqueInsuficienteException com propriedades ProdutoId e QuantidadeDisponivel, e um método ReservarProduto que a lança.
  2. Escreva um programa que tenta ler um arquivo passado por argumento e trata separadamente: arquivo inexistente, sem permissão (UnauthorizedAccessException) e caminho inválido — com mensagens amigáveis distintas.
  3. Demonstre a diferença entre throw; e throw ex; imprimindo o stack trace nos dois casos.
  4. Implemente IDisposable em uma classe MedidorDeTempo que inicia um Stopwatch no construtor e imprime o tempo decorrido no Dispose — permitindo medir qualquer bloco com using var m = new MedidorDeTempo("nome");.
Ver solução do exercício 4
public sealed class MedidorDeTempo : IDisposable
{
    private readonly string _nome;
    private readonly System.Diagnostics.Stopwatch _sw;

    public MedidorDeTempo(string nome)
    {
        _nome = nome;
        _sw = System.Diagnostics.Stopwatch.StartNew();
    }

    public void Dispose()
    {
        _sw.Stop();
        Console.WriteLine($"[{_nome}] {_sw.ElapsedMilliseconds} ms");
    }
}

// Uso:
using (var m = new MedidorDeTempo("processamento"))
{
    Thread.Sleep(300);   // trabalho simulado
}
// [processamento] ~300 ms
[Capitulo(12, Nivel.Intermediario)]

Null safety e pattern matching

O NullReferenceException é historicamente o erro nº 1 em produção .NET. O C# moderno ataca isso com nullable reference types (NRT): com <Nullable>enable</Nullable> no .csproj (padrão nos templates novos), o compilador rastreia o que pode ser null.

#nullable enable

string nome = null;      // ⚠ AVISO do compilador: nome não é anulável
string? apelido = null;  // OK: o ? declara "pode ser null"

// O compilador exige verificação antes de usar:
Console.WriteLine(apelido.Length);      // ⚠ aviso: possível null
if (apelido != null)
    Console.WriteLine(apelido.Length);  // ok: fluxo analisado

// Operadores para trabalhar com null:
int? tamanho = apelido?.Length;          // ?.  navegação segura → null se apelido null
string exibicao = apelido ?? "(sem apelido)";   // ?? valor padrão
apelido ??= "anônimo";                   // ??= atribui só se for null
string forcado = apelido!;               // !  "confia em mim, não é null" (use raro!)
Boa prática

Trate os avisos de nullability como erros (<WarningsAsErrors>nullable</WarningsAsErrors>) em projetos novos. E desconfie do ! (null-forgiving): cada uso é uma aposta; em review, cada ! precisa de justificativa.

Nullable<T> para tipos de valor

int? idade = null;               // açúcar para Nullable<int>
if (idade.HasValue)
    Console.WriteLine(idade.Value);
Console.WriteLine(idade.GetValueOrDefault(-1));   // -1

// Muito comum em modelos de banco: colunas opcionais
public class Cliente
{
    public DateTime? UltimaCompra { get; set; }   // pode nunca ter comprado
}

Pattern matching: o canivete do C# moderno

// Type pattern + declaração
object obj = "texto";
if (obj is string s && s.Length > 3)
    Console.WriteLine(s.ToUpper());

// Property pattern: casa com a "forma" do objeto
public record Pedido(decimal Valor, string Uf, bool ClienteVip);

decimal CalcularFrete(Pedido p) => p switch
{
    { ClienteVip: true }            => 0m,
    { Valor: >= 300m }              => 0m,
    { Uf: "SP" or "RJ" }            => 15m,
    { Uf: "AM" or "RR" or "AC" }    => 60m,
    _                               => 30m
};

// Relational + logical patterns
string ClassificarIdade(int idade) => idade switch
{
    < 0        => "inválida",
    < 12       => "criança",
    >= 12 and < 18 => "adolescente",
    < 60       => "adulto",
    _          => "idoso"
};

// Tuple pattern: decisões com múltiplos valores
string JoKenPo(string a, string b) => (a, b) switch
{
    ("pedra", "tesoura") or ("tesoura", "papel") or ("papel", "pedra") => "jogador 1",
    var (x, y) when x == y => "empate",
    _ => "jogador 2"
};

// List patterns (C# 11)
int[] v = { 1, 2, 3, 4 };
string desc = v switch
{
    []              => "vazio",
    [var unico]     => $"um item: {unico}",
    [var p, .., var u] => $"começa {p}, termina {u}",
};
No mercado

Código novo em empresas atualizadas usa pattern matching intensamente — regras de negócio como a do frete acima ficam declarativas e fáceis de revisar. Reconhecer e escrever switch expressions com property patterns sinaliza que você não parou no C# de 2015.

Cai na entrevista

"Como o C# moderno reduz NullReferenceException?" — Fale de NRT (análise de fluxo em compilação, tipos anuláveis explícitos com ?), dos operadores ?., ??, ??=, e de padrões de projeto: validar na borda, nunca retornar null de coleções (retorne vazia), e usar tipos como TryXxx/resultados em vez de null "mágico".

Exercícios · Capítulo 12
  1. Reescreva com ?., ?? e no máximo 3 linhas: dado Cliente? com Endereco? que tem Cidade?, imprima a cidade ou "não informada".
  2. Implemente decimal CalcularDesconto(Cliente c) com switch expression e property patterns: VIP → 20%; mais de 10 compras → 10%; cadastro com mais de 5 anos → 5%; senão 0%.
  3. Com list patterns, escreva um método que descreve um comando splitado (ex.: ["mover", "10", "20"] → chama mover; ["sair"] → sai; qualquer outro → "comando desconhecido").
Ver solução do exercício 2
public record Cliente(bool Vip, int TotalCompras, DateTime Cadastro);

decimal CalcularDesconto(Cliente c) => c switch
{
    { Vip: true }                => 0.20m,
    { TotalCompras: > 10 }       => 0.10m,
    var x when (DateTime.Now - x.Cadastro).TotalDays > 5 * 365 => 0.05m,
    _                            => 0m
};
Parte IV

C# Avançado

Concorrência, gestão de memória e metaprogramação — os temas que definem entrevistas de nível pleno e sênior, e que explicam por que o código se comporta como se comporta.

[Capitulo(13, Nivel.Avancado)]

Async, await e concorrência

Este é, disparado, o tema avançado mais cobrado em entrevistas de backend. A ideia central: operações de I/O (banco, HTTP, disco) passam a maior parte do tempo esperando. Código síncrono bloqueia uma thread inteira durante essa espera; async/await libera a thread para servir outras requisições e retoma seu método quando o resultado chega.

// Assinatura async: retorna Task (sem valor) ou Task<T> (com valor)
public async Task<string> BaixarPaginaAsync(string url)
{
    using var http = new HttpClient();

    // await: "pausa" o método SEM bloquear a thread;
    // o resto vira uma continuação agendada para quando a Task completar
    string html = await http.GetStringAsync(url);

    return html.Length > 1000 ? "página grande" : "página pequena";
}

// Consumindo:
string resultado = await BaixarPaginaAsync("https://example.com");
Boa prática

Async "contamina" para cima — e deve mesmo. Se um método usa await, quem o chama também deve ser async, até chegar no topo (o ASP.NET Core aceita async Task em controllers; console aceita async Task Main). Sufixe métodos assíncronos com Async por convenção.

Paralelismo de Tasks: fazendo várias esperas ao mesmo tempo

// RUIM: sequencial — soma os tempos (3 chamadas de 1s = 3s)
var a = await BuscarClienteAsync(1);
var b = await BuscarClienteAsync(2);
var c = await BuscarClienteAsync(3);

// BOM: dispara as três, espera todas — tempo ≈ o da mais lenta (~1s)
Task<Cliente> t1 = BuscarClienteAsync(1);   // já começa a executar
Task<Cliente> t2 = BuscarClienteAsync(2);
Task<Cliente> t3 = BuscarClienteAsync(3);

Cliente[] clientes = await Task.WhenAll(t1, t2, t3);

// WhenAny: o primeiro que terminar (útil p/ timeout ou corrida entre fontes)
Task<Cliente> vencedora = await Task.WhenAny(t1, t2, t3);

Cancelamento: obrigatório em código profissional

public async Task ProcessarRelatorioAsync(CancellationToken ct)
{
    for (int i = 0; i < 100; i++)
    {
        ct.ThrowIfCancellationRequested();          // coopera com o cancelamento
        await Task.Delay(100, ct);                  // repasse o token para dentro!
        Console.WriteLine($"lote {i}");
    }
}

using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2));   // timeout
try
{
    await ProcessarRelatorioAsync(cts.Token);
}
catch (OperationCanceledException)
{
    Console.WriteLine("Cancelado (timeout ou usuário).");
}

No ASP.NET Core, cada requisição fornece um CancellationToken — se o cliente desconecta, sua consulta ao banco pode ser abortada em vez de desperdiçar recursos. Aceitar e repassar o token é marca de código maduro.

Os erros clássicos (e como entrevistadores os cobram)

// 1) BLOQUEAR sobre async: .Result / .Wait() — risco de DEADLOCK
var dado = BaixarPaginaAsync(url).Result;    // ❌ NUNCA em código com contexto (UI/ASP.NET clássico)
// Correto: await. Se não há como, repense o design.

// 2) async void — exceções irrecuperáveis, impossível de aguardar
public async void Processar() { ... }        // ❌ só para event handlers de UI
public async Task Processar() { ... }        // ✓

// 3) Esquecer o await — a Task roda "solta", exceções somem
_ = SalvarLogAsync();          // fire-and-forget CONSCIENTE exige tratamento próprio
await SalvarLogAsync();        // ✓ normal

// 4) Task.Run para I/O — desperdiça thread; Task.Run é para trabalho de CPU
var html = await Task.Run(() => http.GetStringAsync(url));  // ❌ redundante
var html2 = await http.GetStringAsync(url);                  // ✓
Cai na entrevista

"await bloqueia a thread?" — Não. Ele registra o resto do método como continuação e devolve a thread ao pool; quando a operação completa, a continuação é agendada. "Diferença entre concorrência assíncrona e paralelismo?" — Async trata de não bloquear durante I/O (pode até rodar em uma thread só); paralelismo (Parallel.For, PLINQ, múltiplas threads) usa vários núcleos para trabalho de CPU. "O que é ConfigureAwait(false)?" — Em bibliotecas, evita capturar o contexto de sincronização, prevenindo deadlocks e melhorando performance; em apps ASP.NET Core (sem SynchronizationContext) é menos crítico, mas em libs continua boa prática.

Threads, lock e estado compartilhado

// Quando VÁRIAS threads tocam o mesmo dado, há condição de corrida:
int contador = 0;
Parallel.For(0, 100_000, _ => contador++);
Console.WriteLine(contador);      // < 100000! (perdeu incrementos)

// Solução 1: lock — região crítica de acesso exclusivo
object trava = new();
int seguro = 0;
Parallel.For(0, 100_000, _ => { lock (trava) seguro++; });

// Solução 2: tipos atômicos/concorrentes
int atomico = 0;
Parallel.For(0, 100_000, _ => Interlocked.Increment(ref atomico));

var mapa = new System.Collections.Concurrent.ConcurrentDictionary<string, int>();
mapa.AddOrUpdate("chave", 1, (k, atual) => atual + 1);

// SemaphoreSlim: limitar concorrência (ex.: no máx. 5 chamadas simultâneas a uma API)
var semaforo = new SemaphoreSlim(5);
async Task ChamarComLimiteAsync()
{
    await semaforo.WaitAsync();
    try { await ChamarApiExternaAsync(); }
    finally { semaforo.Release(); }
}
Armadilha

List<T> e Dictionary<K,V> não são thread-safe: escrita concorrente corrompe a estrutura com erros intermitentes dificílimos de reproduzir. Compartilhou entre threads? Use Concurrent*, lock, ou — melhor — evite estado compartilhado mutável.

Exercícios · Capítulo 13
  1. Simule 3 "downloads" com Task.Delay de durações diferentes e compare o tempo total sequencial vs. Task.WhenAll (use Stopwatch).
  2. Adicione CancellationToken a um processamento em loop e cancele-o quando o usuário apertar Enter (dica: Console.ReadLine em outra Task).
  3. Reproduza a condição de corrida do contador e conserte de duas formas (lock e Interlocked).
  4. Escreva Task<string> BuscarComTimeoutAsync(Func<Task<string>> operacao, TimeSpan timeout) usando Task.WhenAny.
Ver solução do exercício 4
static async Task<string> BuscarComTimeoutAsync(
    Func<Task<string>> operacao, TimeSpan timeout)
{
    Task<string> tarefa = operacao();
    Task vencedora = await Task.WhenAny(tarefa, Task.Delay(timeout));

    if (vencedora != tarefa)
        throw new TimeoutException($"Operação excedeu {timeout.TotalSeconds}s.");

    return await tarefa;   // já completou; await propaga exceções se houver
}
[Capitulo(14, Nivel.Avancado)]

Memória, GC e performance

Você não gerencia memória manualmente em C# — mas quem entende como ela funciona escreve código mais rápido, evita vazamentos e responde as perguntas que separam plenos de sêniores (e, em jogos, evita os travamentos de GC que arruínam o frame rate).

Stack vs. heap, de verdade

  • Stack: memória por thread, alocação/liberação automática ao entrar/sair de métodos. Guarda variáveis locais de tipos de valor e referências para objetos. Rapidíssima.
  • Heap: onde vivem os objetos (instâncias de class, arrays, strings). Gerenciada pelo Garbage Collector.
void Exemplo()
{
    int x = 10;                    // stack: o valor 10
    var p = new Pessoa("Ana");     // stack: a referência p │ heap: o objeto Pessoa
    Ponto pt = new(1, 2);          // struct local: inteira na stack
    var lista = new List<Ponto>(); // heap: a lista; os Pontos ficam EMBUTIDOS no array interno
}   // sai do método: stack limpa na hora; o objeto Pessoa vira "lixo" quando
    // nenhuma referência mais o alcança — o GC coleta depois

Como o GC funciona: gerações

O GC do .NET é geracional, baseado na observação de que a maioria dos objetos morre jovem:

GeraçãoO que contémColeta
Gen 0Objetos recém-criadosFrequente e baratíssima
Gen 1Sobreviventes da Gen 0Intermediária
Gen 2Objetos de vida longa (caches, singletons)Rara e cara ("full GC")
LOHObjetos grandes (> 85 KB — arrays grandes, strings enormes)Junto da Gen 2; fragmenta

O ciclo: o GC marca tudo que ainda é alcançável a partir das raízes (variáveis ativas, statics), varre o resto e compacta a memória. Durante partes disso, as threads da aplicação pausam — em servidores isso vira latência; em jogos, stutter.

Boxing e unboxing: a alocação invisível

int n = 42;
object caixa = n;          // BOXING: aloca um objeto no heap e copia o int p/ dentro
int volta = (int)caixa;    // UNBOXING: cast + cópia de volta

// Onde acontece sem você ver:
ArrayList antiga = new();  // coleções pré-genéricos: tudo é object
antiga.Add(42);            // boxing!

string s = "valor: " + 42;                 // ok: compilador otimiza
Console.WriteLine("valor: {0}", 42);       // boxing! (parâmetro object)

IComparable comp = 42;                     // boxing: struct → interface
// Por isso genéricos importam: List<int> guarda ints "crus", sem boxing.

Vazamentos de memória em linguagem gerenciada? Sim.

O GC só coleta o inalcançável. Vazamento em .NET = referências que você esqueceu vivas:

  • Eventos não desassinados: publisher de vida longa segura assinantes para sempre (o clássico nº 1 — visto no cap. 9).
  • Caches/statics que só crescem: dicionário estático sem política de expiração.
  • IDisposable sem Dispose: handles de arquivo/conexão acumulando (não é exatamente GC, mas esgota recursos do SO).
  • Closures capturando objetos grandes guardadas em coleções duradouras.

Ferramentas de performance do C# moderno

// Span<T>: uma "janela" sobre memória existente, SEM alocar nem copiar
ReadOnlySpan<char> texto = "2026-07-07".AsSpan();
ReadOnlySpan<char> ano = texto[..4];                 // fatia sem criar string!
int anoNum = int.Parse(ano);                          // parse direto do span

// Comparação: Substring ALOCA uma nova string a cada chamada.
// Em um parser que roda milhões de vezes, isso é a diferença entre 10ms e 400ms.

// stackalloc: buffer pequeno direto na stack (zero GC)
Span<byte> buffer = stackalloc byte[64];

// StringBuilder para montagem de texto em loop (cap. 2)
// ArrayPool<T>: alugar arrays grandes em vez de alocar
var pool = System.Buffers.ArrayPool<byte>.Shared;
byte[] alugado = pool.Rent(4096);
try { /* usa */ } finally { pool.Return(alugado); }

// struct vs class na prática: um array de 1M de structs Ponto(x,y) é UM bloco
// contíguo de memória (cache-friendly, zero pressão de GC);
// 1M de classes Ponto são 1M de objetos espalhados + 1M de referências.
Boa prática

Meça antes de otimizar. A ferramenta padrão do ecossistema é o BenchmarkDotNet (micro-benchmarks confiáveis) e os profilers (dotnet-trace, dotMemory, o profiler do Visual Studio). Otimização guiada por intuição erra o alvo na maioria das vezes — dizer isso em entrevista, com nome de ferramenta, conta pontos.

Cai na entrevista

"Explique o GC do .NET." — Estruture: heap gerenciado; coleta por alcançabilidade (mark & sweep & compact); três gerações + LOH e por quê (hipótese geracional); modos workstation/server. "O que é boxing e por que evitar?" — Conversão de tipo de valor para object/interface: aloca no heap, copia, pressiona o GC; genéricos evitam. "Como você investigaria um vazamento de memória?" — Capturar dumps/snapshots, comparar objetos retidos entre snapshots, procurar raízes (statics, eventos), citar dotnet-counters/dotMemory.

Exercícios · Capítulo 14
  1. Escreva duas versões de um contador de vogais de uma string de 10 MB: uma com Substring/alocações e outra com ReadOnlySpan<char>; compare tempo e (se souber) alocações.
  2. Provoque um vazamento: uma classe Publicador estática com evento, e 10.000 assinantes criados em loop sem desassinar. Depois corrija com -= e explique.
  3. Demonstre boxing: adicione 1 milhão de ints em um ArrayList e em um List<int> e compare os tempos com Stopwatch.
[Capitulo(15, Nivel.Avancado)]

Reflection, attributes e metaprogramação

Reflection é a capacidade de inspecionar e manipular tipos em tempo de execução. Você raramente escreverá reflection do zero — mas os frameworks que você usa todo dia (ASP.NET, EF Core, serializadores JSON, Unity Inspector, xUnit) são construídos sobre ela, e entender isso desmistifica "a mágica".

using System.Reflection;

var tipo = typeof(Cliente);                    // metadados do tipo em compilação
var tipo2 = new Cliente().GetType();           // ou a partir da instância

Console.WriteLine(tipo.FullName);
foreach (PropertyInfo prop in tipo.GetProperties())
    Console.WriteLine($"{prop.PropertyType.Name} {prop.Name}");

// Criar instância e acessar membros dinamicamente (por nome!)
object obj = Activator.CreateInstance(tipo)!;
PropertyInfo nome = tipo.GetProperty("Nome")!;
nome.SetValue(obj, "Dinâmico");
Console.WriteLine(nome.GetValue(obj));         // "Dinâmico"

// É assim que um serializador JSON genérico consegue ler QUALQUER objeto
// sem conhecer seu tipo em compilação.
Armadilha

Reflection é lenta (ordens de magnitude vs. acesso direto) e quebra em compilação AOT/trimming (Native AOT, IL2CPP do Unity). Por isso o ecossistema migrou muita coisa para source generators — geração de código em compilação (ex.: System.Text.Json com JsonSerializerContext). Saber citar essa evolução é sinal de atualização.

Attributes: metadados declarativos

// Attributes anotam código; frameworks os leem via reflection.
// Você já viu vários sem perceber:

public class ClienteDto
{
    [Required(ErrorMessage = "Nome é obrigatório")]      // validação ASP.NET
    [StringLength(100)]
    public string Nome { get; set; } = "";

    [JsonPropertyName("email_principal")]                 // serialização JSON
    public string Email { get; set; } = "";
}

[Obsolete("Use CalcularV2")]                              // aviso de compilador
public decimal Calcular() => 0;

// Criando o SEU attribute:
[AttributeUsage(AttributeTargets.Property)]
public class AuditavelAttribute : Attribute
{
    public string Motivo { get; }
    public AuditavelAttribute(string motivo) => Motivo = motivo;
}

public class Pedido
{
    [Auditavel("valor financeiro sensível")]
    public decimal Total { get; set; }
}

// Lendo-o via reflection:
foreach (var prop in typeof(Pedido).GetProperties())
{
    var attr = prop.GetCustomAttribute<AuditavelAttribute>();
    if (attr != null)
        Console.WriteLine($"{prop.Name} é auditável: {attr.Motivo}");
}

Onde a "mágica" dos frameworks mora

FrameworkO que ele faz com reflection/attributes
ASP.NET CoreDescobre controllers, lê [HttpGet("rota")], [FromBody], valida [Required]
EF CoreMapeia classes → tabelas lendo propriedades e [Key], [Column]
xUnit/NUnitEncontra e executa métodos marcados com [Fact]/[Test]
Unity[SerializeField] expõe campos no Inspector; [Header], [Range] customizam
Injeção de dependênciaConstrói grafos de objetos descobrindo construtores

dynamic e árvores de expressão (visão geral)

// dynamic: desliga a checagem de tipos em COMPILAÇÃO (resolve em runtime)
dynamic d = "texto";
Console.WriteLine(d.Length);   // ok em runtime
d = 42;
// Console.WriteLine(d.Length); // RuntimeBinderException — só explode ao executar!
// Uso legítimo: interop (COM/Office, JSON dinâmico). Fora isso, evite.

// Expression<Func<...>>: o CÓDIGO como DADOS — é o que permite ao EF Core
// traduzir seu lambda para SQL:
System.Linq.Expressions.Expression<Func<int, bool>> expr = n => n > 10;
Console.WriteLine(expr.Body);   // (n > 10) — uma árvore inspecionável, não um delegate
Cai na entrevista

"Como o EF Core transforma Where(x => x.Idade > 18) em SQL?" — Porque IQueryable recebe Expression<Func<T,bool>>, não Func: o compilador entrega uma árvore de expressão que o provider percorre e traduz. Essa resposta conecta os capítulos 10 e 15 e impressiona em entrevistas plenas/sêniores.

Exercícios · Capítulo 15
  1. Escreva um "mini serializador": método que recebe object e retorna uma string Prop1=valor;Prop2=valor lendo as propriedades via reflection.
  2. Crie [ColunaAttribute(string nome)] e um método que gera um CREATE TABLE fake a partir de uma classe anotada.
  3. Implemente um mini test-runner: descubra em uma classe todos os métodos marcados com seu attribute [MeuTeste], execute-os e reporte quais lançaram exceção.
Ver solução do exercício 1
static string Serializar(object obj)
{
    var partes = obj.GetType()
        .GetProperties()
        .Select(p => $"{p.Name}={p.GetValue(obj) ?? "null"}");
    return string.Join(";", partes);
}

// Serializar(new { Nome = "Ana", Idade = 30 })  →  "Nome=Ana;Idade=30"
Parte V

Engenharia de Software

Escrever código que funciona é o começo; escrever código que times conseguem manter, testar e evoluir por anos é o que empresas pagam. Esta parte é o que mais aparece em entrevistas de nível pleno e sênior.

[Capitulo(16, Nivel.Engenharia)]

SOLID e design patterns

SOLID são cinco princípios de design orientado a objetos. Não são regras rígidas, e sim heurísticas para reduzir acoplamento e facilitar mudança. Saber recitá-los com exemplo é quase obrigatório em entrevistas plenas.

S — Single Responsibility (Responsabilidade Única)

Uma classe deve ter um único motivo para mudar. A classe abaixo faz três coisas — persistência, e-mail e regra de negócio — e muda por três motivos diferentes:

// ❌ Viola SRP: mistura cadastro, banco e e-mail
public class CadastroUsuario
{
    public void Registrar(string nome, string email)
    {
        if (!email.Contains('@')) throw new ArgumentException("E-mail inválido");
        // ... abre conexão, monta SQL, INSERT ...
        // ... conecta no servidor SMTP e envia boas-vindas ...
    }
}

// ✓ Cada responsabilidade em seu lugar
public class CadastroUsuario
{
    private readonly IUsuarioRepositorio _repo;
    private readonly INotificador _notificador;
    public CadastroUsuario(IUsuarioRepositorio repo, INotificador notificador)
        => (_repo, _notificador) = (repo, notificador);

    public void Registrar(string nome, string email)
    {
        var usuario = Usuario.Criar(nome, email);   // validação vive na entidade
        _repo.Adicionar(usuario);
        _notificador.Enviar(email, "Bem-vindo!");
    }
}

O — Open/Closed (Aberto/Fechado)

Aberto para extensão, fechado para modificação: adicionar comportamento novo não deveria exigir editar código existente e testado.

// ❌ Cada novo meio de pagamento obriga a mexer no switch (e re-testar tudo)
public decimal CalcularTaxa(string tipo, decimal valor) => tipo switch
{
    "pix"    => 0m,
    "credito" => valor * 0.03m,
    _ => throw new NotSupportedException()
};

// ✓ Novo meio = nova classe, sem tocar nas existentes
public interface IMeioPagamento { decimal CalcularTaxa(decimal valor); }
public class Pix : IMeioPagamento { public decimal CalcularTaxa(decimal v) => 0m; }
public class Credito : IMeioPagamento { public decimal CalcularTaxa(decimal v) => v * 0.03m; }
// Adicionar Boleto amanhã não quebra Pix nem Credito.

L — Liskov Substitution (Substituição de Liskov)

Um subtipo deve poder substituir seu tipo base sem quebrar expectativas. O exemplo canônico do "quadrado que herda de retângulo":

// ❌ Quadrado "é um" Retângulo? Ao forçar lados iguais, quebra quem espera
//    poder mudar largura e altura independentemente.
public class Retangulo { public virtual int Largura { get; set; } public virtual int Altura { get; set; } }
public class Quadrado : Retangulo
{
    public override int Largura { set { base.Largura = base.Altura = value; } }
    // Código que faz r.Largura = 5; r.Altura = 4; e espera área 20 recebe 16. Surpresa = bug.
}
// ✓ Se o subtipo não honra o contrato do pai, a herança está errada:
//   prefira uma abstração comum (IForma) em vez de herança forçada.

I — Interface Segregation (Segregação de Interface)

Muitas interfaces pequenas e específicas são melhores que uma "gorda". Ninguém deve ser forçado a implementar métodos que não usa.

// ❌ Interface obesa
public interface IMaquina { void Imprimir(); void Escanear(); void Faxear(); }
// Uma impressora simples é obrigada a implementar Faxear() com NotImplementedException.

// ✓ Segregada
public interface IImpressora { void Imprimir(); }
public interface IScanner { void Escanear(); }
// Uma multifuncional implementa as duas; a simples, só IImpressora.

D — Dependency Inversion (Inversão de Dependência)

Dependa de abstrações, não de implementações concretas. Módulos de alto nível não devem depender de detalhes; ambos dependem de interfaces. É a base do próximo capítulo.

// ❌ Serviço amarrado a uma implementação concreta
public class RelatorioServico
{
    private readonly SqlServerRepositorio _repo = new();   // acoplado ao SQL Server
}

// ✓ Depende da abstração; a implementação chega de fora (injeção)
public class RelatorioServico
{
    private readonly IRepositorio _repo;
    public RelatorioServico(IRepositorio repo) => _repo = repo;
}

Design patterns essenciais para o mercado

Padrões são soluções nomeadas para problemas recorrentes. Você não precisa decorar os 23 do "Gang of Four" — precisa reconhecer e aplicar os que aparecem no dia a dia:

Strategy — algoritmos intercambiáveis

public interface IEstrategiaFrete { decimal Calcular(decimal peso); }
public class FreteSedex : IEstrategiaFrete { public decimal Calcular(decimal p) => p * 2.5m; }
public class FretePac   : IEstrategiaFrete { public decimal Calcular(decimal p) => p * 1.2m; }

public class Carrinho
{
    private readonly IEstrategiaFrete _frete;
    public Carrinho(IEstrategiaFrete frete) => _frete = frete;   // escolhe a estratégia
    public decimal Total(decimal produtos, decimal peso) => produtos + _frete.Calcular(peso);
}

Factory — centralizar a criação

public static class MeioPagamentoFactory
{
    public static IMeioPagamento Criar(string tipo) => tipo switch
    {
        "pix"     => new Pix(),
        "credito" => new Credito(),
        _         => throw new NotSupportedException($"Meio '{tipo}' não suportado.")
    };
}

Repository — abstrair o acesso a dados

public interface IProdutoRepositorio
{
    Task<Produto?> ObterPorIdAsync(int id);
    Task AdicionarAsync(Produto p);
    Task<IReadOnlyList<Produto>> ListarAsync();
}
// A regra de negócio conversa com a interface; trocar EF Core por Dapper
// não afeta o domínio. (Padrão onipresente em backend .NET.)

Outros que valem conhecer de nome e propósito: Decorator (envolver um objeto para adicionar comportamento — ex.: logging/cache sobre um repositório), Observer (é o padrão por trás de eventos, cap. 9), Singleton (uma única instância — mas prefira o lifetime Singleton do container de DI a implementá-lo na mão), e Adapter (compatibilizar interfaces incompatíveis).

Armadilha

Não force padrões. Aplicar Factory + Strategy + Decorator num CRUD de 3 telas é over-engineering — e reprova em entrevista tanto quanto não conhecê-los. O sinal de senioridade é justificar quando um padrão paga seu custo de complexidade.

Cai na entrevista

"Explique SOLID com um exemplo real." — Escolha um princípio e mostre o antes/depois (o exemplo de Open/Closed com meios de pagamento é ótimo). "Qual design pattern você mais usa?" — Repository e Strategy são respostas seguras para backend; explique o problema que resolvem, não só a estrutura. Evite decorar UML — entrevistadores querem ver que você entende o porquê.

Exercícios · Capítulo 16
  1. Pegue uma classe que valida, salva no banco e envia e-mail, e refatore-a aplicando SRP (separe em 3 colaboradores com interfaces).
  2. Implemente o padrão Strategy para um sistema de descontos com 3 estratégias (sem desconto, percentual, valor fixo) e um contexto que aceita qualquer uma.
  3. Crie um ILogger simples e um Decorator LoggerComTimestamp que envolve outro logger acrescentando data/hora à mensagem.
  4. Escreva um exemplo próprio que viole LSP e explique em comentário por que a substituição quebra.
Ver solução do exercício 3 (Decorator)
public interface ILogger { void Log(string msg); }

public class ConsoleLogger : ILogger
{
    public void Log(string msg) => Console.WriteLine(msg);
}

// Decorator: MESMA interface, envolve outro ILogger e adiciona comportamento
public class LoggerComTimestamp : ILogger
{
    private readonly ILogger _interno;
    public LoggerComTimestamp(ILogger interno) => _interno = interno;

    public void Log(string msg)
        => _interno.Log($"[{DateTime.Now:HH:mm:ss}] {msg}");
}

// Uso: empilha decorators sem tocar no ConsoleLogger
ILogger logger = new LoggerComTimestamp(new ConsoleLogger());
logger.Log("Sistema iniciado");   // [14:03:27] Sistema iniciado
[Capitulo(17, Nivel.Engenharia)]

Injeção de dependência e arquitetura

Injeção de dependência (DI) é o mecanismo prático da inversão de dependência: em vez de uma classe criar suas próprias dependências (new SqlRepositorio()), ela as recebe prontas — normalmente pelo construtor. O ASP.NET Core traz um container de DI nativo, e dominá-lo é pré-requisito para qualquer vaga de backend .NET.

// A classe declara o que precisa; NÃO sabe como isso é construído
public class PedidoServico
{
    private readonly IPedidoRepositorio _repo;
    private readonly INotificador _notificador;
    private readonly ILogger<PedidoServico> _logger;

    public PedidoServico(
        IPedidoRepositorio repo,
        INotificador notificador,
        ILogger<PedidoServico> logger)
    {
        _repo = repo;
        _notificador = notificador;
        _logger = logger;
    }

    public async Task FinalizarAsync(int pedidoId)
    {
        var pedido = await _repo.ObterPorIdAsync(pedidoId)
            ?? throw new InvalidOperationException("Pedido não encontrado.");
        pedido.Finalizar();
        await _repo.SalvarAsync(pedido);
        _logger.LogInformation("Pedido {Id} finalizado", pedidoId);
        await _notificador.EnviarAsync(pedido.EmailCliente, "Pedido finalizado!");
    }
}

Registrando serviços no container

Program.cs
var builder = WebApplication.CreateBuilder(args);

// "Quando alguém pedir IPedidoRepositorio, entregue um PedidoRepositorioEf"
builder.Services.AddScoped<IPedidoRepositorio, PedidoRepositorioEf>();
builder.Services.AddScoped<INotificador, NotificadorEmail>();
builder.Services.AddScoped<PedidoServico>();
builder.Services.AddSingleton<ICachePrecos, CachePrecosMemoria>();

var app = builder.Build();
// O container resolve todo o grafo de dependências automaticamente ao criar PedidoServico.

Os três lifetimes — pergunta garantida de entrevista

LifetimeQuantas instânciasUse para
TransientUma nova a cada injeção/resoluçãoServiços leves e sem estado
ScopedUma por requisição HTTP (escopo)Repositórios, DbContext — o padrão em web
SingletonUma para toda a aplicaçãoCache, configuração, clientes reutilizáveis
Armadilha

Captive dependency: injetar um serviço Scoped (ex.: DbContext) dentro de um Singleton "prende" aquela instância scoped viva para sempre — causando bugs de dados compartilhados entre requisições e vazamentos. O container do ASP.NET Core detecta muitos desses casos e lança exceção na inicialização. Regra: um serviço nunca deve depender de outro com lifetime mais curto.

Arquitetura em camadas e Clean Architecture

Conforme o sistema cresce, organiza-se em camadas com dependências apontando numa direção só:

┌─────────────────────────────────────────────┐
│  API / UI        (controllers, endpoints)    │  ← detalhes de entrega
├─────────────────────────────────────────────┤
│  Application     (casos de uso, serviços)    │  ← orquestra o domínio
├─────────────────────────────────────────────┤
│  Domain          (entidades, regras)         │  ← o CORAÇÃO, sem dependências
├─────────────────────────────────────────────┤
│  Infrastructure  (EF Core, e-mail, APIs)     │  ← implementa interfaces do domínio
└─────────────────────────────────────────────┘

A ideia central da Clean Architecture (e da Onion/Hexagonal) é a regra da dependência: o código de dentro (Domain) não conhece o de fora. O domínio define a interface IPedidoRepositorio; a camada de Infrastructure a implementa com EF Core. Assim você troca banco, framework web ou provedor de e-mail sem tocar nas regras de negócio — e testa o domínio isoladamente.

No mercado

Você verá variações: "arquitetura em 3 camadas" (mais simples, comum em sistemas médios), Clean/Onion (em sistemas maiores), e às vezes só uma organização por features. O erro é dogmatismo: um microserviço pequeno com Clean Architecture completa vira burocracia. Saber adequar a arquitetura ao tamanho do problema é sinal de senioridade.

Cai na entrevista

"Diferença entre Transient, Scoped e Singleton?" — (tabela acima) e cite o DbContext como Scoped clássico. "O que é injeção de dependência e qual problema resolve?" — Desacopla criação de uso, permitindo testar com dublês (mocks) e trocar implementações; é a aplicação prática do D do SOLID. "O que é a regra de dependência na Clean Architecture?" — Dependências apontam para dentro; o domínio não referencia infraestrutura.

Exercícios · Capítulo 17
  1. Monte um mini-console com o container Microsoft.Extensions.DependencyInjection: registre INotificadorNotificadorConsole e resolva um serviço que o utiliza.
  2. Crie um exemplo que provoque uma captive dependency (Scoped dentro de Singleton) e observe/explique o comportamento.
  3. Esboce (em pastas/projetos) uma solução em camadas Domain/Application/Infrastructure/Api para um cadastro de produtos, indicando em qual camada mora cada interface e cada implementação.
Ver solução do exercício 1
// dotnet add package Microsoft.Extensions.DependencyInjection
using Microsoft.Extensions.DependencyInjection;

var services = new ServiceCollection();
services.AddScoped<INotificador, NotificadorConsole>();
services.AddScoped<BoasVindasServico>();

using var provider = services.BuildServiceProvider();
using var scope = provider.CreateScope();

var servico = scope.ServiceProvider.GetRequiredService<BoasVindasServico>();
servico.Executar("ana@x.com");

public interface INotificador { void Enviar(string para, string msg); }
public class NotificadorConsole : INotificador
{
    public void Enviar(string para, string msg) => Console.WriteLine($"→ {para}: {msg}");
}
public class BoasVindasServico
{
    private readonly INotificador _n;
    public BoasVindasServico(INotificador n) => _n = n;
    public void Executar(string email) => _n.Enviar(email, "Bem-vindo!");
}
[Capitulo(18, Nivel.Engenharia)]

Testes automatizados

Testes não são "extra" — são o que permite alterar código com confiança e é requisito explícito em boa parte das vagas. O framework dominante no .NET é o xUnit (NUnit e MSTest são alternativas equivalentes na prática).

O primeiro teste e o padrão AAA

// dotnet new xunit -n MeuProjeto.Tests
using Xunit;

public class CalculadoraTests
{
    [Fact]   // um teste sem parâmetros
    public void Somar_DoisPositivos_RetornaSoma()
    {
        // Arrange — prepara o cenário
        var calc = new Calculadora();

        // Act — executa a ação sob teste
        int resultado = calc.Somar(2, 3);

        // Assert — verifica o resultado
        Assert.Equal(5, resultado);
    }
}

O padrão AAA (Arrange-Act-Assert) estrutura todo bom teste. O nome do método comunica o que se testa, sob qual condição e o resultado esperado — assim, quando um teste falha, você entende o problema sem ler o corpo.

[Theory]: um teste, vários casos

[Theory]
[InlineData(2, 3, 5)]
[InlineData(-1, 1, 0)]
[InlineData(0, 0, 0)]
[InlineData(100, 200, 300)]
public void Somar_VariosCasos_RetornaEsperado(int a, int b, int esperado)
{
    var calc = new Calculadora();
    Assert.Equal(esperado, calc.Somar(a, b));
}

Asserts que você usará

Assert.Equal(esperado, real);
Assert.NotEqual(0, saldo);
Assert.True(pedido.EstaPago);
Assert.False(lista.Any());
Assert.Null(resultado);
Assert.NotNull(usuario);
Assert.Contains("erro", mensagem);
Assert.Empty(carrinho.Itens);
Assert.IsType<Gerente>(funcionario);

// Testando que uma exceção é lançada:
var ex = Assert.Throws<ArgumentOutOfRangeException>(
    () => conta.Sacar(-100m));
Assert.Contains("positivo", ex.Message);

// Assíncrono:
await Assert.ThrowsAsync<InvalidOperationException>(
    () => servico.FinalizarAsync(999));

Mocks com Moq: isolando a unidade sob teste

Para testar PedidoServico sem banco nem servidor de e-mail reais, substituímos as dependências por dublês. A biblioteca Moq é a mais usada:

// dotnet add package Moq
using Moq;

[Fact]
public async Task FinalizarAsync_PedidoExistente_NotificaCliente()
{
    // Arrange — configura os mocks
    var repo = new Mock<IPedidoRepositorio>();
    var pedido = new Pedido(1, "cliente@x.com");
    repo.Setup(r => r.ObterPorIdAsync(1)).ReturnsAsync(pedido);

    var notificador = new Mock<INotificador>();
    var logger = new Mock<ILogger<PedidoServico>>();

    var servico = new PedidoServico(repo.Object, notificador.Object, logger.Object);

    // Act
    await servico.FinalizarAsync(1);

    // Assert — verifica que a interação esperada aconteceu
    notificador.Verify(
        n => n.EnviarAsync("cliente@x.com", It.IsAny<string>()),
        Times.Once);
    repo.Verify(r => r.SalvarAsync(pedido), Times.Once);
}
Boa prática

Testes de unidade (rápidos, isolados com mocks) formam a base; acima deles vêm testes de integração (com banco real, muitas vezes em container) e alguns testes end-to-end — a "pirâmide de testes". Muitos testes de unidade, alguns de integração, pouquíssimos E2E. Não teste getters triviais; teste regras de negócio e caminhos de erro.

TDD em uma frase

Test-Driven Development: escreva o teste que falha (red), faça-o passar com o mínimo (green), depois refatore (refactor). Não é obrigatório em todo time, mas conhecer o ciclo e conseguir aplicá-lo em um exercício de entrevista é diferencial.

Cai na entrevista

"Diferença entre teste de unidade e de integração?" — Unidade testa uma classe isolada, com dependências mockadas, em milissegundos; integração testa vários componentes juntos (incluindo banco/HTTP reais). "O que é um mock e quando usar?" — Um dublê que simula uma dependência e permite verificar interações; use para isolar a unidade e evitar efeitos colaterais. "O que faz um bom teste?" — Rápido, determinístico, independente, com um motivo único de falha e nome descritivo.

Exercícios · Capítulo 18
  1. Escreva testes [Fact] e [Theory] para a classe ContaBancaria do capítulo 5: depósito válido, saque com saldo insuficiente e depósito negativo (deve lançar).
  2. Crie um PedidoServico com IPedidoRepositorio mockado (Moq) e verifique que, ao finalizar um pedido inexistente, ele lança exceção e não chama o notificador.
  3. Pratique TDD: escreva primeiro o teste de um método EhPrimo(int), veja falhar, implemente e refatore.
Ver solução do exercício 1
public class ContaBancariaTests
{
    [Fact]
    public void Depositar_ValorValido_AumentaSaldo()
    {
        var conta = new ContaBancaria("Ana", 100m);
        conta.Depositar(50m);
        Assert.Equal(150m, conta.Saldo);
    }

    [Fact]
    public void Sacar_SaldoInsuficiente_RetornaFalse()
    {
        var conta = new ContaBancaria("Ana", 100m);
        bool ok = conta.TentarSacar(200m);
        Assert.False(ok);
        Assert.Equal(100m, conta.Saldo);   // saldo intacto
    }

    [Theory]
    [InlineData(0)]
    [InlineData(-50)]
    public void Depositar_ValorInvalido_Lanca(decimal valor)
    {
        var conta = new ContaBancaria("Ana");
        Assert.Throws<ArgumentOutOfRangeException>(() => conta.Depositar(valor));
    }
}
Parte VI

Mercado de Trabalho

Tudo o que você aprendeu, aplicado nas três frentes que você escolheu: backend com APIs, aplicações desktop/web e jogos. Depois, a reta final: entrevistas, projetos de portfólio e roadmap de carreira.

[Capitulo(19, Nivel.Mercado)]

Backend: ASP.NET Core + EF Core

Este é o maior mercado para devs C# no Brasil. Uma API REST com ASP.NET Core e persistência com Entity Framework Core é o feijão-com-arroz que você precisa saber construir de olhos fechados. Vamos montar uma API de tarefas ponta a ponta.

Minimal API: o essencial em poucas linhas

Program.cs
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<AppDbContext>(opt =>
    opt.UseSqlite("Data Source=tarefas.db"));   // troque por UseSqlServer/UseNpgsql em prod
builder.Services.AddScoped<ITarefaRepositorio, TarefaRepositorio>();

var app = builder.Build();

app.MapGet("/tarefas", async (ITarefaRepositorio repo) =>
    Results.Ok(await repo.ListarAsync()));

app.MapGet("/tarefas/{id:int}", async (int id, ITarefaRepositorio repo) =>
{
    var tarefa = await repo.ObterPorIdAsync(id);
    return tarefa is null ? Results.NotFound() : Results.Ok(tarefa);
});

app.MapPost("/tarefas", async (CriarTarefaDto dto, ITarefaRepositorio repo) =>
{
    var tarefa = new Tarefa { Titulo = dto.Titulo, Concluida = false };
    await repo.AdicionarAsync(tarefa);
    return Results.Created($"/tarefas/{tarefa.Id}", tarefa);   // 201 + Location
});

app.MapPut("/tarefas/{id:int}", async (int id, AtualizarTarefaDto dto, ITarefaRepositorio repo) =>
{
    var tarefa = await repo.ObterPorIdAsync(id);
    if (tarefa is null) return Results.NotFound();
    tarefa.Titulo = dto.Titulo;
    tarefa.Concluida = dto.Concluida;
    await repo.SalvarAsync();
    return Results.NoContent();   // 204
});

app.MapDelete("/tarefas/{id:int}", async (int id, ITarefaRepositorio repo) =>
{
    await repo.RemoverAsync(id);
    return Results.NoContent();
});

app.Run();

// DTOs como records imutáveis (cap. 7) — a API expõe DTOs, nunca entidades cruas
public record CriarTarefaDto(string Titulo);
public record AtualizarTarefaDto(string Titulo, bool Concluida);

Controllers: a forma que domina o mercado corporativo

Minimal APIs são ótimas para serviços pequenos, mas a maioria das empresas usa controllers — mais estruturados para APIs grandes:

Controllers/TarefasController.cs
[ApiController]
[Route("api/[controller]")]
public class TarefasController : ControllerBase
{
    private readonly ITarefaRepositorio _repo;
    public TarefasController(ITarefaRepositorio repo) => _repo = repo;

    [HttpGet]
    public async Task<ActionResult<IEnumerable<Tarefa>>> Listar()
        => Ok(await _repo.ListarAsync());

    [HttpGet("{id:int}")]
    public async Task<ActionResult<Tarefa>> Obter(int id)
    {
        var tarefa = await _repo.ObterPorIdAsync(id);
        return tarefa is null ? NotFound() : Ok(tarefa);
    }

    [HttpPost]
    public async Task<ActionResult<Tarefa>> Criar(CriarTarefaDto dto)
    {
        var tarefa = new Tarefa { Titulo = dto.Titulo };
        await _repo.AdicionarAsync(tarefa);
        return CreatedAtAction(nameof(Obter), new { id = tarefa.Id }, tarefa);
    }
}

Entity Framework Core: o mapeamento objeto-relacional

Data/AppDbContext.cs
public class Tarefa
{
    public int Id { get; set; }                  // convenção: "Id" vira chave primária
    public string Titulo { get; set; } = "";
    public bool Concluida { get; set; }
    public DateTime CriadaEm { get; set; } = DateTime.UtcNow;
}

public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }

    public DbSet<Tarefa> Tarefas => Set<Tarefa>();   // vira a tabela "Tarefas"

    protected override void OnModelCreating(ModelBuilder mb)
    {
        mb.Entity<Tarefa>(e =>
        {
            e.Property(t => t.Titulo).IsRequired().HasMaxLength(200);
            e.HasIndex(t => t.Concluida);
        });
    }
}

As migrations versionam o schema do banco a partir das suas classes:

dotnet tool install --global dotnet-ef
dotnet ef migrations add CriacaoInicial   # gera o script de criação
dotnet ef database update                 # aplica no banco

O repositório com consultas LINQ traduzidas para SQL

public class TarefaRepositorio : ITarefaRepositorio
{
    private readonly AppDbContext _db;
    public TarefaRepositorio(AppDbContext db) => _db = db;

    // Este Where vira "WHERE Concluida = 0" no banco (IQueryable, cap. 10/15)
    public async Task<IReadOnlyList<Tarefa>> ListarPendentesAsync()
        => await _db.Tarefas
            .Where(t => !t.Concluida)
            .OrderBy(t => t.CriadaEm)
            .AsNoTracking()             // só leitura: dispensa rastreamento, mais rápido
            .ToListAsync();

    public async Task<Tarefa?> ObterPorIdAsync(int id)
        => await _db.Tarefas.FindAsync(id);

    public async Task AdicionarAsync(Tarefa t)
    {
        _db.Tarefas.Add(t);
        await _db.SaveChangesAsync();   // gera o INSERT e preenche t.Id
    }

    public async Task SalvarAsync() => await _db.SaveChangesAsync();
}
Armadilha

O problema N+1: carregar uma lista de pedidos e, dentro de um loop, acessar pedido.Cliente dispara uma consulta por pedido (1 + N). Use .Include(p => p.Cliente) para trazer tudo em uma query. É o bug de performance nº 1 com EF Core — e pergunta frequente de entrevista.

Verbos HTTP e status codes que você precisa dominar

VerboUsoSucesso típico
GETLer recurso(s)200 OK
POSTCriar recurso201 Created
PUTSubstituir recurso inteiro200 / 204
PATCHAtualizar parcialmente200 / 204
DELETERemover recurso204 No Content

Erros comuns: 400 (requisição inválida), 401 (não autenticado), 403 (autenticado mas sem permissão), 404 (não encontrado), 409 (conflito), 500 (erro no servidor).

Autenticação com JWT e middleware de erros

// Middleware global de exceções: o domínio lança, a borda traduz (cap. 11)
app.UseExceptionHandler(errApp => errApp.Run(async ctx =>
{
    var erro = ctx.Features.Get<IExceptionHandlerFeature>()?.Error;
    ctx.Response.StatusCode = erro switch
    {
        KeyNotFoundException => StatusCodes.Status404NotFound,
        ArgumentException    => StatusCodes.Status400BadRequest,
        _                    => StatusCodes.Status500InternalServerError
    };
    await ctx.Response.WriteAsJsonAsync(new { erro = erro?.Message });
}));

// Autenticação JWT (esqueleto)
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(opt =>
    {
        opt.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            // Issuer, Audience e a chave vêm de configuração/secrets, nunca hardcoded!
        };
    });
app.UseAuthentication();
app.UseAuthorization();

// Proteger um endpoint:
app.MapGet("/admin", () => "área restrita").RequireAuthorization();
No mercado

Stack de backend .NET que aparece nas vagas: ASP.NET Core + EF Core (ou Dapper para performance), SQL Server/PostgreSQL, JWT para auth, Swagger/OpenAPI para documentação, Docker para empacotar, Serilog para logs estruturados, e xUnit + Moq para testes. Bônus muito valorizado: mensageria (RabbitMQ/Kafka), cache (Redis) e CI/CD (GitHub Actions/Azure DevOps).

Cai na entrevista

"O que é REST?" — Estilo arquitetural baseado em recursos identificados por URLs, manipulados por verbos HTTP, com respostas stateless e status codes semânticos. "Diferença entre PUT e PATCH?" — PUT substitui o recurso inteiro; PATCH altera parcialmente. "O que é o problema N+1 e como resolver?" — (ver armadilha acima) resolva com Include/projeção. "AsNoTracking, para quê?" — Desliga o rastreamento de mudanças do EF em consultas somente-leitura, melhorando performance.

Exercícios · Capítulo 19
  1. Crie do zero uma API de "Contatos" (nome, e-mail, telefone) com CRUD completo, EF Core + SQLite, DTOs em records e os status codes corretos em cada endpoint.
  2. Adicione uma entidade Categoria relacionada a Contato (1:N) e escreva uma consulta com Include que evita o problema N+1.
  3. Implemente paginação em GET /contatos?pagina=1&tamanho=20 usando Skip/Take, retornando também o total de registros.
  4. Adicione um endpoint de login que gera um JWT e proteja o CRUD com RequireAuthorization().
[Capitulo(20, Nivel.Mercado)]

Desktop e Web UI: WPF, MAUI e Blazor

O C# cobre praticamente todo tipo de interface: aplicações desktop corporativas, apps mobile/desktop multiplataforma e aplicações web sem JavaScript. Você não precisa dominar as três — mas conhecer o panorama e o padrão comum a elas (MVVM e data binding) abre muitas portas.

WPF — desktop Windows corporativo

WPF (Windows Presentation Foundation) continua onipresente em sistemas internos de empresas. A UI é declarada em XAML e ligada ao código por data binding:

MainWindow.xaml
<Window x:Class="App.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
    <StackPanel Margin="20">
        <TextBox Text="{Binding NovoTitulo, UpdateSourceTrigger=PropertyChanged}" />
        <Button Content="Adicionar" Command="{Binding AdicionarCommand}" />
        <ListBox ItemsSource="{Binding Tarefas}" DisplayMemberPath="Titulo" />
    </StackPanel>
</Window>

MVVM: o padrão que unifica WPF, MAUI e mais

Model-View-ViewModel separa a interface (View, em XAML) da lógica de apresentação (ViewModel, em C#), ligadas por binding. A View não tem lógica; a ViewModel não conhece a View — o que a torna testável como qualquer classe comum.

TarefasViewModel.cs
public class TarefasViewModel : INotifyPropertyChanged
{
    public ObservableCollection<Tarefa> Tarefas { get; } = new();  // notifica a UI ao mudar

    private string _novoTitulo = "";
    public string NovoTitulo
    {
        get => _novoTitulo;
        set { _novoTitulo = value; OnPropertyChanged(); }   // dispara atualização da tela
    }

    public ICommand AdicionarCommand { get; }

    public TarefasViewModel()
        => AdicionarCommand = new RelayCommand(Adicionar, () => !string.IsNullOrWhiteSpace(NovoTitulo));

    private void Adicionar()
    {
        Tarefas.Add(new Tarefa { Titulo = NovoTitulo });
        NovoTitulo = "";
    }

    public event PropertyChangedEventHandler? PropertyChanged;
    private void OnPropertyChanged([CallerMemberName] string? nome = null)
        => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nome));
}
No mercado

Em vagas de WPF/MAUI, o toolkit CommunityToolkit.Mvvm é quase padrão: com os atributos [ObservableProperty] e [RelayCommand] (source generators, cap. 15), ele elimina todo o boilerplate de INotifyPropertyChanged mostrado acima. Vale citar que você conhece.

.NET MAUI — um código, várias plataformas

MAUI (Multi-platform App UI) é o sucessor do Xamarin: um projeto que compila para Android, iOS, Windows e macOS. Reaproveita XAML e MVVM, então quem sabe WPF migra rápido. É a aposta da Microsoft para apps móveis/desktop multiplataforma em C#.

Blazor — web em C#, sem JavaScript

Blazor permite construir SPAs (single-page applications) usando C# no lugar de JavaScript, com componentes reutilizáveis:

Contador.razor
@page "/contador"

<h3>Contador</h3>
<p>Valor atual: @_contagem</p>
<button @onclick="Incrementar">Clique</button>

@code {
    private int _contagem = 0;
    private void Incrementar() => _contagem++;   // C# reagindo a evento de UI
}

Blazor tem dois modelos principais de execução: Server (a UI roda no servidor e sincroniza via SignalR — leve no cliente, exige conexão constante) e WebAssembly (o C# roda no navegador via WASM — funciona offline, mas o download inicial é maior). O .NET 8+ unificou tudo em render modes, permitindo misturar os dois na mesma aplicação.

Cai na entrevista

"O que é MVVM e que problema resolve?" — Separa apresentação de UI, tornando a lógica testável e reutilizável; o binding conecta View e ViewModel sem acoplamento. "Diferença entre Blazor Server e WebAssembly?" — Server executa no servidor com UI sincronizada por SignalR (baixa latência de download, exige conexão); WASM roda C# no navegador (offline-capable, bundle maior). "O que é data binding?" — Sincronização automática entre propriedades da UI e do modelo, nos dois sentidos quando bidirecional.

Armadilha

Esquecer de implementar INotifyPropertyChanged (ou usar uma List em vez de ObservableCollection) faz a tela "não atualizar" quando os dados mudam — o bug de iniciante nº 1 em WPF/MAUI. A UI só reage a quem a notifica.

Exercícios · Capítulo 20
  1. Crie uma lista de tarefas em Blazor: componente com input, botão e lista, marcando itens como concluídos (use @bind e eventos).
  2. Escreva uma ViewModel (independente de framework) com INotifyPropertyChanged e um comando, e teste-a com xUnit — provando que MVVM é testável sem UI.
  3. Pesquise o CommunityToolkit.Mvvm e reescreva a ViewModel do exercício 2 usando [ObservableProperty] e [RelayCommand]; compare o tamanho do código.
[Capitulo(21, Nivel.Mercado)]

Jogos: C# no Unity

O Unity é a engine de jogos que mais emprega desenvolvedores C# no mundo — de jogos indie a AAA, passando por simulações, AR/VR e visualização. Você escreve scripts em C# que controlam objetos da cena. As regras da linguagem são as mesmas; o que muda é o modelo de execução e as preocupações de performance.

MonoBehaviour e o ciclo de vida

Todo script que se acopla a um objeto da cena herda de MonoBehaviour. A engine chama métodos "mágicos" em momentos específicos — você não os invoca:

using UnityEngine;

public class Jogador : MonoBehaviour
{
    [SerializeField] private float _velocidade = 5f;   // aparece no Inspector (cap. 15)
    [SerializeField] private int _vidaMaxima = 100;
    private int _vida;

    // Awake: uma vez, ao carregar o objeto — inicialização interna
    void Awake() => _vida = _vidaMaxima;

    // Start: uma vez, antes do primeiro frame — bom para referências a outros objetos
    void Start() => Debug.Log("Jogador pronto");

    // Update: A CADA FRAME — input e lógica leve. Cuidado com performance!
    void Update()
    {
        float h = Input.GetAxis("Horizontal");
        float v = Input.GetAxis("Vertical");

        // Time.deltaTime torna o movimento independente do frame rate:
        Vector3 movimento = new Vector3(h, 0, v) * _velocidade * Time.deltaTime;
        transform.position += movimento;
    }

    // FixedUpdate: em intervalo fixo — TODA física (Rigidbody, forças) vai aqui
    void FixedUpdate() { /* aplicar forças, física */ }

    // OnDestroy: ao destruir o objeto — DESASSINE eventos aqui (cap. 9/14)!
    void OnDestroy() { /* GerenciadorJogo.OnPause -= Pausar; */ }
}
Armadilha

Nunca multiplique velocidade por Time.deltaTime no FixedUpdate usando Time.deltaTime — lá se usa Time.fixedDeltaTime. E física (mover um Rigidbody) fora do FixedUpdate causa comportamento errático. Em Update, sempre multiplique por Time.deltaTime para que a velocidade não dependa do FPS da máquina.

Vetores, structs e a semântica de cópia (cap. 7 na prática)

// Vector3 é um STRUCT (tipo de valor). Isto NÃO funciona como você espera:
// transform.position.x = 5;   // ERRO de compilação — position retorna uma CÓPIA

// Correto: crie um novo Vector3 e reatribua
Vector3 pos = transform.position;
pos.x = 5f;
transform.position = pos;

// Ou direto:
transform.position = new Vector3(5f, transform.position.y, transform.position.z);

// float, não double! Unity usa float em toda a API — note o sufixo 'f'
float dano = 12.5f;

Coroutines: lógica ao longo do tempo

using System.Collections;

// Coroutine: "pausa" a execução entre frames sem bloquear o jogo
IEnumerator PiscarEDesaparecer()
{
    for (int i = 0; i < 3; i++)
    {
        GetComponent<Renderer>().enabled = false;
        yield return new WaitForSeconds(0.2f);   // espera 0,2s (sem travar)
        GetComponent<Renderer>().enabled = true;
        yield return new WaitForSeconds(0.2f);
    }
    Destroy(gameObject);
}
// Dispara com: StartCoroutine(PiscarEDesaparecer());

Performance: o GC é seu inimigo no código quente

Em jogos, um pico de coleta do GC (cap. 14) causa stutter visível. A regra de ouro é zero alocação por frame no Update:

// ❌ Alocações escondidas rodando a cada frame:
void Update()
{
    var inimigos = FindObjectsOfType<Inimigo>();     // aloca array + é lentíssimo
    string debug = "Vida: " + _vida;                 // aloca string
    foreach (var i in GetComponentsInChildren<X>())  // aloca a cada chamada
        { }
}

// ✓ Faça o cache uma vez; evite LINQ e concatenação em código quente:
private Inimigo[] _inimigos;
void Start() => _inimigos = FindObjectsOfType<Inimigo>();   // uma vez só

// Object Pooling: reutilize objetos em vez de Instantiate/Destroy constante
// (tiros, partículas, inimigos) — Instantiate/Destroy pressionam muito o GC.
public class PoolDeTiros : MonoBehaviour
{
    private readonly Queue<GameObject> _disponiveis = new();

    public GameObject Obter()
        => _disponiveis.Count > 0
            ? Reativar(_disponiveis.Dequeue())
            : Instantiate(_prefabTiro);

    public void Devolver(GameObject tiro)
    {
        tiro.SetActive(false);
        _disponiveis.Enqueue(tiro);      // volta pro pool em vez de Destroy
    }
}
No mercado

Além do C#, vagas de Unity pedem: entender o editor e o Inspector, prefabs, sistema de física, ScriptableObjects (dados como assets), UI (Canvas), e cada vez mais o DOTS/ECS e o Job System para performance extrema. Um portfólio com 2–3 jogos pequenos e completos (publicados no itch.io) vale mais que um projeto gigante inacabado.

Cai na entrevista

"Diferença entre Update e FixedUpdate?"Update roda uma vez por frame (variável); FixedUpdate em intervalo fixo, usado para física. "Por que Time.deltaTime?" — Para tornar movimento/animação independentes do frame rate. "Como evitar picos de GC?" — Zero alocação por frame, object pooling, cache de referências e componentes, evitar LINQ e boxing no código quente. "Por que transform.position.x = 5 não compila?"Vector3 é struct; a propriedade retorna cópia (cap. 7).

Exercícios · Capítulo 21
  1. Escreva um script de movimento em primeira/terceira pessoa usando Input.GetAxis e Time.deltaTime, com velocidade exposta via [SerializeField].
  2. Implemente um sistema de vida com evento OnMorte (cap. 9) e assine/desassine corretamente em OnEnable/OnDisable.
  3. Crie um object pool genérico para projéteis e compare (mentalmente ou com o Profiler) contra Instantiate/Destroy a cada tiro.
  4. Use uma coroutine para um efeito de "dano" que pisca o sprite por 1 segundo e depois some.
[Capitulo(22, Nivel.Mercado)]

Preparação para entrevistas

Este capítulo consolida as perguntas que apareceram ao longo da apostila e adiciona o que falta para você entrar em uma entrevista técnica de C# com confiança. Um processo típico tem: triagem de currículo, teste técnico (quiz ou desafio de código), entrevista técnica (teoria + live coding) e entrevista comportamental.

Banco de perguntas conceituais (com respostas de referência)

Tipos de valor vs. referência

P: Qual a diferença? R: Tipos de valor (int, struct, enum) contêm o dado e são copiados na atribuição; tipos de referência (class, arrays, string, delegates) contêm um endereço para um objeto no heap, e a atribuição copia a referência. Consequência: passar um objeto a um método e alterar suas propriedades reflete no chamador; reatribuir o parâmetro, não.

string é imutável — por quê importa?

R: Toda "alteração" cria uma nova string. Concatenar em loop gera lixo para o GC; use StringBuilder. A imutabilidade também torna strings seguras para uso como chave e entre threads.

Boxing

R: Converter um tipo de valor em object/interface aloca no heap e copia o valor. Custa performance e pressiona o GC; genéricos existem em grande parte para evitá-lo.

IEnumerable vs. IQueryable

R: IEnumerable processa em memória; IQueryable constrói uma árvore de expressão traduzida para SQL pelo provider (EF Core). Filtrar cedo no IQueryable vira WHERE no banco; materializar cedo demais traz a tabela inteira.

abstract class vs. interface

R: Classe abstrata compartilha estado e implementação e modela "é um"; interface é um contrato de capacidade, permite múltipla implementação. Preferência moderna: interfaces + composição, herança só para hierarquias naturais.

async/await e deadlock

R: await não bloqueia a thread — registra uma continuação. Bloquear com .Result/.Wait() em um contexto com SynchronizationContext (UI, ASP.NET clássico) causa deadlock porque a continuação espera a thread que está bloqueada esperando ela. Solução: use await ponta a ponta.

Garbage Collector

R: Heap gerenciado, coleta por alcançabilidade (mark-sweep-compact), gerações 0/1/2 + LOH baseadas na hipótese de que a maioria dos objetos morre jovem. Vazamentos vêm de referências esquecidas (eventos, statics, caches).

Lifetimes de DI

R: Transient (nova a cada resolução), Scoped (uma por requisição — DbContext), Singleton (uma na aplicação). Nunca injete Scoped em Singleton (captive dependency).

== vs. Equals

R: Para tipos de referência, == compara identidade por padrão (mesmo objeto), enquanto Equals pode ser sobrescrito para comparar conteúdo. string sobrecarrega == para comparar valor. Records comparam por valor em ambos. Sobrescreveu Equals? Sobrescreva GetHashCode junto.

Live coding: os problemas que mais aparecem

A maioria dos desafios de triagem cai em poucas categorias. Pratique até resolver no reflexo:

  • Strings: inverter, verificar palíndromo, contar frequência de caracteres, primeiro caractere não repetido, anagramas.
  • Arrays/listas: two-sum (par que soma um alvo), remover duplicatas, encontrar o elemento faltante, mover zeros para o fim, rotacionar.
  • Contagem e agrupamento: palavra mais frequente, agrupar anagramas — quase sempre com Dictionary.
  • Pilha: parênteses balanceados (visto no cap. 4).
  • Recursão/matemática: fatorial, Fibonacci, FizzBuzz, verificar primo.
// Exemplo: two-sum em O(n) com Dictionary — pergunta clássica
static (int, int)? TwoSum(int[] nums, int alvo)
{
    var vistos = new Dictionary<int, int>();   // valor → índice
    for (int i = 0; i < nums.Length; i++)
    {
        int complemento = alvo - nums[i];
        if (vistos.TryGetValue(complemento, out int j))
            return (j, i);
        vistos[nums[i]] = i;
    }
    return null;
}
// Chave da resposta: reconhecer que a força bruta é O(n²) e que o Dictionary
// troca tempo por memória, chegando a O(n). Verbalize esse raciocínio!
Boa prática

Em live coding, pense em voz alta. O entrevistador avalia seu raciocínio, não só a solução: esclareça requisitos ("os números podem repetir?"), comente a complexidade (Big-O) da sua abordagem, comece por uma solução simples e só depois otimize, e teste com um exemplo à mão. Silêncio total prejudica mesmo com o código certo.

A parte comportamental

Prepare histórias curtas no formato STAR (Situação, Tarefa, Ação, Resultado) para perguntas como "conte sobre um bug difícil que resolveu", "um conflito no time" ou "um projeto do qual se orgulha". Tenha também boas perguntas para fazer ao entrevistador (sobre o time, processo, stack) — demonstra interesse real.

No mercado

Seja honesto sobre seu nível. Dizer "não sei, mas resolveria assim..." vale mais que inventar. Para vaga júnior, ninguém espera que você domine tudo desta apostila — espera-se fundamentos sólidos (POO, coleções, LINQ básico), vontade de aprender e capacidade de raciocinar sobre um problema. Profundidade em async, GC e arquitetura é o que diferencia para pleno/sênior.

Exercícios · Capítulo 22
  1. Responda em voz alta, cronometrando 2 minutos cada, a cinco perguntas conceituais deste capítulo — grave-se e revise.
  2. Resolva, sem consultar, do reflexo: inverter string, palíndromo, two-sum, FizzBuzz e frequência de caracteres. Meça o tempo.
  3. Escreva duas histórias STAR sobre projetos/estudos seus (mesmo que pessoais), com resultado concreto.
  4. Implemente "agrupar anagramas": dada uma lista de palavras, agrupe as que são anagramas entre si (dica: ordene as letras de cada palavra para gerar a chave do Dictionary).
Ver solução do exercício 4 (agrupar anagramas)
static Dictionary<string, List<string>> AgruparAnagramas(IEnumerable<string> palavras)
{
    var grupos = new Dictionary<string, List<string>>();
    foreach (var palavra in palavras)
    {
        // Chave canônica: letras ordenadas ("amor" e "roma" → "amor")
        var chave = new string(palavra.ToLower().OrderBy(c => c).ToArray());
        if (!grupos.TryGetValue(chave, out var lista))
            grupos[chave] = lista = new List<string>();
        lista.Add(palavra);
    }
    return grupos;
}
// Versão LINQ equivalente:
// palavras.GroupBy(p => new string(p.ToLower().OrderBy(c => c).ToArray()))
[Capitulo(23, Nivel.Mercado)]

Projetos práticos e roadmap de carreira

Conhecimento sem prova não abre portas. Um portfólio de projetos completos e publicados é o que transforma "estudei C#" em "sei entregar software". Aqui estão projetos por área e um roadmap de crescimento.

Projetos de portfólio por área

Backend / API

  • API de gerenciamento (nível 1): um CRUD completo (tarefas, finanças pessoais, biblioteca) com ASP.NET Core, EF Core, autenticação JWT, Swagger e testes. Prova que você domina o feijão-com-arroz do cap. 19.
  • API com integrações (nível 2): consuma uma API externa (clima, cotação, CEP), adicione cache com Redis, logs estruturados com Serilog e empacote com Docker.
  • Sistema com arquitetura (nível 3): aplique Clean Architecture, adicione mensageria (RabbitMQ) para um fluxo assíncrono (ex.: processamento de pedidos) e CI/CD no GitHub Actions.

Desktop / Web

  • Um app de produtividade em Blazor ou MAUI com MVVM, persistência local e uma UI caprichada — publicado (Blazor no GitHub Pages/Azure; MAUI como APK no Android).
  • Um dashboard que consome sua própria API de backend, fechando o ciclo full-stack.

Jogos

  • Dois ou três jogos pequenos e completos no Unity (um clone de Flappy Bird, um endless runner, um puzzle), com menu, som, pontuação persistida e publicados no itch.io. Completude vale mais que ambição.
  • Participe de uma game jam (Ludum Dare, GMTK): entregar algo jogável em 48h é uma linha e tanto no currículo.
Boa prática

Todo projeto precisa de um README bom: o que é, screenshot/GIF, como rodar, tecnologias usadas e o que você aprendeu. Recrutadores passam segundos em cada repositório — o README é sua vitrine. Commits pequenos e com boas mensagens também contam: mostram como você trabalha.

Roadmap: de júnior a sênior

NívelO que dominarComo demonstrar
JúniorFundamentos (Partes I–III): POO, coleções, LINQ, tratamento de erros, Git, um CRUD com API + banco1–2 projetos completos, README caprichado, contribuições em GitHub
PlenoAsync, testes, SOLID, DI, EF Core avançado, design patterns, noções de arquitetura e performanceProjeto com testes e arquitetura em camadas; autonomia para entregar uma feature ponta a ponta
SêniorArquitetura de sistemas, decisões de trade-off, GC/performance profunda, mensageria, observabilidade, mentoriaLiderança técnica, decisões documentadas, impacto além do próprio código

Currículo, LinkedIn e GitHub para o mercado brasileiro

  • Currículo: uma página, objetivo, stack em destaque (C#, .NET, ASP.NET Core, SQL...), projetos com link, e experiências com resultado ("reduzi o tempo de X em Y%") em vez de só tarefas. Sem foto obrigatória, sem informações irrelevantes.
  • GitHub: fixe seus 3–4 melhores repositórios, cada um com README. Um perfil ativo (mesmo com projetos de estudo) supera um vazio.
  • LinkedIn: título claro ("Desenvolvedor .NET | C# | ASP.NET Core"), resumo em primeira pessoa, e publique o que estuda — visibilidade atrai recrutadores. Marque "aberto a oportunidades".
  • Onde buscar vagas: LinkedIn, Gupy, plataformas de tech recruiting, comunidades no Discord/Telegram de .NET Brasil, e vagas remotas internacionais (onde o C# é muito demandado).
No mercado

O ecossistema .NET no Brasil é forte em bancos, fintechs, indústria, saúde e consultorias — muitas com folha em .NET Framework legado migrando para .NET moderno. Inglês técnico (ler documentação, participar de reuniões) amplia bastante o leque, especialmente para vagas remotas com pagamento em dólar. Nunca pare de estudar: o .NET lança uma versão maior por ano, e acompanhar as novidades é parte do trabalho.

Próximos passos depois desta apostila

Você percorreu do Console.WriteLine até arquitetura, concorrência e três áreas de mercado. Para continuar: aprofunde a área que escolheu com a documentação oficial da Microsoft (learn.microsoft.com), leia código de projetos open source em C# no GitHub, e — acima de tudo — construa. A diferença entre quem sabe e quem é contratado é a pilha de projetos terminados.

Exercícios finais · Capítulo 23
  1. Escolha uma das três áreas e comece hoje o projeto de nível 1 correspondente. Defina um escopo pequeno o suficiente para terminar em duas semanas.
  2. Crie ou reformule seu GitHub: perfil com bio, ao menos um repositório com README completo e commits organizados.
  3. Escreva um roadmap pessoal de 90 dias com metas semanais, combinando estudo (revisar capítulos desta apostila) e prática (o projeto).
  4. Faça uma auto-avaliação honesta: para cada capítulo (1 a 22), marque "domino / preciso revisar / preciso estudar" e priorize as lacunas antes de aplicar para vagas.
// Fim da apostila — return CarreiraEmCSharp.Comecar();

Você chegou ao fim de uma trilha completa de C#, do básico ao avançado, com foco no que o mercado realmente pede. Guarde esta apostila como referência: volte aos capítulos conforme os temas aparecerem no seu trabalho e nos seus estudos. O próximo passo não está aqui — está no editor de código. Bons projetos e boa carreira.