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.
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:
Convenções e hábitos que times profissionais esperam de você desde o primeiro dia.
Perguntas clássicas de processo seletivo relacionadas ao tema do capítulo — com a resposta que o entrevistador quer ouvir.
Como o conceito aparece no dia a dia real de empresas: em code review, em sistemas legados, em decisões de arquitetura.
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.
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.
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.
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)
| Nome | O 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 Standard | Especificação de compatibilidade entre os dois mundos. | Bibliotecas que precisam rodar em ambos. |
| Mono / Unity | Runtime alternativo usado pelo Unity (com scripting backend IL2CPP para builds). | Desenvolvimento de jogos (capítulo 21). |
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:
Console.WriteLine("Olá, mercado de trabalho!");
Mas você precisa conhecer a forma completa, porque é o que existe em 99% dos códigos de empresa:
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!");
}
}
}
usingimporta namespaces (comoimport/includeem outras linguagens).namespaceagrupa 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;argssã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:
<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
"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.
- Instale o SDK e crie um projeto console chamado
Curriculoque imprime seu nome, sua stack-alvo (backend, jogos...) e o ano em que você quer estar empregado como dev C#. - Descubra (via
dotnet --list-sdks) quais SDKs estão instalados na sua máquina. - Converta o template moderno (top-level) para a forma clássica com
Main, e faça o programa imprimir cada argumento recebido emargs.
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
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
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
| Tipo | Tamanho | Uso típico | Exemplo |
|---|---|---|---|
int | 32 bits | Contadores, IDs, quantidades | int qtd = 42; |
long | 64 bits | IDs de banco grandes, timestamps | long id = 9_000_000_000L; |
double | 64 bits | Cálculo científico, física de jogos | double vel = 9.81; |
float | 32 bits | Unity usa float em tudo | float dt = 0.016f; |
decimal | 128 bits | Dinheiro — sempre! | decimal preco = 19.90m; |
bool | — | Verdadeiro/falso | bool ativo = true; |
char | 16 bits | Um caractere Unicode | char letra = 'A'; |
string | — | Texto (tipo de referência!) | string s = "olá"; |
byte | 8 bits | Dados binários, buffers | byte b = 255; |
Nunca use double ou float para dinheiro. Eles são binários de ponto flutuante: 0.1 + 0.2 == 0.3 dá false! 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"
"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");
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)
- Leia dois números do console com
TryParsee mostre soma, divisão real e resto, tratando entradas inválidas e divisão por zero. - Prove no console que
0.1 + 0.2 != 0.3comdouble, e que comdecimala igualdade vale. - 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. - 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
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
}
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);
"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);
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.
- Escreva
FizzBuzzde 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. - Crie
bool TentarCalcularImc(double peso, double altura, out double imc)seguindo o padrão TryXxx, validando entradas. - Implemente
Fibonacci(int n)de forma recursiva e depois iterativa. Meça grosseiramente a diferença de tempo paran = 40(dica:System.Diagnostics.Stopwatch). - 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"
};
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ção | Array / List | Dictionary / HashSet |
|---|---|---|
| Acesso por índice/chave | O(1) | O(1) médio |
Busca por valor (Contains) | O(n) | O(1) médio |
| Inserir no fim | O(1) amortizado | O(1) médio |
| Inserir no início/meio | O(n) — desloca elementos | — |
| Remover por valor | O(n) | O(1) médio |
"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.
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.
- Leia palavras do usuário até ele digitar "fim" e mostre quantas palavras distintas foram digitadas.
- Conte a frequência de cada caractere de uma string usando
Dictionary<char,int>(pergunta real de triagem técnica). - Simule uma fila de atendimento: comandos "chegar <nome>", "atender" e "listar" no console, usando
Queue<string>. - Verifique se uma expressão com parênteses/colchetes/chaves está balanceada — ex.:
"{[()]}"é válida,"([)]"não — usandoStack<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;
}
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.
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
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
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.
"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.
- Modele
Jogadorpara um jogo: propriedadesNome,Vida(0–100, nunca fora da faixa), métodoReceberDano(int)eCurar(int), e propriedade calculadaEstaVivo. Nenhum código externo pode colocar a vida em valor inválido. - Crie
CarrinhoDeComprascom lista interna privada de itens, métodosAdicionar,Removere propriedadeTotalemdecimal. Exponha os itens só comoIReadOnlyList<Item>. - Implemente um contador
staticde 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);
}
}
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.
"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
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.)
- Modele um sistema de formas com
Formaabstrata (comArea()ePerimetro()), implementeCirculo,RetanguloeTriangulo, e escreva um método que recebeList<Forma>e retorna a soma das áreas. - Crie
IRepositorio<T>-like simplificado: interfaceIArmazenamentocomSalvar(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. - Explique em comentários por que
List<Gerente>não pode ser atribuída aList<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}");
}
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.
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
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);
"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.
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.
- Crie
readonly struct Temperaturacom valor em Celsius e propriedades calculadas para Fahrenheit e Kelvin, mais operadores+e-. - Modele
enum DiaSemanae escreva um método que, dado um dia, retorna se é útil ou fim de semana com switch expression. - Crie o record
Endereco(string Rua, string Cidade, string Uf)e demonstre: igualdade por valor entre duas instâncias idênticas e cópia comwithmudando só a cidade. - 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
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.
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
"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.
- Implemente
Pilha<T>do zero (com array interno que dobra de tamanho quando enche):Push,Pop,Peek,Count. - Crie
static T? MaiorOuNull<T>(IEnumerable<T> itens)que retornanull/default para coleção vazia, com constraint adequada. - Estenda o
RepositorioMemoria<T>comRemover(int id)ebool Existe(int id), e use-o com duas entidades diferentes (ex.:ClienteeProduto) 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.");
}
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!
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
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.
"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.
- Escreva
List<T> Filtrar<T>(List<T> itens, Func<T, bool> criterio)— você acabou de reimplementar oWheredo LINQ. - Crie uma classe
SensorDeTemperaturacom eventoTemperaturaCriticadisparado quando a leitura passa de 80°C; assine dois handlers (alarme e log). - Demonstre a pegadinha da closure: crie 3
Actions em umforcapturandoi, 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 ✓
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();
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.
"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
- Com a lista
devs: encontre o dev mais bem pago de cada área (dica:GroupBy+MaxBy). - Dada uma
List<string>de palavras, retorne as 3 mais frequentes, em ordem decrescente de frequência (pergunta real de entrevista — resolva comGroupBy,OrderByDescending,Take). - Demonstre a execução adiada: crie uma query
Wherecom umConsole.WriteLinedentro da lambda e observe quando ele imprime. - 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);
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.");
}
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();
}
"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.
- Crie
EstoqueInsuficienteExceptioncom propriedadesProdutoIdeQuantidadeDisponivel, e um métodoReservarProdutoque a lança. - 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. - Demonstre a diferença entre
throw;ethrow ex;imprimindo o stack trace nos dois casos. - Implemente
IDisposableem uma classeMedidorDeTempoque inicia umStopwatchno construtor e imprime o tempo decorrido noDispose— permitindo medir qualquer bloco comusing 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
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!)
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}",
};
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.
"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".
- Reescreva com
?.,??e no máximo 3 linhas: dadoCliente?comEndereco?que temCidade?, imprima a cidade ou "não informada". - 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%. - 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
};
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.
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");
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); // ✓
"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(); }
}
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.
- Simule 3 "downloads" com
Task.Delayde durações diferentes e compare o tempo total sequencial vs.Task.WhenAll(useStopwatch). - Adicione
CancellationTokena um processamento em loop e cancele-o quando o usuário apertar Enter (dica:Console.ReadLineem outra Task). - Reproduza a condição de corrida do contador e conserte de duas formas (lock e Interlocked).
- Escreva
Task<string> BuscarComTimeoutAsync(Func<Task<string>> operacao, TimeSpan timeout)usandoTask.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
}
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ção | O que contém | Coleta |
|---|---|---|
| Gen 0 | Objetos recém-criados | Frequente e baratíssima |
| Gen 1 | Sobreviventes da Gen 0 | Intermediária |
| Gen 2 | Objetos de vida longa (caches, singletons) | Rara e cara ("full GC") |
| LOH | Objetos 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.
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.
"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.
- Escreva duas versões de um contador de vogais de uma string de 10 MB: uma com
Substring/alocações e outra comReadOnlySpan<char>; compare tempo e (se souber) alocações. - Provoque um vazamento: uma classe
Publicadorestática com evento, e 10.000 assinantes criados em loop sem desassinar. Depois corrija com-=e explique. - Demonstre boxing: adicione 1 milhão de ints em um
ArrayListe em umList<int>e compare os tempos comStopwatch.
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.
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
| Framework | O que ele faz com reflection/attributes |
|---|---|
| ASP.NET Core | Descobre controllers, lê [HttpGet("rota")], [FromBody], valida [Required] |
| EF Core | Mapeia classes → tabelas lendo propriedades e [Key], [Column] |
| xUnit/NUnit | Encontra e executa métodos marcados com [Fact]/[Test] |
| Unity | [SerializeField] expõe campos no Inspector; [Header], [Range] customizam |
| Injeção de dependência | Constró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
"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.
- Escreva um "mini serializador": método que recebe
objecte retorna uma stringProp1=valor;Prop2=valorlendo as propriedades via reflection. - Crie
[ColunaAttribute(string nome)]e um método que gera umCREATE TABLEfake a partir de uma classe anotada. - 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"
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.
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).
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.
"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ê.
- Pegue uma classe que valida, salva no banco e envia e-mail, e refatore-a aplicando SRP (separe em 3 colaboradores com interfaces).
- 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.
- Crie um
ILoggersimples e um DecoratorLoggerComTimestampque envolve outro logger acrescentando data/hora à mensagem. - 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
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
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
| Lifetime | Quantas instâncias | Use para |
|---|---|---|
| Transient | Uma nova a cada injeção/resolução | Serviços leves e sem estado |
| Scoped | Uma por requisição HTTP (escopo) | Repositórios, DbContext — o padrão em web |
| Singleton | Uma para toda a aplicação | Cache, configuração, clientes reutilizáveis |
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.
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.
"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.
- Monte um mini-console com o container
Microsoft.Extensions.DependencyInjection: registreINotificador→NotificadorConsolee resolva um serviço que o utiliza. - Crie um exemplo que provoque uma captive dependency (Scoped dentro de Singleton) e observe/explique o comportamento.
- 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!");
}
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);
}
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.
"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.
- Escreva testes
[Fact]e[Theory]para a classeContaBancariado capítulo 5: depósito válido, saque com saldo insuficiente e depósito negativo (deve lançar). - Crie um
PedidoServicocomIPedidoRepositoriomockado (Moq) e verifique que, ao finalizar um pedido inexistente, ele lança exceção e não chama o notificador. - 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));
}
}
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.
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
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:
[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
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();
}
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
| Verbo | Uso | Sucesso típico |
|---|---|---|
| GET | Ler recurso(s) | 200 OK |
| POST | Criar recurso | 201 Created |
| PUT | Substituir recurso inteiro | 200 / 204 |
| PATCH | Atualizar parcialmente | 200 / 204 |
| DELETE | Remover recurso | 204 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();
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).
"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.
- 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.
- Adicione uma entidade
Categoriarelacionada aContato(1:N) e escreva uma consulta comIncludeque evita o problema N+1. - Implemente paginação em
GET /contatos?pagina=1&tamanho=20usandoSkip/Take, retornando também o total de registros. - Adicione um endpoint de login que gera um JWT e proteja o CRUD com
RequireAuthorization().
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:
<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.
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));
}
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:
@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.
"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.
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.
- Crie uma lista de tarefas em Blazor: componente com input, botão e lista, marcando itens como concluídos (use
@binde eventos). - Escreva uma
ViewModel(independente de framework) comINotifyPropertyChangede um comando, e teste-a com xUnit — provando que MVVM é testável sem UI. - Pesquise o
CommunityToolkit.Mvvme reescreva a ViewModel do exercício 2 usando[ObservableProperty]e[RelayCommand]; compare o tamanho do código.
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; */ }
}
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
}
}
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.
"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).
- Escreva um script de movimento em primeira/terceira pessoa usando
Input.GetAxiseTime.deltaTime, com velocidade exposta via[SerializeField]. - Implemente um sistema de vida com evento
OnMorte(cap. 9) e assine/desassine corretamente emOnEnable/OnDisable. - Crie um object pool genérico para projéteis e compare (mentalmente ou com o Profiler) contra
Instantiate/Destroya cada tiro. - Use uma coroutine para um efeito de "dano" que pisca o sprite por 1 segundo e depois some.
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!
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.
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.
- Responda em voz alta, cronometrando 2 minutos cada, a cinco perguntas conceituais deste capítulo — grave-se e revise.
- Resolva, sem consultar, do reflexo: inverter string, palíndromo, two-sum, FizzBuzz e frequência de caracteres. Meça o tempo.
- Escreva duas histórias STAR sobre projetos/estudos seus (mesmo que pessoais), com resultado concreto.
- 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()))
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.
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ível | O que dominar | Como demonstrar |
|---|---|---|
| Júnior | Fundamentos (Partes I–III): POO, coleções, LINQ, tratamento de erros, Git, um CRUD com API + banco | 1–2 projetos completos, README caprichado, contribuições em GitHub |
| Pleno | Async, testes, SOLID, DI, EF Core avançado, design patterns, noções de arquitetura e performance | Projeto com testes e arquitetura em camadas; autonomia para entregar uma feature ponta a ponta |
| Sênior | Arquitetura de sistemas, decisões de trade-off, GC/performance profunda, mensageria, observabilidade, mentoria | Lideranç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).
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.
- 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.
- Crie ou reformule seu GitHub: perfil com bio, ao menos um repositório com README completo e commits organizados.
- Escreva um roadmap pessoal de 90 dias com metas semanais, combinando estudo (revisar capítulos desta apostila) e prática (o projeto).
- 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.
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.