DevTools

Cheatsheet C#

Linguagem de programação da Microsoft

Voltar às linguagens
C#
122 cards encontrados
Categorias:
Versões:

Sintaxe Básica


11 cards
Variáveis
int idade = 30;
string nome = "Ana";
double preco = 9.99;
bool ativo = true;
char letra = 'A';

// Tipo inferido pelo compilador
var total = 100;      // int
var mensagem = "Olá"; // string

Em C# declaras o tipo antes do nome. O var deixa o compilador inferir o tipo a partir do valor.

Interpolação de strings
string nome = "Ana";
int idade = 30;

string s = $"A {nome} tem {idade} anos";
string calc = $"Daqui a 5 anos: {idade + 5}";

// Formatação
double preco = 9.5;
string p = $"Preço: {preco:F2}€";  // 9,50€

O prefixo $ permite inserir expressões dentro de { }. Os formatos (ex.: F2) controlam as casas decimais.

Comentários
// Comentário de uma linha

/* Comentário
   de várias linhas */

/// <summary>
/// Documentação XML (aparece no IntelliSense)
/// </summary>
int Somar(int a, int b) => a + b;

// TODO: tarefa pendente
// NOTE: nota importante

// comenta uma linha e /* */ várias. O /// gera documentação XML que o Visual Studio mostra no IntelliSense.

Tipos de dados
// Números inteiros
int n = 10;                 // 32 bits
long grande = 9999999999L;
short pequeno = 100;

// Números com vírgula
float f = 3.14f;
double d = 3.14;
decimal preco = 9.99m;      // dinheiro

// Outros
bool ok = true;
char c = 'A';
string s = "texto";
object obj = 1;

Usa decimal para valores monetários (mais preciso). O sufixo (f, m, L) indica o tipo do literal.

Nullable
int? x = null;       // int que aceita null
int y = x ?? 0;      // 0 se x for null
int z = x!.Value;    // força (cuidado!)

// Verificar antes de usar
if (x.HasValue) {
    Console.WriteLine(x.Value);
}

string? nome = null; // referência nullable

Por defeito, tipos de valor não aceitam null. O ? torna-os nullable. O ?? dá um valor alternativo se for null.

Console: entrada e saída
// Saída
Console.WriteLine("Olá!");       // com quebra de linha
Console.Write("sem quebra");
Console.WriteLine($"Valor: {10:F2}");

// Entrada (devolve sempre string)
string? nome = Console.ReadLine();
Console.WriteLine($"Olá, {nome}");

// Ler uma tecla
var tecla = Console.ReadKey();

Console.WriteLine escreve e muda de linha; Console.Write não muda. Console.ReadLine() lê o que foi digitado (sempre como string).

Constantes
// Constante de compilação
const int Max = 100;
const string App = "Aula";

// Só de leitura (definida em runtime)
readonly int _id;
public Pessoa(int id) {
    _id = id;
}

const é fixa em tempo de compilação. readonly pode ser definida uma vez (no construtor) e depois não muda mais.

Conversões
// String para número
int n = int.Parse("123");
double d = double.Parse("3.14");

// Mais seguro (não lança exceção)
if (int.TryParse("abc", out int r)) {
    Console.WriteLine(r);
}

// Entre tipos
double dd = Convert.ToDouble("3.14");
int i = (int)3.9;  // cast: 3 (trunca)

Usa TryParse em vez de Parse quando a entrada pode ser inválida — devolve false em vez de lançar exceção.

Verbatim strings (@)
// Sem @: precisa de escapar as barras
string caminho = "C:\\pastas\\ficheiro.txt";

// Com @: literal, como está escrito
string caminho2 = @"C:\pastas\ficheiro.txt";

// Várias linhas preservadas
string sql = @"SELECT *
FROM clientes
WHERE ativo = 1";

O prefixo @ cria uma string literal: barras e quebras de linha são mantidas como estão. Ideal para caminhos de ficheiros e SQL.

Operadores
// Aritméticos
10 + 3    // 13
10 / 3    // 3 (inteiro!)
10.0 / 3  // 3.333
10 % 3    // 1 (resto)

// Comparação
5 == 5    // true
5 != 3    // true
5 >= 3    // true

// Lógicos
true && false  // false (E)
true || false  // true  (OU)
!true          // false (NÃO)

Atenção: 10 / 3 dá 3 (divisão inteira). Usa 10.0 / 3 para obter decimais. && é E e || é OU.

Métodos de string úteis
string s = "  Olá Mundo  ";

s.Trim()           // "Olá Mundo"
s.ToUpper()        // "  OLÁ MUNDO  "
s.ToLower()
s.Replace("Mundo", "C#")
s.Split(" ")       // array de palavras
s.Contains("Olá")  // true
s.Length           // 13

Strings são imutáveis: os métodos devolvem uma nova string. Trim() remove espaços nas pontas e Split() divide em partes.

Controlo de Fluxo


9 cards
if / else
int idade = 20;

if (idade >= 18) {
    Console.WriteLine("adulto");
} else if (idade >= 13) {
    Console.WriteLine("adolescente");
} else {
    Console.WriteLine("criança");
}

A condição vai entre parênteses e o bloco entre chavetas. O else if encadeia várias condições.

foreach
var frutas = new[] { "maçã", "pera", "uvas" };

foreach (var fruta in frutas) {
    Console.WriteLine(fruta);
}

// Com índice (se precisares)
for (int i = 0; i < frutas.Length; i++) {
    Console.WriteLine($"{i}: {frutas[i]}");
}

O foreach percorre cada elemento de uma coleção sem precisares de índice. Mais simples e legível que o for para percorrer.

Loops aninhados
// Tabela de multiplicação
for (int i = 1; i <= 3; i++) {
    for (int j = 1; j <= 3; j++) {
        Console.Write($"{i * j,4}");
    }
    Console.WriteLine();
}

// break 2 não existe em C# — usa uma flag ou método
for (int i = 0; i < 5; i++) {
    for (int j = 0; j < 5; j++) {
        if (i * j > 6) goto fim;
    }
}
fim:;

Loops dentro de loops percorrem grelhas e matrizes. Para sair de vários níveis, extrai para um método (com return) ou usa uma variável de controlo.

switch
int dia = 1;

switch (dia) {
    case 1:
        Console.WriteLine("Segunda");
        break;
    case 2:
        Console.WriteLine("Terça");
        break;
    default:
        Console.WriteLine("Outro");
        break;
}

O switch compara um valor com vários case. Cada caso precisa de break (ou return). O default é o caso padrão.

while / do-while
int x = 5;
while (x > 0) {
    Console.WriteLine(x);
    x--;
}

// Executa pelo menos uma vez
do {
    x++;
} while (x < 10);

O while testa a condição antes de executar. O do-while executa primeiro e testa depois — corre sempre pelo menos uma vez.

switch expression (C# 8+)
int dia = 1;

string nome = dia switch {
    1 => "Segunda",
    2 => "Terça",
    3 => "Quarta",
    _ => "Outro"      // caso padrão
};

Versão moderna e compacta do switch. Usa => para devolver o valor e _ como caso padrão. Sem break.

Operador ternário
int idade = 20;

string estado = idade >= 18 ? "adulto" : "menor";

// Equivalente a:
// if (idade >= 18) estado = "adulto";
// else estado = "menor";

Forma compacta de um if/else numa linha: condição ? valor_se_verdadeiro : valor_se_falso.

for
for (int i = 0; i < 10; i++) {
    Console.WriteLine(i);  // 0 a 9
}

// Decrescente
for (int i = 10; i > 0; i--) {
    Console.WriteLine(i);
}

O for tem três partes: inicialização, condição e incremento. Ideal quando sabes quantas vezes queres repetir.

break e continue
for (int i = 0; i < 10; i++) {
    if (i == 3) continue;  // salta o 3
    if (i == 7) break;     // pára no 7
    Console.WriteLine(i);  // 0,1,2,4,5,6
}

foreach (var item in lista) {
    if (item == null) continue;
    Processar(item);
}

continue salta para a próxima iteração; break termina o loop imediatamente. Úteis para ignorar casos ou parar cedo.

Métodos


10 cards
Método simples
int Somar(int a, int b) {
    return a + b;
}

// Sem retorno (void)
void Saudar(string nome) {
    Console.WriteLine($"Olá, {nome}!");
}

int r = Somar(2, 3);  // 5

Um método declara o tipo de retorno, o nome e os parâmetros. Usa void quando não devolve nada.

params
int Somar(params int[] nums) {
    return nums.Sum();
}

Somar(1, 2, 3);        // 6
Somar(1, 2, 3, 4, 5);  // 15

// Também aceita um array
Somar(new[] { 1, 2, 3 });

O params permite passar um número variável de argumentos. Por dentro, chegam como um array.

Recursão
// Método que se chama a si próprio
long Fatorial(int n) {
    if (n <= 1) return 1;      // caso base (obrigatório!)
    return n * Fatorial(n - 1);
}

Fatorial(5);  // 120

// Fibonacci
int Fib(int n) => n <= 1 ? n : Fib(n - 1) + Fib(n - 2);

Um método recursivo chama-se a si próprio. Precisa sempre de um caso base para parar, senão entra em loop infinito (StackOverflowException).

Expression-bodied
// Corpo numa expressão (=>)
int Dobro(int x) => x * 2;

string Saudar(string n) => $"Olá, {n}";

void Ola() => Console.WriteLine("Olá");

Para métodos de uma linha, usa => sem chavetas nem return. Mais conciso e legível.

Método assíncrono
async Task<int> ObterAsync() {
    await Task.Delay(1000);  // simula espera
    return 42;
}

// Sem retorno
async Task ImprimirAsync() {
    await Task.Delay(500);
    Console.WriteLine("feito");
}

Métodos com async devolvem Task (sem valor) ou Task<T> (com valor). O await espera sem bloquear a thread.

Métodos de extensão
public static class StringExtensions {
    // this no 1º parâmetro = extensão de string
    public static string PrimeiraMaiuscula(this string s) {
        if (string.IsNullOrEmpty(s)) return s;
        return char.ToUpper(s[0]) + s.Substring(1);
    }
}

// Uso: parece um método nativo
"olá".PrimeiraMaiuscula();  // "Olá"

Um método de extensão adiciona métodos a tipos existentes sem os modificar. Declara-se numa classe static com this no primeiro parâmetro.

Parâmetros opcionais e nomeados
void Ligar(string host, int porta = 3306, int timeout = 30) {
    Console.WriteLine($"{host}:{porta}");
}

Ligar("localhost");              // usa defaults
Ligar("localhost", 5432);
Ligar(porta: 5432, host: "db");  // por nome

Parâmetros com = valor são opcionais. Podes passar argumentos por nome, em qualquer ordem, para mais clareza.

Métodos locais
int Calcular(int x) {
    // Função dentro da função
    int Quadrado(int n) => n * n;

    return Quadrado(x) + Quadrado(x + 1);
}

Podes definir funções dentro de funções. A função local (ex.: Quadrado) só existe dentro do método que a cria.

ref e out
// ref: entra com valor e pode ser alterado
void Incrementar(ref int x) {
    x++;
}
int n = 5;
Incrementar(ref n);  // n = 6

// out: só devolve valor
bool Tenta(out int resultado) {
    resultado = 42;
    return true;
}

ref passa por referência (lê e escreve). out serve só para devolver valores — o método tem de lhe atribuir valor.

Sobrecarga de métodos
// Mesmo nome, assinaturas diferentes
int Somar(int a, int b) => a + b;
double Somar(double a, double b) => a + b;
int Somar(int a, int b, int c) => a + b + c;

Somar(1, 2);        // chama a 1ª
Somar(1.5, 2.5);    // chama a 2ª
Somar(1, 2, 3);     // chama a 3ª

A sobrecarga permite vários métodos com o mesmo nome (ex.: Somar), desde que os parâmetros difiram (tipo ou quantidade). O compilador escolhe o certo.

Classes e Objetos


10 cards
Classe com propriedades
public class Pessoa {
    public string Nome { get; set; }
    public int Idade { get; set; }
}

var p = new Pessoa {
    Nome = "Ana",
    Idade = 30
};
Console.WriteLine(p.Nome);

As auto-propriedades ({ get; set; }) criam o campo interno automaticamente. A sintaxe new { ... } inicializa as propriedades.

Enum
enum Estado { Ativo, Inativo, Suspenso }

Estado e = Estado.Ativo;

if (e == Estado.Ativo) {
    Console.WriteLine("ativo");
}

// Converter
Estado doTexto = Enum.Parse<Estado>("Inativo");

Um enum define um conjunto de constantes nomeadas. Torna o código mais legível do que usar números (0, 1, 2...).

Indexadores
public class Semana {
    private readonly string[] _dias = new string[7];

    // Aceder com [] como um array
    public string this[int indice] {
        get => _dias[indice];
        set => _dias[indice] = value;
    }
}

var s = new Semana();
s[0] = "Segunda";
Console.WriteLine(s[0]);

Um indexador (this[...]) permite aceder a um objeto com a sintaxe de array (obj[0]). Útil para classes que envolvem coleções.

Construtor
public class Pessoa {
    public string Nome { get; }
    public int Idade { get; }

    public Pessoa(string nome, int idade) {
        Nome = nome;
        Idade = idade;
    }
}

var p = new Pessoa("Ana", 30);

O construtor tem o mesmo nome da classe e corre ao criar o objeto. Propriedades só com get são definidas aqui (imutáveis).

Static
public static class Matematica {
    public static int Dobro(int x) => x * 2;
    public const double Pi = 3.14159;
}

// Usa sem criar instância
int r = Matematica.Dobro(5);

Membros static pertencem à classe, não a uma instância. Uma classe static só tem membros estáticos e não pode ser instanciada.

required (C# 11)
public class Cliente {
    public required string Nome { get; init; }
    public required string Email { get; init; }
    public string? Telefone { get; init; }  // opcional
}

// Obriga a definir Nome e Email
var c = new Cliente {
    Nome = "Ana",
    Email = "ana@mail.com"
};
// new Cliente();  // ERRO: faltam propriedades required

O modificador required obriga a definir essas propriedades no inicializador. O compilador avisa se esqueceres alguma — evita objetos incompletos.

Construtor primário (C# 12)
public class Pessoa(string nome, int idade) {
    public string Nome => nome;
    public int Idade => idade;

    public string Apresentar() => $"Sou {nome}";
}

O construtor primário declara os parâmetros (ex.: nome, idade) logo na classe. Ficam disponíveis em todo o corpo. Sintaxe bem mais curta.

Struct
public struct Ponto {
    public int X { get; set; }
    public int Y { get; set; }
}

var p = new Ponto { X = 1, Y = 2 };

Uma struct é um tipo de valor (como int), copiada ao atribuir. Ideal para dados pequenos e simples; classes são tipos de referência.

Record
public record Ponto(int X, int Y);

var p = new Ponto(1, 2);
var p2 = p with { X = 5 };  // cópia com X=5

Console.WriteLine(p);  // Ponto { X = 1, Y = 2 }
Console.WriteLine(p == new Ponto(1, 2));  // true

Um record é um tipo imutável com igualdade por valor (compara o conteúdo, não a referência). O with cria uma cópia alterada.

Propriedades init-only
public class Pessoa {
    public string Nome { get; init; }
    public int Idade { get; init; }
}

// Só podem ser definidas na criação
var p = new Pessoa {
    Nome = "Ana",
    Idade = 30
};

// p.Nome = "Rui";  // ERRO de compilação!

O modificador init (C# 9+) permite definir a propriedade apenas na inicialização. Depois disso fica imutável — ideal para objetos só de leitura.

Herança e Interfaces


8 cards
Herança
public class Animal {
    public virtual void Falar() {
        Console.WriteLine("...");
    }
}

public class Cao : Animal {
    public override void Falar() {
        Console.WriteLine("Au!");
    }
}

Uma classe herda de outra com :. O método virtual na base pode ser override (substituído) na derivada.

sealed
public sealed class Final {
    // não pode ser herdada
}

// Também pode selar um método
public class A {
    public sealed override void Falar() { }
}

Uma classe sealed não pode ser herdada. Um método sealed override impede que seja substituído mais abaixo na hierarquia.

Classe abstrata
public abstract class Forma {
    public abstract double Area();

    public void Descrever() {
        Console.WriteLine($"Área: {Area()}");
    }
}

public class Circulo : Forma {
    public override double Area() => 3.14;
}

Uma classe abstract não pode ser instanciada e pode ter métodos abstratos (sem corpo) que as filhas são obrigadas a implementar.

is e as
object obj = new Cao();

// is: verifica o tipo (e pode extrair)
if (obj is Cao cao) {
    cao.Falar();
}

// as: converte (null se falhar)
Cao? c = obj as Cao;
if (c != null) c.Falar();

is verifica se um objeto é de um tipo (e extrai a variável). as tenta converter e devolve null se não conseguir.

Interface
public interface IAnimal {
    void Falar();
    string Nome { get; }
}

public class Cao : IAnimal {
    public string Nome => "Rex";
    public void Falar() => Console.WriteLine("Au");
}

Uma interface é um contrato: define o que uma classe deve ter, sem implementar. Uma classe pode implementar várias interfaces.

Interfaces com implementação default
public interface ILog {
    void Escrever(string msg);

    // Implementação por defeito (C# 8+)
    void Erro(string msg) {
        Escrever($"ERRO: {msg}");
    }
}

Desde o C# 8, interfaces podem ter métodos com corpo (implementação default). As classes herdam-nos sem precisar de os reimplementar.

base
public class Animal {
    public Animal(string nome) {
        Nome = nome;
    }
    public string Nome { get; }
}

public class Cao : Animal {
    public Cao(string nome) : base(nome) {
        // chama o construtor da base
    }
}

O base(...) chama o construtor da classe pai. O base.Método() chama um método da classe pai.

Polimorfismo na prática
public abstract class Animal {
    public abstract string Falar();
}
public class Cao : Animal {
    public override string Falar() => "Au!";
}
public class Gato : Animal {
    public override string Falar() => "Miau!";
}

// Cada objeto responde à sua maneira
var animais = new List<Animal> { new Cao(), new Gato() };
foreach (var a in animais) {
    Console.WriteLine(a.Falar());  // Au! / Miau!
}

Polimorfismo é tratar objetos diferentes de forma uniforme: a variável é do tipo Animal, mas cada subclasse executa o seu próprio override.

Coleções e LINQ


12 cards
List<T>
var lista = new List<int> { 1, 2, 3 };

lista.Add(4);          // adiciona
lista.Remove(1);       // remove o valor 1
lista.RemoveAt(0);     // remove pelo índice
lista.Contains(2);     // true
lista.Count;           // quantidade
int primeiro = lista[0];

A List<T> é uma lista dinâmica (cresce sozinha). O <T> define o tipo dos elementos. Mais flexível que um array.

LINQ: agregação
var nums = new List<int> { 1, 5, 8, 12 };

nums.Count(x => x > 5)           // 2
nums.Any(x => x > 10)            // true (existe?)
nums.All(x => x > 0)             // true (todos?)
nums.FirstOrDefault(x => x > 5)  // 8
nums.Sum()                       // 26
nums.Average()                   // 6.5
nums.Max()                       // 12

Operações que reduzem a coleção a um valor: Count, Sum, Average, Max. Any/All testam condições.

LINQ: GroupBy
var pessoas = new[] {
    new { Nome = "Ana", Cidade = "Lisboa" },
    new { Nome = "Rui", Cidade = "Porto" },
    new { Nome = "Eva", Cidade = "Lisboa" },
};

var porCidade = pessoas
    .GroupBy(p => p.Cidade)
    .Select(g => new {
        Cidade = g.Key,
        Total = g.Count(),
        Nomes = string.Join(", ", g.Select(x => x.Nome))
    });

GroupBy agrupa elementos por uma chave. Cada grupo (IGrouping) tem a Key e os elementos — usa Count(), Select, etc. dentro de cada um.

Dictionary
var dict = new Dictionary<string, int> {
    ["um"] = 1,
    ["dois"] = 2
};

dict["tres"] = 3;                  // adicionar
int v = dict["um"];                // aceder
dict.TryGetValue("um", out int r); // seguro
dict.ContainsKey("um");            // true

O Dictionary guarda pares chave/valor com acesso rápido pela chave. O TryGetValue evita erros se a chave não existir.

LINQ: ordenação
var pessoas = new List<Pessoa> { /* ... */ };

var ordenadas = pessoas
    .OrderBy(p => p.Nome)            // ascendente
    .ThenByDescending(p => p.Idade)  // depois idade (desc)
    .ToList();

var top = pessoas
    .OrderByDescending(p => p.Idade)
    .Take(3);

OrderBy ordena de forma ascendente e OrderByDescending descendente. ThenBy adiciona desempates. Take limita os resultados.

LINQ: Distinct, Union e Except
var a = new[] { 1, 2, 3, 3, 4 };
var b = new[] { 3, 4, 5 };

a.Distinct()        // 1, 2, 3, 4 (sem repetidos)
a.Union(b)          // 1, 2, 3, 4, 5 (junção sem duplicados)
a.Intersect(b)      // 3, 4 (em comum)
a.Except(b)         // 1, 2 (em A mas não em B)

Operações de conjuntos: Distinct remove duplicados, Union junta, Intersect devolve os comuns e Except subtrai.

Array
int[] nums = { 1, 2, 3 };
nums[0] = 10;
nums.Length;  // 3

// Criar com tamanho fixo
int[] arr = new int[5];

// Percorrer
for (int i = 0; i < nums.Length; i++) {
    Console.WriteLine(nums[i]);
}

Um array tem tamanho fixo (definido à criação) e é mais rápido que uma lista. Usa Length para o tamanho.

HashSet, Queue e Stack
// HashSet: valores únicos
var set = new HashSet<int> { 1, 2, 2, 3 };  // {1,2,3}

// Queue: FIFO (primeiro a entrar, primeiro a sair)
var fila = new Queue<string>();
fila.Enqueue("A");
fila.Dequeue();  // "A"

// Stack: LIFO (último a entrar, primeiro a sair)
var pilha = new Stack<int>();
pilha.Push(1);
pilha.Pop();  // 1

HashSet guarda valores únicos. Queue é uma fila (FIFO) e Stack uma pilha (LIFO). Cada uma serve um padrão de acesso.

LINQ: First, Single e Last
var nums = new[] { 1, 2, 3, 4 };

nums.First(x => x > 2)            // 3
nums.FirstOrDefault(x => x > 9)   // 0 (não lança erro)
nums.Single(x => x == 4)          // 4 (erro se houver 2)
nums.Last()                       // 4
nums.ElementAt(1)                 // 2

// Prefere as versões OrDefault para evitar exceções

First devolve o primeiro, Last o último, Single o único (lança erro se houver vários). As versões OrDefault devolvem o valor padrão em vez de lançar exceção.

LINQ: Where / Select
using System.Linq;

var nums = new List<int> { 1, 5, 8, 12, 3 };

var resultado = nums
    .Where(x => x > 5)      // filtra: 8, 12
    .Select(x => x * 2)     // transforma: 16, 24
    .ToList();

O LINQ faz consultas sobre coleções. Where filtra e Select transforma cada elemento. Encadeia-se em cadeia.

IEnumerable vs List
// IEnumerable: só leitura, avaliação preguiçosa
IEnumerable<int> Pares() {
    yield return 2;
    yield return 4;
}

// List: em memória, permite Add/Remove
var lista = new List<int> { 1, 2, 3 };

// LINQ devolve IEnumerable (não executa já)
var consulta = lista.Where(x => x > 1);
var materializada = consulta.ToList();  // executa agora

IEnumerable<T> é só de leitura e avalia de forma preguiçosa (yield). List<T> está em memória e permite alterações. ToList() materializa uma consulta.

LINQ: Join
var clientes = new[] { new { Id = 1, Nome = "Ana" } };
var pedidos = new[] { new { ClienteId = 1, Total = 50 } };

var resultado = clientes.Join(
    pedidos,
    c => c.Id,          // chave de clientes
    p => p.ClienteId,   // chave de pedidos
    (c, p) => new { c.Nome, p.Total }
);

// Alternativa legível (query syntax)
var q = from c in clientes
        join p in pedidos on c.Id equals p.ClienteId
        select new { c.Nome, p.Total };

Join cruza duas coleções por uma chave comum (como um JOIN de SQL). A query syntax com join ... on ... equals é mais legível.

Exceções


7 cards
try / catch / finally
try {
    int r = 10 / 0;
} catch (DivideByZeroException ex) {
    Console.WriteLine($"Erro: {ex.Message}");
} catch (Exception ex) {
    Console.WriteLine("Outro erro");
} finally {
    Console.WriteLine("executa sempre");
}

O try testa código; o catch trata o erro (podes capturar tipos específicos). O finally corre sempre, haja erro ou não.

Exceção personalizada
public class SaldoInsuficienteException : Exception {
    public SaldoInsuficienteException(string msg)
        : base(msg) { }
}

throw new SaldoInsuficienteException(
    "Saldo insuficiente");

Cria exceções próprias herdando de Exception. Útil para erros específicos da tua aplicação, com nomes claros.

throw
void DefinirIdade(int idade) {
    if (idade < 0) {
        throw new ArgumentException(
            "A idade não pode ser negativa");
    }
}

// Exceções comuns:
// ArgumentNullException
// InvalidOperationException

Usa throw para lançar uma exceção quando algo está errado. Isto interrompe a execução e informa quem chamou o método.

try/finally sem catch
var ligacao = AbrirLigacao();
try {
    // usa a ligação (pode lançar erro)
    ligacao.Executar();
} finally {
    // corre SEMPRE (mesmo com exceção)
    ligacao.Fechar();
}

// Melhor ainda: using (faz isto sozinho)
using var l2 = AbrirLigacao();
l2.Executar();

finally sem catch garante a limpeza de recursos sem engolir o erro (a exceção continua a propagar). O using é a alternativa preferida.

using (recursos)
// Forma clássica
using (var fs = File.OpenRead("f.txt")) {
    // usa o ficheiro
}  // fecha automaticamente

// Forma moderna (C# 8+)
using var sr = new StreamReader("f.txt");
var texto = sr.ReadToEnd();

O using liberta recursos automaticamente (ficheiros, ligações) no fim do bloco, mesmo com erro. Funciona com objetos IDisposable.

Boas práticas com exceções
// 1. Captura tipos específicos primeiro
catch (FileNotFoundException ex) { /* ... */ }
catch (Exception ex) { /* genérico por último */ }

// 2. Relança sem perder a stack trace
try { /* ... */ }
catch (Exception) {
    throw;        // certo (preserva a origem)
    // throw ex;   // errado (reinicia a stack)
}

// 3. Nunca faças isto:
catch { }  // engole o erro em silêncio

Captura exceções específicas antes das genéricas. Usa throw; (sem a variável) para relançar preservando a stack trace. Nunca ignores erros em silêncio.

Filtros de exceção
try {
    // código de risco
} catch (Exception ex) when (ex is IOException) {
    Console.WriteLine("Erro de IO");
} catch (Exception ex) when (ex.Message.Contains("timeout")) {
    Console.WriteLine("Tempo esgotado");
}

A cláusula when filtra exceções por uma condição. Só captura se a condição for verdadeira — mais preciso que vários catch.

Async / Await


8 cards
async / await
async Task<string> LerAsync() {
    string texto = await File.ReadAllTextAsync("f.txt");
    return texto;
}

// Chamar
string conteudo = await LerAsync();

O async/await permite código assíncrono que parece síncrono. O await espera a tarefa sem bloquear a thread (útil em UI e web).

Cancelamento
var cts = new CancellationTokenSource();

async Task TrabalhoAsync(CancellationToken token) {
    while (!token.IsCancellationRequested) {
        await Task.Delay(100, token);
        // faz trabalho...
    }
}

cts.Cancel();  // pede cancelamento

O CancellationToken permite cancelar tarefas a meio. Passa-o para os métodos async e verifica IsCancellationRequested.

Task.WhenAll
async Task BuscarTudo() {
    Task<string> t1 = BuscarA();
    Task<string> t2 = BuscarB();

    await Task.WhenAll(t1, t2);  // espera ambas

    string a = t1.Result;
    string b = t2.Result;
}

Task.WhenAll espera várias tarefas em paralelo. Mais rápido que esperar uma de cada vez quando são independentes.

Task.WhenAny
Task<string> t1 = BuscarServidorA();
Task<string> t2 = BuscarServidorB();

// Espera a PRIMEIRA a terminar
Task<string> vencedora = await Task.WhenAny(t1, t2);
string resultado = await vencedora;

// Timeout com WhenAny
var tarefa = BuscarDados();
var timeout = Task.Delay(5000);
if (await Task.WhenAny(tarefa, timeout) == timeout) {
    Console.WriteLine("Tempo esgotado");
}

Task.WhenAny devolve a primeira tarefa a terminar — útil para redundância (pedir a 2 servidores) ou para implementar timeouts.

Task.Run
async Task Processar() {
    // Trabalho pesado noutra thread
    int resultado = await Task.Run(() => {
        return CalcularPesado();
    });
}

Task.Run executa trabalho pesado numa thread separada, libertando a thread principal (ex.: para a interface não "congelar").

await foreach (IAsyncEnumerable)
// Produz valores de forma assíncrona
async IAsyncEnumerable<int> GerarAsync() {
    for (int i = 0; i < 5; i++) {
        await Task.Delay(100);
        yield return i;
    }
}

// Consome de forma assíncrona
await foreach (var n in GerarAsync()) {
    Console.WriteLine(n);
}

IAsyncEnumerable<T> é uma sequência assíncrona: cada elemento pode ser produzido com await. Consome-se com await foreach (ideal para streams e paginação).

ValueTask
async ValueTask<int> ObterAsync() {
    if (_cache != 0) return _cache;  // síncrono
    return await BuscarDaRede();
}

ValueTask é uma alternativa mais leve a Task quando o resultado muitas vezes já está disponível (evita alocações). Usa com moderação.

async void: cuidados
// ERRADO: não dá para await nem capturar erros
async void Enviar() {
    await Task.Delay(100);
    throw new Exception();  // crasha o processo!
}

// CERTO: devolve Task
async Task EnviarAsync() {
    await Task.Delay(100);
}

await EnviarAsync();  // pode esperar e capturar

Evita async void: não dá para fazer await e as exceções escapam (podem crashar a aplicação). Usa async Taskasync void só em handlers de eventos.

Dicas e Boas Práticas


9 cards
Null-coalescing (??)
string nome = null;
string s = nome ?? "Anónimo";  // "Anónimo"

// Atribui se for null
List<int> lista = null;
lista ??= new List<int>();

// Encadear
string r = a ?? b ?? c ?? "default";

O ?? devolve o valor da esquerda se não for null, senão o da direita. O ??= só atribui se a variável for null.

Properties com lógica
private int _idade;
public int Idade {
    get => _idade;
    set => _idade = value > 0 ? value : 0;
}

// Só leitura calculada
public string NomeCompleto => $"{Nome} {Apelido}";

Uma propriedade pode ter lógica no get/set (validação, cálculo). O value é o valor recebido no set. => cria propriedades só de leitura.

Pattern matching com switch
string Classificar(object obj) => obj switch {
    int n when n < 0     => "negativo",
    int n                => "positivo",
    string { Length: 0 } => "string vazia",
    string s             => $"texto: {s}",
    null                 => "nulo",
    _                    => "outro tipo"
};

Classificar(-5);   // "negativo"
Classificar("");   // "string vazia"

switch expression com padrões: combina tipo (int n), propriedade ({ Length: 0 }), condição (when) e null. Bem mais poderoso que o switch tradicional.

Null-conditional (?.)
string nome = null;

int? len = nome?.Length;   // null (sem erro!)
lista?.Add(1);             // só adiciona se lista != null

// Encadear
string cidade = pessoa?.Morada?.Cidade;

O ?. acede a um membro só se o objeto não for null — senão devolve null. Evita o temido NullReferenceException.

using static
using static System.Console;
using static System.Math;

WriteLine("Olá");     // em vez de Console.WriteLine
double r = Sqrt(16);  // em vez de Math.Sqrt

O using static importa os membros estáticos de uma classe. Permite usá-los sem repetir o nome da classe.

Pattern matching
object obj = 42;

// Padrão com condição
if (obj is int n and > 0) {
    Console.WriteLine($"positivo: {n}");
}

// Padrão de tipo com propriedade
if (obj is string { Length: > 3 } s) {
    Console.WriteLine(s);
}

O pattern matching verifica tipo, valor e propriedades numa só expressão. Combina-se com and, or e not.

var vs tipo explícito
// var: tipo inferido
var lista = new List<int>();
var cliente = ObterCliente();

// Tipo explícito: mais claro
int total = Calcular();
List<int> nums = new();  // target-typed new (C# 9+)

Usa var quando o tipo é óbvio (ex.: new List<int>()). Usa o tipo explícito quando torna o código mais legível. new() omite o tipo à direita.

Strings literais (C# 11+)
string json = """
    {
        "nome": "Ana",
        "idade": 30
    }
    """;

// Com interpolação
string s = $"""
    Olá {nome}
    """;

Strings com """ (três aspas) preservam quebras de linha e aspas sem escapar. Ideais para JSON, SQL ou HTML no código.

Ranges e índices (C# 8)
var nums = new[] { 0, 1, 2, 3, 4, 5 };

nums[^1]      // 5 (último)
nums[^2]      // 4 (penúltimo)

nums[1..4]    // {1, 2, 3} (índices 1 a 3)
nums[..3]     // {0, 1, 2} (do início até 3)
nums[3..]     // {3, 4, 5} (do 3 até ao fim)
nums[..]      // cópia de todos

Range r = 2..5;
nums[r]       // {2, 3, 4}

O operador ^ conta a partir do fim (^1 é o último). .. cria um intervalo (Range) para extrair sub-arrays. Disponível desde o C# 8.

Strings


8 cards
StringBuilder
using System.Text;

var sb = new StringBuilder();
sb.Append("Olá");          // junta sem criar novo objeto
sb.Append(' ');
sb.AppendLine("Mundo");    // + quebra de linha
sb.Insert(0, ">>> ");      // insere numa posição
sb.Replace("Mundo", "C#"); // substitui

string resultado = sb.ToString();

StringBuilder constrói texto de forma eficiente quando há muitas concatenações (não cria um objeto novo a cada Append, ao contrário do +). Essencial dentro de loops.

Métodos char
char c = '7';

char.IsDigit(c)            // true
char.IsLetter('a')         // true
char.IsLetterOrDigit('?')  // false
char.IsUpper('A')          // true
char.IsWhiteSpace(' ')     // true

char.ToUpper('a')          // 'A'
char.ToLower('A')          // 'a'

// Validar se uma string tem dígitos
"abc123".Any(char.IsDigit)   // true

A classe char tem utilitários para validar caracteres: IsDigit, IsLetter, IsUpper, IsWhiteSpace. ToUpper/ToLower convertem.

Split e Join
string csv = "maçã,pera,uvas";
string[] frutas = csv.Split(',');       // divide

// Ignorar entradas vazias
"a,,b".Split(',', StringSplitOptions.RemoveEmptyEntries);

// Juntar de volta
string unido = string.Join(" | ", frutas);
// "maçã | pera | uvas"

string linha = string.Join(",", new[] { "x", "y" });

Split divide uma string por um separador. string.Join junta partes com um delimitador. RemoveEmptyEntries ignora partes vazias.

Comparação de strings
string a = "Olá";
string b = "olá";

a == b              // false (maiúsculas importam)
a.Equals(b)         // false

// Ignorar maiúsculas/minúsculas
a.Equals(b, StringComparison.OrdinalIgnoreCase);  // true

a.StartsWith("Ol")  // true
a.EndsWith("á")     // true
a.Contains("LÁ", StringComparison.OrdinalIgnoreCase);  // true

Por padrão, a comparação distingue maiúsculas. Usa StringComparison.OrdinalIgnoreCase para ignorar. StartsWith/EndsWith verificam os extremos.

Substring e IndexOf
string s = "Olá Mundo C#";

s.Substring(4)       // "Mundo C#"
s.Substring(4, 5)    // "Mundo"

s.IndexOf("Mundo")   // 4 (posição)
s.LastIndexOf("o")   // última ocorrência

// Extrair domínio de um email
string email = "ana@mail.com";
string dominio = email.Substring(email.IndexOf('@') + 1);

Substring extrai uma parte (posição inicial e, opcionalmente, comprimento). IndexOf devolve a posição da primeira ocorrência ou -1 se não existir.

string.Format
double preco = 1234.5;

string.Format("Preço: {0:C}", preco);   // moeda
string.Format("{0,10}", "dir");         // alinhado à direita
string.Format("{0} e {1}", "A", "B");   // múltiplos

// Equivalente com interpolação
$"Preço: {preco:C}";

// Formatos numéricos comuns
$"{1234.5:N2}"   // "1,234.50"
$"{0.85:P0}"     // "85%"
$"{255:X}"       // "FF" (hexadecimal)

string.Format substitui {0}, {1} pelos valores. Os formatos :C (moeda), :N2 (número), :P0 (percentagem) e :X (hex) controlam a saída.

Padding e Trim
"42".PadLeft(5, '0')     // "00042"
"Olá".PadRight(8, '.')   // "Olá....."

"  texto  ".Trim()       // "texto"
"  texto  ".TrimStart()  // "texto  "
"  texto  ".TrimEnd()    // "  texto"

// Alinhamento na interpolação
$"{"esq",-8}|"   // "esq     |" (esquerda)
$"{42,5}"        // "   42" (direita)

PadLeft/PadRight preenchem com um carácter até ao comprimento. Trim remove espaços. Na interpolação, ,N alinha (negativo = esquerda).

Concatenação
string a = "Olá";
string b = "Mundo";

string r1 = a + " " + b;              // operador +
string r2 = string.Concat(a, " ", b); // método
string r3 = $"{a} {b}";               // interpolação (preferida)

// Juntar várias strings
var partes = new[] { "a", "b", "c" };
string r4 = string.Concat(partes);    // "abc"

// Em loops, usa StringBuilder

Para casos simples usa + ou interpolação ($""). string.Concat junta várias strings. Dentro de loops, prefere StringBuilder por desempenho.

Genéricos


5 cards
Classes genéricas
public class Caixa<T> {
    public T Conteudo { get; set; }
}

var caixaInt = new Caixa<int> { Conteudo = 42 };
var caixaStr = new Caixa<string> { Conteudo = "olá" };

// O tipo é definido no uso
int n = caixaInt.Conteudo;      // int (sem cast)
string s = caixaStr.Conteudo;   // string

Uma classe genérica usa um parâmetro de tipo <T> definido no momento do uso. O mesmo código funciona para qualquer tipo, com segurança (sem object nem casts).

Interfaces genéricas
// As coleções do .NET assentam em interfaces genéricas
IEnumerable<int> seq = new List<int>();
IList<string> lista = new List<string>();
IDictionary<string, int> mapa = new Dictionary<string, int>();
IComparer<int> comp = Comparer<int>.Default;

// Programar contra a interface (flexível)
int Somar(IEnumerable<int> nums) => nums.Sum();

As coleções do .NET são construídas sobre interfaces genéricas: IEnumerable<T>, IList<T>, IDictionary<K,V>. Programar contra a interface torna o código flexível.

Métodos genéricos
// O tipo é inferido pelo argumento
T Primeiro<T>(T[] itens) => itens[0];

Primeiro(new[] { 1, 2, 3 });       // int
Primeiro(new[] { "a", "b" });      // string

// Trocar dois valores de qualquer tipo
void Trocar<T>(ref T a, ref T b) {
    (a, b) = (b, a);
}

Um método genérico declara <T> a seguir ao nome. O compilador infere o tipo a partir dos argumentos — não precisas de o indicar. Funciona para qualquer tipo.

Constraints (where)
// T tem de ser uma classe
public class Repositorio<T> where T : class { }

// T tem de implementar IComparable
T Max<T>(T a, T b) where T : IComparable<T> {
    return a.CompareTo(b) >= 0 ? a : b;
}

// Várias restrições em simultâneo
void Processar<T>(T item)
    where T : class, IDisposable, new() { }

where restringe os tipos aceites: class, struct, new() (construtor sem parâmetros), interfaces ou uma classe base. Permite usar membros específicos com segurança.

default(T)
default(int)       // 0
default(string)    // null
default(bool)      // false
default(DateTime)  // 01/01/0001

// Essencial em genéricos (tipo desconhecido)
T? Encontrar<T>(List<T> lista, Func<T, bool> filtro) {
    foreach (var item in lista) {
        if (filtro(item)) return item;
    }
    return default;   // não encontrado
}

default(T) devolve o valor padrão de um tipo: 0 para números, null para referências, false para bool. Indispensável em código genérico.

Delegates e Eventos


6 cards
Expressões lambda
// Lambda: função anónima compacta
Func<int, int> dobro = x => x * 2;
dobro(5);  // 10

// Com vários parâmetros
Func<int, int, int> soma = (a, b) => a + b;

// Com corpo (bloco)
Func<int, string> classifica = n => {
    if (n >= 10) return "aprovado";
    return "reprovado";
};

// Muito usadas no LINQ
nums.Where(x => x > 5);

Uma lambda (x => expressão) é uma função anónima e concisa. É muito usada no LINQ e como argumento de métodos que recebem Func/Action.

Delegates personalizados
// Definir um delegate (assinatura de método)
public delegate int Operacao(int a, int b);

int Somar(int a, int b) => a + b;
int Multiplicar(int a, int b) => a * b;

Operacao op = Somar;   // aponta para Somar
op(2, 3);              // 5

op = Multiplicar;      // agora aponta para Multiplicar
op(2, 3);              // 6

// Em código novo, prefere Func<int, int, int>

Um delegate define uma assinatura de método que pode ser referenciada. Em código novo prefere Func/Action — delegates próprios ainda são usados em APIs públicas e eventos.

Action
// Action: delegate sem retorno (void)
Action<string> imprimir = msg => Console.WriteLine(msg);
imprimir("Olá");

// Com vários parâmetros
Action<int, int> somar = (a, b) => Console.WriteLine(a + b);

// Passar comportamento como argumento
void Executar(Action acao) => acao();
Executar(() => Console.WriteLine("feito"));

// List.ForEach usa Action
new List<int> { 1, 2 }.ForEach(n => Console.WriteLine(n));

Action é um delegate que representa um método sem retorno (void), com 0 a 16 parâmetros. Ideal para passar comportamento como argumento.

Eventos (event)
public class Temporizador {
    // Evento (usa o delegate EventHandler)
    public event EventHandler? Tick;

    public void Disparar() {
        Tick?.Invoke(this, EventArgs.Empty);
    }
}

// Subscrever (+=) e desubscrever (-=)
var t = new Temporizador();
t.Tick += (s, e) => Console.WriteLine("tick!");
t.Disparar();   // "tick!"

event expõe uma notificação que código externo pode subscrever (+=) ou cancelar (-=). Dispara-se com Invoke. É a base do padrão observador no .NET.

Func
// Func: delegate COM retorno (último tipo = retorno)
Func<int> obter = () => 42;
obter();  // 42

Func<int, int> dobro = x => x * 2;          // int -> int
Func<string, int, bool> maior = (s, n) => s.Length > n;

// Uso prático: receber um critério
int[] Filtrar(int[] nums, Func<int, bool> criterio) {
    return nums.Where(criterio).ToArray();
}
Filtrar(new[] { 1, 5, 8 }, x => x > 3);  // {5, 8}

Func<T, TResult> é um delegate com retorno — o último tipo genérico é o tipo devolvido. Usa-se para passar transformações e critérios a métodos.

Predicate
// Predicate: recebe T, devolve bool
Predicate<int> isPar = n => n % 2 == 0;
isPar(4);  // true

// Métodos de lista que usam Predicate
var nums = new List<int> { 1, 2, 3, 4, 5 };
nums.Find(isPar);            // 2 (primeiro par)
nums.FindAll(isPar);         // {2, 4}
nums.Exists(isPar);          // true
nums.RemoveAll(n => n < 3);  // remove 1 e 2

Predicate<T> equivale a Func<T, bool>: recebe um valor e devolve true/false. É usado em Find, FindAll, Exists e RemoveAll.

Ficheiros e JSON


8 cards
Ler ficheiro
using System.IO;

// Ler tudo de uma vez (ficheiros pequenos)
string texto = File.ReadAllText("notas.txt");

// Versão assíncrona (não bloqueia a UI)
string texto2 = await File.ReadAllTextAsync("notas.txt");

// Ler como bytes
byte[] dados = File.ReadAllBytes("imagem.png");

File.ReadAllText lê o ficheiro inteiro para uma string. Para ficheiros grandes usa ReadLines ou StreamReader. A versão Async não bloqueia.

Path
string caminho = @"C:\dados\foto.jpg";

Path.GetFileName(caminho)                  // "foto.jpg"
Path.GetExtension(caminho)                 // ".jpg"
Path.GetFileNameWithoutExtension(caminho)  // "foto"
Path.GetDirectoryName(caminho)             // "C:\dados"

// Juntar caminhos com segurança
string novo = Path.Combine("C:", "dados", "f.txt");

// Pasta temporária do sistema
Path.GetTempPath();

Path manipula caminhos: GetFileName, GetExtension, GetDirectoryName. Path.Combine junta partes com o separador correto.

Escrever ficheiro
// Sobrescreve (ou cria) o ficheiro
File.WriteAllText("saida.txt", "Olá Mundo");

// Acrescentar ao fim (append)
File.AppendAllText("log.txt", "nova linha\n");

// Versão assíncrona
await File.WriteAllTextAsync("saida.txt", "conteúdo");

// Escrever bytes
File.WriteAllBytes("dados.bin", new byte[] { 1, 2, 3 });

File.WriteAllText cria ou sobrescreve; File.AppendAllText acrescenta ao fim. Ambos fecham o ficheiro automaticamente.

Verificar existência
// Verificar antes de usar
if (File.Exists("config.json")) {
    var json = File.ReadAllText("config.json");
}

if (!Directory.Exists("logs")) {
    Directory.CreateDirectory("logs");
}

// Detalhes sobre o ficheiro
var info = new FileInfo("foto.jpg");
info.Length;         // tamanho em bytes
info.LastWriteTime;  // última modificação

File.Exists e Directory.Exists verificam a existência. FileInfo dá detalhes como o tamanho (Length) e a data (LastWriteTime).

Linhas (Read/WriteAllLines)
// Ler como array de linhas
string[] linhas = File.ReadAllLines("dados.csv");

// Ler de forma preguiçosa (eficiente em memória)
foreach (var linha in File.ReadLines("grande.csv")) {
    if (linha.StartsWith("#")) continue;
}

// Escrever várias linhas (cada elemento = 1 linha)
File.WriteAllLines("lista.txt", new[] { "a", "b", "c" });

ReadAllLines devolve um array de linhas. ReadLines lê linha a linha de forma preguiçosa — ideal para ficheiros grandes. WriteAllLines escreve cada elemento como uma linha.

JSON: serializar
using System.Text.Json;

var pessoa = new { Nome = "Ana", Idade = 30 };

// Objeto -> JSON
string json = JsonSerializer.Serialize(pessoa);
// {"Nome":"Ana","Idade":30}

// Com opções (indentado)
var opcoes = new JsonSerializerOptions { WriteIndented = true };
string bonito = JsonSerializer.Serialize(pessoa, opcoes);

JsonSerializer.Serialize (de System.Text.Json) converte um objeto em JSON. A opção WriteIndented formata com indentação.

Directory
// Criar pasta
Directory.CreateDirectory("dados/backup");

// Listar ficheiros (com filtro opcional)
string[] ficheiros = Directory.GetFiles("dados");
string[] soTxt = Directory.GetFiles("dados", "*.txt");

// Listar subpastas
string[] pastas = Directory.GetDirectories("dados");

// Apagar pasta (true = recursivo)
Directory.Delete("dados/backup", true);

Directory gere pastas: CreateDirectory, GetFiles (aceita filtros como *.txt), GetDirectories e Delete.

JSON: desserializar
using System.Text.Json;

string json = """{"Nome":"Ana","Idade":30}""";

// JSON -> objeto (classe ou record)
var pessoa = JsonSerializer.Deserialize<Pessoa>(json);
Console.WriteLine(pessoa!.Nome);  // "Ana"

// Ignorar maiúsculas/minúsculas nos nomes
var opcoes = new JsonSerializerOptions {
    PropertyNameCaseInsensitive = true
};

JsonSerializer.Deserialize<T> converte JSON num objeto do tipo T. PropertyNameCaseInsensitive aceita nomes de propriedades em qualquer caixa.

Datas e Tempo


6 cards
DateTime atual
DateTime agora = DateTime.Now;    // data e hora local
DateTime hoje = DateTime.Today;   // só a data (00:00)
DateTime utc = DateTime.UtcNow;   // hora UTC

agora.Year        // 2026
agora.Month       // 7
agora.Day         // 24
agora.Hour        // 14
agora.Minute      // 30
agora.DayOfWeek   // Thursday
agora.DayOfYear   // dia do ano

DateTime.Now é a data/hora local e DateTime.UtcNow é UTC. Acede aos componentes com Year, Month, Day, Hour e DayOfWeek.

Formatar datas
var d = new DateTime(2026, 7, 24, 15, 30, 0);

d.ToString("dd/MM/yyyy")        // "24/07/2026"
d.ToString("dd/MM/yyyy HH:mm")  // "24/07/2026 15:30"
d.ToString("yyyy-MM-dd")        // "2026-07-24" (ISO)
d.ToString("dddd")              // "sexta-feira"
d.ToString("MMM")               // "jul"

// Formatos padrão
d.ToShortDateString()   // "24/07/2026"
d.ToLongTimeString()    // "15:30:00"

// Na interpolação
$"{d:dd/MM/yyyy}";

Formata com ToString("..."): dd dia, MM mês, yyyy ano, HH:mm hora. Os mesmos formatos funcionam na interpolação ({d:dd/MM/yyyy}).

Criar datas
// Criar uma data específica
var natal = new DateTime(2026, 12, 25);
var reuniao = new DateTime(2026, 7, 24, 15, 30, 0);

// DateOnly e TimeOnly (C# 10+)
var data = new DateOnly(2026, 12, 25);
var hora = new TimeOnly(15, 30);

// Úteis
natal.DayOfWeek               // Friday
DateTime.DaysInMonth(2026, 2) // 28 (fev)

Cria uma data com new DateTime(ano, mês, dia, hora, min, seg). DateOnly e TimeOnly (C# 10+) representam só a data ou só a hora.

Parse de datas
// Converter string em DateTime
DateTime d = DateTime.Parse("24/07/2026");

// Mais seguro (não lança exceção)
if (DateTime.TryParse("31/02/2026", out var data)) {
    Console.WriteLine(data);
} else {
    Console.WriteLine("Data inválida");
}

// Formato específico
DateTime.ParseExact("2026-07-24", "yyyy-MM-dd",
    CultureInfo.InvariantCulture);

DateTime.Parse converte uma string em data. Usa TryParse para entradas inválidas (não lança exceção). ParseExact exige o formato exato.

Somar e subtrair tempo
var hoje = DateTime.Now;

hoje.AddDays(7)      // daqui a uma semana
hoje.AddHours(-2)    // há 2 horas
hoje.AddMonths(1)
hoje.AddMinutes(90)

// Diferença entre datas (TimeSpan)
var natal = new DateTime(2026, 12, 25);
TimeSpan falta = natal - hoje;
falta.Days           // dias restantes

Soma ou subtrai tempo com AddDays, AddHours, AddMonths — devolvem um novo DateTime (não alteram o original). Subtrair duas datas dá um TimeSpan.

TimeSpan
// Representa uma duração (não uma data)
var duracao = new TimeSpan(2, 30, 0);   // 2h30m
var umDia = TimeSpan.FromDays(1);
var meiaHora = TimeSpan.FromMinutes(30);

duracao.Hours         // 2
duracao.TotalMinutes  // 150

// Operações
var soma = umDia + meiaHora;
var dobro = duracao * 2;

// Usar com DateTime
DateTime amanha = DateTime.Now + umDia;

TimeSpan representa uma duração. Cria-se com FromDays/FromMinutes ou com o construtor. TotalMinutes dá o valor total nessa unidade.

Tuplos


5 cards
Tuplos básicos
// Tuplo: agrupar valores sem criar uma classe
var ponto = (1, 2);
ponto.Item1   // 1
ponto.Item2   // 2

var pessoa = ("Ana", 30, true);
pessoa.Item1  // "Ana"

// Com tipos explícitos
(int x, int y) pos = (5, 10);

Um tuplo agrupa vários valores numa única estrutura sem criar uma classe. Sem nomes, acede-se por Item1, Item2, etc.

Retornar múltiplos valores
// Método que devolve vários valores
(int min, int max) MinMax(int[] nums) {
    return (nums.Min(), nums.Max());
}

var resultado = MinMax(new[] { 3, 1, 7, 4 });
resultado.min   // 1
resultado.max   // 7

// Ou desconstruir logo
var (menor, maior) = MinMax(new[] { 3, 1, 7, 4 });

Os tuplos são ideais para devolver vários valores de um método sem usar parâmetros out nem criar uma classe. Desconstrói-se no destino.

Campos nomeados
// Dar nomes aos campos
var pessoa = (Nome: "Ana", Idade: 30);
pessoa.Nome   // "Ana"
pessoa.Idade  // 30

// Em retornos de métodos
(string cidade, int cp) ObterLocal() {
    return ("Lisboa", 1000);
}

var local = ObterLocal();
local.cidade  // "Lisboa"

Dá nomes aos campos (Nome:, Idade:) para tornar o tuplo mais legível. Acede-se pelo nome em vez de Item1.

Deconstruction
var pessoa = (Nome: "Ana", Idade: 30);

// Extrair para variáveis separadas
var (nome, idade) = pessoa;
Console.WriteLine($"{nome}, {idade}");

// Ignorar campos com _
var (n, _) = pessoa;

// Desconstruir um KeyValuePair
foreach (var (chave, valor) in dicionario) {
    Console.WriteLine($"{chave} = {valor}");
}

A desconstrução extrai os campos do tuplo para variáveis individuais: var (nome, idade) = pessoa. Usa _ para ignorar campos.

Trocar valores (swap)
int a = 1, b = 2;

// Trocar sem variável temporária
(a, b) = (b, a);
// agora a = 2, b = 1

string x = "olá", y = "mundo";
(x, y) = (y, x);

Com tuplos, trocar dois valores é trivial: (a, b) = (b, a) faz o swap sem precisar de uma variável temporária.