Cheatsheet C#
Linguagem de programação da Microsoft
C#
Sintaxe Básica
Variables
int edad = 30; string nombre = "Ana"; double precio = 9.99; bool activo = true; char letra = 'A'; // Tipo inferido por el compilador var total = 100; // int var mensaje = "Hola"; // string
En C# declaras el tipo antes del nombre. var deja que el compilador infiera el tipo a partir del valor.
Interpolación de strings
string nombre = "Ana";
int edad = 30;
string s = $"A {nombre} tem {edad} anios";
string calc = $"Daqui a 5 anios: {edad + 5}";
// Formateo
double precio = 9.5;
string p = $"Precio: {precio:F2}€"; // 9,50€El prefijo $ permite insertar expresiones dentro de { }. Los formatos (ej.: F2) controlan los decimales.
Comentarios
// Comentario de una línea /* Comentario de varias líneas */ /// <summary> /// Documentación XML (aparece en IntelliSense) /// </summary> int Sumar(int a, int b) => a + b; // TODO: tarea pendiente // NOTE: nota importante
// comenta una línea y /* */ varias. El /// genera documentación XML que Visual Studio muestra en IntelliSense.
Tipos de datos
// Numeros enteros int n = 10; // 32 bits long grande = 9999999999L; short pequeño = 100; // Numeros con decimales float f = 3.14f; double d = 3.14; decimal precio = 9.99m; // dinero // Otros bool ok = true; char c = 'A'; string s = "texto"; object obj = 1;
Usa decimal para valores monetarios (más preciso). El sufijo (f, m, L) indica el tipo del literal.
Nullable
int? x = null; // int que acepta null
int y = x ?? 0; // 0 si x es null
int z = x!.Value; // fuerza (¡cuidado!)
// Verificar antes de usar
if (x.HasValue) {
Console.WriteLine(x.Value);
}
string? nombre = null; // referencia nullablePor defecto, los tipos de valor no aceptan null. El ? los hace nullable. El ?? da un valor alternativo si es null.
Console: entrada y salida
// Salida
Console.WriteLine("Hola!"); // con salto de línea
Console.Write("sem salto");
Console.WriteLine($"Valor: {10:F2}");
// Entrada (siempre devuelve string)
string? nombre = Console.ReadLine();
Console.WriteLine($"Hola, {nombre}");
// Leer una tecla
var tecla = Console.ReadKey();Console.WriteLine escribe y cambia de línea; Console.Write no cambia. Console.ReadLine() lee lo que se escribió (siempre como string).
Constantes
// Constante de compilación
const int Max = 100;
const string App = "Aula";
// Solo de lectura (definida en runtime)
readonly int _id;
public Persona(int id) {
_id = id;
}const es fija en tiempo de compilación. readonly se puede definir una vez (en el constructor) y después no cambia más.
Conversiones
// String a numero
int n = int.Parse("123");
double d = double.Parse("3.14");
// Más seguro (no lanza excepción)
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 en vez de Parse cuando la entrada pueda ser inválida — devuelve false en vez de lanzar una excepción.
Verbatim strings (@)
// Sin @: necesita escapar las barras string camino = "C:\\carpetas\\fichero.txt"; // Con @: literal, tal como está escrito string caminho2 = @"C:\carpetas\fichero.txt"; // Varias líneas preservadas string sql = @"SELECT * FROM clientes WHERE activo = 1";
El prefijo @ crea una string literal: las barras y saltos de línea se mantienen tal cual. Ideal para rutas de archivos y SQL.
Operadores
// Aritméticos 10 + 3 // 13 10 / 3 // 3 (¡entero!) 10.0 / 3 // 3.333 10 % 3 // 1 (resto) // Comparación 5 == 5 // true 5 != 3 // true 5 >= 3 // true // Lógicos true && false // false (Y) true || false // true (O) !true // false (NO)
Atención: 10 / 3 da 3 (división entera). Usa 10.0 / 3 para obtener decimales. && es Y y || es O.
Métodos de string útiles
string s = " Hola Mundo ";
s.Trim() // "Hola Mundo"
s.ToUpper() // " HOLA MUNDO "
s.ToLower()
s.Replace("Mundo", "C#")
s.Split(" ") // array de palabras
s.Contains("Hola") // true
s.Length // 13Las strings son inmutables: los métodos devuelven una nueva string. Trim() elimina espacios en los extremos y Split() divide en partes.
Controlo de Fluxo
if / else
int edad = 20;
if (edad >= 18) {
Console.WriteLine("adulto");
} else if (edad >= 13) {
Console.WriteLine("adolescente");
} else {
Console.WriteLine("niño");
}La condición va entre paréntesis y el bloque entre llaves. El else if encadena varias condiciones.
foreach
var frutas = new[] { "manzana", "pera", "uvas" };
foreach (var fruta in frutas) {
Console.WriteLine(fruta);
}
// Con índice (si lo necesitas)
for (int i = 0; i < frutas.Length; i++) {
Console.WriteLine($"{i}: {frutas[i]}");
}El foreach recorre cada elemento de una colección sin necesitar un índice. Más simple y legible que el for para recorrer.
Loops anidados
// Tabla de multiplicar
for (int i = 1; i <= 3; i++) {
for (int j = 1; j <= 3; j++) {
Console.Write($"{i * j,4}");
}
Console.WriteLine();
}
// break 2 no existe en C# — usa una flag o un método
for (int i = 0; i < 5; i++) {
for (int j = 0; j < 5; j++) {
if (i * j > 6) goto fin;
}
}
fin:;Los loops dentro de loops recorren rejillas y matrices. Para salir de varios niveles, extrae a un método (con return) o usa una variable de control.
switch
int dia = 1;
switch (dia) {
case 1:
Console.WriteLine("Segunda");
break;
case 2:
Console.WriteLine("Martes");
break;
default:
Console.WriteLine("Outro");
break;
}El switch compara un valor con varios case. Cada caso necesita break (o return). El default es el caso por defecto.
while / do-while
int x = 5;
while (x > 0) {
Console.WriteLine(x);
x--;
}
// Se ejecuta al menos una vez
do {
x++;
} while (x < 10);El while prueba la condición antes de ejecutar. El do-while ejecuta primero y prueba después — siempre corre al menos una vez.
switch expression (C# 8+)
int dia = 1;
string nombre = dia switch {
1 => "Segunda",
2 => "Martes",
3 => "Quarta",
_ => "Outro" // caso por defecto
};Versión moderna y compacta del switch. Usa => para devolver el valor y _ como caso por defecto. Sin break.
Operador ternario
int edad = 20; string estado = edad >= 18 ? "adulto" : "menor"; // Equivalente a: // if (edad >= 18) estado = "adulto"; // else estado = "menor";
Forma compacta de un if/else en una línea: condición ? valor_si_verdadero : valor_si_falso.
for
for (int i = 0; i < 10; i++) {
Console.WriteLine(i); // 0 a 9
}
// Descendente
for (int i = 10; i > 0; i--) {
Console.WriteLine(i);
}El for tiene tres partes: inicialización, condición e incremento. Ideal cuando sabes cuántas veces quieres repetir.
break y continue
for (int i = 0; i < 10; i++) {
if (i == 3) continue; // salta el 3
if (i == 7) break; // para en el 7
Console.WriteLine(i); // 0,1,2,4,5,6
}
foreach (var item in lista) {
if (item == null) continue;
Procesar(item);
}continue salta a la siguiente iteración; break termina el loop inmediatamente. Útiles para ignorar casos o parar pronto.
Métodos
Método simple
int Sumar(int a, int b) {
return a + b;
}
// Sin retorno (void)
void Saudar(string nombre) {
Console.WriteLine($"Hola, {nombre}!");
}
int r = Sumar(2, 3); // 5Un método declara el tipo de retorno, el nombre y los parámetros. Usa void cuando no devuelve nada.
params
int Sumar(params int[] nums) {
return nums.Sum();
}
Sumar(1, 2, 3); // 6
Sumar(1, 2, 3, 4, 5); // 15
// También acepta un array
Sumar(new[] { 1, 2, 3 });params permite pasar un numero variable de argumentos. Internamente, llegan como un array.
Recursión
// Método que se llama a sí mismo
long Factorial(int n) {
if (n <= 1) return 1; // caso base (¡obligatorio!)
return n * Factorial(n - 1);
}
Factorial(5); // 120
// Fibonacci
int Fib(int n) => n <= 1 ? n : Fib(n - 1) + Fib(n - 2);Un método recursivo se llama a sí mismo. Siempre necesita un caso base para parar, si no entra en un loop infinito (StackOverflowException).
Expression-bodied
// Cuerpo en una expresión (=>)
int Doble(int x) => x * 2;
string Saudar(string n) => $"Hola, {n}";
void Hola() => Console.WriteLine("Hola");Para métodos de una línea, usa => sin llaves ni return. Más conciso y legible.
Método asíncrono
async Task<int> GetAsync() {
await Task.Delay(1000); // simula una espera
return 42;
}
// Sin retorno
async Task ImprimirAsync() {
await Task.Delay(500);
Console.WriteLine("hecho");
}Los métodos con async devuelven Task (sin valor) o Task<T> (con valor). El await espera sin bloquear el thread.
Métodos de extensión
public static class StringExtensions {
// this en el 1er parámetro = extensión 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 un método nativo
"hola".PrimeiraMaiuscula(); // "Hola"Un método de extensión añade métodos a tipos existentes sin modificarlos. Se declara en una clase static con this en el primer parámetro.
Parámetros opcionales y nombrados
void Conectar(string host, int puerto = 3306, int timeout = 30) {
Console.WriteLine($"{host}:{puerto}");
}
Conectar("localhost"); // usa los defaults
Conectar("localhost", 5432);
Conectar(puerto: 5432, host: "db"); // por nombreLos parámetros con = valor son opcionales. Puedes pasar argumentos por nombre, en cualquier orden, para más claridad.
Métodos locales
int Calcular(int x) {
// Función dentro de la función
int Cuadrado(int n) => n * n;
return Cuadrado(x) + Cuadrado(x + 1);
}Puedes definir funciones dentro de funciones. La función local (ej.: Cuadrado) solo existe dentro del método que la crea.
ref y out
// ref: entra con valor y puede ser modificado
void Incrementar(ref int x) {
x++;
}
int n = 5;
Incrementar(ref n); // n = 6
// out: solo devuelve un valor
bool Tenta(out int resultado) {
resultado = 42;
return true;
}ref pasa por referencia (lee y escribe). out sirve solo para devolver valores — el método debe asignarle un valor.
Sobrecarga de métodos
// Mismo nombre, firmas diferentes int Sumar(int a, int b) => a + b; double Sumar(double a, double b) => a + b; int Sumar(int a, int b, int c) => a + b + c; Sumar(1, 2); // llama a la 1ª Sumar(1.5, 2.5); // llama a la 2ª Sumar(1, 2, 3); // llama a la 3ª
La sobrecarga permite varios métodos con el mismo nombre (ej.: Sumar), siempre que los parámetros difieran (tipo o cantidad). El compilador elige el correcto.
Classes e Objetos
Clase con propiedades
public class Persona {
public string Nombre { get; set; }
public int Edad { get; set; }
}
var p = new Persona {
Nombre = "Ana",
Edad = 30
};
Console.WriteLine(p.Nombre);Las auto-propiedades ({ get; set; }) crean el campo interno automáticamente. La sintaxis new { ... } inicializa las propiedades.
Enum
enum Estado { Activo, Inactivo, Suspendido }
Estado e = Estado.Activo;
if (e == Estado.Activo) {
Console.WriteLine("activo");
}
// Convertir
Estado doTexto = Enum.Parse<Estado>("Inactivo");Un enum define un conjunto de constantes nombradas. Hace el código más legible que usar numeros (0, 1, 2...).
Indexadores
public class Semana {
private readonly string[] _días = new string[7];
// Acceder con [] como un array
public string this[int índice] {
get => _días[índice];
set => _días[índice] = value;
}
}
var s = new Semana();
s[0] = "Segunda";
Console.WriteLine(s[0]);Un indexador (this[...]) permite acceder a un objeto con la sintaxis de array (obj[0]). Útil para clases que envuelven colecciones.
Constructor
public class Persona {
public string Nombre { get; }
public int Edad { get; }
public Persona(string nombre, int edad) {
Nombre = nombre;
Edad = edad;
}
}
var p = new Persona("Ana", 30);El constructor tiene el mismo nombre que la clase y se ejecuta al crear el objeto. Las propiedades solo con get se definen aquí (inmutables).
Static
public static class Matemática {
public static int Doble(int x) => x * 2;
public const double Pi = 3.14159;
}
// Usar sin crear una instancia
int r = Matemática.Doble(5);Los miembros static pertenecen a la clase, no a una instancia. Una clase static solo tiene miembros estáticos y no puede ser instanciada.
required (C# 11)
public class Cliente {
public required string Nombre { get; init; }
public required string Email { get; init; }
public string? Telefono { get; init; } // opcional
}
// Obliga a definir Nombre y Email
var c = new Cliente {
Nombre = "Ana",
Email = "ana@mail.com"
};
// new Cliente(); // ERROR: faltan propiedades requiredEl modificador required obliga a definir esas propiedades en el inicializador. El compilador avisa si olvidas alguna — evita objetos incompletos.
Constructor primario (C# 12)
public class Persona(string nombre, int edad) {
public string Nombre => nombre;
public int Edad => edad;
public string Apresentar() => $"Soy {nombre}";
}El constructor primario declara los parámetros (ej.: nombre, edad) directamente en la clase. Quedan disponibles en todo el cuerpo. Sintaxis mucho más corta.
Struct
public struct Ponto {
public int X { get; set; }
public int Y { get; set; }
}
var p = new Ponto { X = 1, Y = 2 };Una struct es un tipo de valor (como int), copiada al asignar. Ideal para datos pequeños y simples; las clases son tipos de referencia.
Record
public record Ponto(int X, int Y);
var p = new Ponto(1, 2);
var p2 = p with { X = 5 }; // copia con X=5
Console.WriteLine(p); // Ponto { X = 1, Y = 2 }
Console.WriteLine(p == new Ponto(1, 2)); // trueUn record es un tipo inmutable con igualdad por valor (compara el contenido, no la referencia). El with crea una copia modificada.
Propiedades init-only
public class Persona {
public string Nombre { get; init; }
public int Edad { get; init; }
}
// Solo pueden definirse en la creación
var p = new Persona {
Nombre = "Ana",
Edad = 30
};
// p.Nombre = "Ray"; // ¡ERROR de compilación!El modificador init (C# 9+) permite definir la propiedad solo en la inicialización. Después de eso queda inmutable — ideal para objetos de solo lectura.
Herança e Interfaces
Herencia
public class Animal {
public virtual void Hablar() {
Console.WriteLine("...");
}
}
public class Perro : Animal {
public override void Hablar() {
Console.WriteLine("Au!");
}
}Una clase hereda de otra con :. El método virtual en la base puede ser override (sustituido) en la derivada.
sealed
public sealed class Final {
// no puede ser heredada
}
// También puedes sellar un método
public class A {
public sealed override void Hablar() { }
}Una clase sealed no puede ser heredada. Un método sealed override impide que sea sobrescrito más abajo en la jerarquía.
Clase abstracta
public abstract class Forma {
public abstract double Area();
public void Describir() {
Console.WriteLine($"Área: {Area()}");
}
}
public class Círculo : Forma {
public override double Area() => 3.14;
}Una clase abstract no puede ser instanciada y puede tener métodos abstractos (sin cuerpo) que las hijas están obligadas a implementar.
is y as
object obj = new Perro();
// is: verifica el tipo (y puede extraer)
if (obj is Perro perro) {
perro.Hablar();
}
// as: convierte (null si falla)
Perro? c = obj as Perro;
if (c != null) c.Hablar();is verifica si un objeto es de un tipo (y extrae la variable). as intenta convertir y devuelve null si no puede.
Interface
public interface IAnimal {
void Hablar();
string Nombre { get; }
}
public class Perro : IAnimal {
public string Nombre => "Rex";
public void Hablar() => Console.WriteLine("Au");
}Una interface es un contrato: define lo que una clase debe tener, sin implementarlo. Una clase puede implementar varias interfaces.
Interfaces con implementación default
public interface ILog {
void Escribir(string msg);
// Implementación por defecto (C# 8+)
void Error(string msg) {
Escribir($"ERROR: {msg}");
}
}Desde C# 8, las interfaces pueden tener métodos con cuerpo (implementación default). Las clases los heredan sin necesitar reimplementarlos.
base
public class Animal {
public Animal(string nombre) {
Nombre = nombre;
}
public string Nombre { get; }
}
public class Perro : Animal {
public Perro(string nombre) : base(nombre) {
// llama al constructor de la base
}
}base(...) llama al constructor de la clase padre. base.Método() llama a un método de la clase padre.
Polimorfismo en la práctica
public abstract class Animal {
public abstract string Hablar();
}
public class Perro : Animal {
public override string Hablar() => "Au!";
}
public class Gato : Animal {
public override string Hablar() => "Miau!";
}
// Cada objeto responde a su manera
var animales = new List<Animal> { new Perro(), new Gato() };
foreach (var a in animales) {
Console.WriteLine(a.Hablar()); // Au! / Miau!
}El polimorfismo es tratar objetos diferentes de forma uniforme: la variable es de tipo Animal, pero cada subclase ejecuta su propio override.
Coleções e LINQ
List<T>
var lista = new List<int> { 1, 2, 3 };
lista.Add(4); // añade
lista.Remove(1); // elimina el valor 1
lista.RemoveAt(0); // elimina por índice
lista.Contains(2); // true
lista.Count; // cantidad
int primero = lista[0];La List<T> es una lista dinámica (crece sola). El <T> define el tipo de los elementos. Más flexible que un array.
LINQ: agregación
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() // 12Operaciones que reducen la colección a un solo valor: Count, Sum, Average, Max. Any/All prueban condiciones.
LINQ: GroupBy
var personas = new[] {
new { Nombre = "Ana", Ciudad = "Lisbon" },
new { Nombre = "Ray", Ciudad = "Porto" },
new { Nombre = "Eva", Ciudad = "Lisbon" },
};
var porCidade = personas
.GroupBy(p => p.Ciudad)
.Select(g => new {
Ciudad = g.Key,
Total = g.Count(),
Nombres = string.Join(", ", g.Select(x => x.Nombre))
});GroupBy agrupa elementos por una clave. Cada grupo (IGrouping) tiene la Key y los elementos — usa Count(), Select, etc. dentro de cada uno.
Dictionary
var dict = new Dictionary<string, int> {
["um"] = 1,
["dois"] = 2
};
dict["tres"] = 3; // añadir
int v = dict["um"]; // acceder
dict.TryGetValue("um", out int r); // seguro
dict.ContainsKey("um"); // trueEl Dictionary guarda pares clave/valor con acceso rápido por la clave. El TryGetValue evita errores si la clave no existe.
LINQ: ordenación
var personas = new List<Persona> { /* ... */ };
var ordenadas = personas
.OrderBy(p => p.Nombre) // ascendente
.ThenByDescending(p => p.Edad) // después edad (desc)
.ToList();
var top = personas
.OrderByDescending(p => p.Edad)
.Take(3);OrderBy ordena de forma ascendente y OrderByDescending descendente. ThenBy añade desempates. Take limita los resultados.
LINQ: Distinct, Union y Except
var a = new[] { 1, 2, 3, 3, 4 };
var b = new[] { 3, 4, 5 };
a.Distinct() // 1, 2, 3, 4 (sin repetidos)
a.Union(b) // 1, 2, 3, 4, 5 (unión sin duplicados)
a.Intersect(b) // 3, 4 (en común)
a.Except(b) // 1, 2 (en A pero no en B)Operaciones de conjuntos: Distinct elimina duplicados, Union junta, Intersect devuelve los comunes y Except resta.
Array
int[] nums = { 1, 2, 3 };
nums[0] = 10;
nums.Length; // 3
// Crear con tamaño fijo
int[] arr = new int[5];
// Recorrer
for (int i = 0; i < nums.Length; i++) {
Console.WriteLine(nums[i]);
}Un array tiene tamaño fijo (definido en la creación) y es más rápido que una lista. Usa Length para el tamaño.
HashSet, Queue y Stack
// HashSet: valores únicos
var set = new HashSet<int> { 1, 2, 2, 3 }; // {1,2,3}
// Queue: FIFO (primero en entrar, primero en salir)
var fila = new Queue<string>();
fila.Enqueue("A");
fila.Dequeue(); // "A"
// Stack: LIFO (último en entrar, primero en salir)
var pilha = new Stack<int>();
pilha.Push(1);
pilha.Pop(); // 1HashSet guarda valores únicos. Queue es una cola (FIFO) y Stack una pila (LIFO). Cada una sirve un patrón de acceso.
LINQ: First, Single y Last
var nums = new[] { 1, 2, 3, 4 };
nums.First(x => x > 2) // 3
nums.FirstOrDefault(x => x > 9) // 0 (no lanza error)
nums.Single(x => x == 4) // 4 (error si hay 2)
nums.Last() // 4
nums.ElementAt(1) // 2
// Prefiere las versiones OrDefault para evitar excepcionesFirst devuelve el primero, Last el último, Single el único (lanza error si hay varios). Las versiones OrDefault devuelven el valor por defecto en vez de lanzar una excepción.
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();LINQ hace consultas sobre colecciones. Where filtra y Select transforma cada elemento. Se encadena en cadena.
IEnumerable vs List
// IEnumerable: solo lectura, evaluación perezosa
IEnumerable<int> Pares() {
yield return 2;
yield return 4;
}
// List: en memoria, permite Add/Remove
var lista = new List<int> { 1, 2, 3 };
// LINQ devuelve IEnumerable (no ejecuta ya)
var consulta = lista.Where(x => x > 1);
var materializada = consulta.ToList(); // ejecuta ahoraIEnumerable<T> es de solo lectura y evalúa de forma perezosa (yield). List<T> está en memoria y permite cambios. ToList() materializa una consulta.
LINQ: Join
var clientes = new[] { new { Id = 1, Nombre = "Ana" } };
var pedidos = new[] { new { ClienteId = 1, Total = 50 } };
var resultado = clientes.Join(
pedidos,
c => c.Id, // clave de clientes
p => p.ClienteId, // clave de pedidos
(c, p) => new { c.Nombre, p.Total }
);
// Alternativa legible (query syntax)
var q = from c in clientes
join p in pedidos on c.Id equals p.ClienteId
select new { c.Nombre, p.Total };Join cruza dos colecciones por una clave común (como un JOIN de SQL). La query syntax con join ... on ... equals es más legible.
Exceções
try / catch / finally
try {
int r = 10 / 0;
} catch (DivideByZeroException ex) {
Console.WriteLine($"Error: {ex.Message}");
} catch (Exception ex) {
Console.WriteLine("Outro error");
} finally {
Console.WriteLine("executa sempre");
}El try prueba código; el catch maneja el error (puedes capturar tipos específicos). El finally se ejecuta siempre, haya error o no.
Excepción personalizada
public class SaldoInsuficienteException : Exception {
public SaldoInsuficienteException(string msg)
: base(msg) { }
}
throw new SaldoInsuficienteException(
"Saldo insuficiente");Crea tus propias excepciones heredando de Exception. Útil para errores específicos de tu aplicación, con nombres claros.
throw
void DefinirIdade(int edad) {
if (edad < 0) {
throw new ArgumentException(
"A edad no pode ser negativa");
}
}
// Excepciones comunes:
// ArgumentNullException
// InvalidOperationExceptionUsa throw para lanzar una excepción cuando algo está mal. Esto interrumpe la ejecución e informa a quien llamó al método.
try/finally sin catch
var conexión = AbrirConexion();
try {
// usa la conexión (puede lanzar error)
conexión.Executar();
} finally {
// se ejecuta SIEMPRE (incluso con excepción)
conexión.Cerrar();
}
// Aún mejor: using (hace esto solo)
using var l2 = AbrirConexion();
l2.Executar();finally sin catch garantiza la limpieza de recursos sin tragarse el error (la excepción sigue propagándose). El using es la alternativa preferida.
using (recursos)
// Forma clásica
using (var fs = File.OpenRead("f.txt")) {
// usa el fichero
} // cierra automáticamente
// Forma moderna (C# 8+)
using var sr = new StreamReader("f.txt");
var texto = sr.ReadToEnd();El using libera recursos automáticamente (ficheros, conexiones) al final del bloque, incluso con error. Funciona con objetos IDisposable.
Buenas prácticas con excepciones
// 1. Captura tipos específicos primero
catch (FileNotFoundException ex) { /* ... */ }
catch (Exception ex) { /* genérico al final */ }
// 2. Relanza sin perder el stack trace
try { /* ... */ }
catch (Exception) {
throw; // correcto (preserva el origen)
// throw ex; // incorrecto (reinicia el stack)
}
// 3. Nunca hagas esto:
catch { } // se traga el error en silencioCaptura excepciones específicas antes que las genéricas. Usa throw; (sin la variable) para relanzar preservando el stack trace. Nunca ignores errores en silencio.
Filtros de excepción
try {
// código de riesgo
} catch (Exception ex) when (ex is IOException) {
Console.WriteLine("Error de IO");
} catch (Exception ex) when (ex.Message.Contains("timeout")) {
Console.WriteLine("Tiempo agotado");
}La cláusula when filtra excepciones por una condición. Solo captura si la condición es verdadera — más preciso que varios catch.
Async / Await
async / await
async Task<string> LerAsync() {
string texto = await File.ReadAllTextAsync("f.txt");
return texto;
}
// Llamar
string contenido = await LerAsync();async/await permite código asíncrono que parece síncrono. El await espera la tarea sin bloquear el hilo (útil en UI y web).
Cancelación
var cts = new CancellationTokenSource();
async Task TrabalhoAsync(CancellationToken token) {
while (!token.IsCancellationRequested) {
await Task.Delay(100, token);
// hace trabajo...
}
}
cts.Cancel(); // pide cancelaciónEl CancellationToken permite cancelar tareas a medias. Pásalo a los métodos async y comprueba 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 varias tareas en paralelo. Más rápido que esperar una a una cuando son independientes.
Task.WhenAny
Task<string> t1 = BuscarServidorA();
Task<string> t2 = BuscarServidorB();
// Espera a la PRIMERA en terminar
Task<string> vencedora = await Task.WhenAny(t1, t2);
string resultado = await vencedora;
// Timeout con WhenAny
var tarea = BuscarDados();
var timeout = Task.Delay(5000);
if (await Task.WhenAny(tarea, timeout) == timeout) {
Console.WriteLine("Tiempo agotado");
}Task.WhenAny devuelve la primera tarea en terminar — útil para redundancia (pedir a 2 servidores) o para implementar timeouts.
Task.Run
async Task Procesar() {
// Trabajo pesado en otro hilo
int resultado = await Task.Run(() => {
return CalcularPesado();
});
}Task.Run ejecuta trabajo pesado en un hilo separado, liberando el hilo principal (ej.: para que la interfaz no se "congele").
await foreach (IAsyncEnumerable)
// Produce valores de forma asíncrona
async IAsyncEnumerable<int> GerarAsync() {
for (int i = 0; i < 5; i++) {
await Task.Delay(100);
yield return i;
}
}
// Consume de forma asíncrona
await foreach (var n in GerarAsync()) {
Console.WriteLine(n);
}IAsyncEnumerable<T> es una secuencia asíncrona: cada elemento puede producirse con await. Se consume con await foreach (ideal para streams y paginación).
ValueTask
async ValueTask<int> GetAsync() {
if (_cache != 0) return _cache; // síncrono
return await BuscarDaRede();
}ValueTask es una alternativa más ligera a Task cuando el resultado a menudo ya está disponible (evita asignaciones). Úsalo con moderación.
async void: cuidados
// INCORRECTO: no se puede await ni capturar errores
async void Enviar() {
await Task.Delay(100);
throw new Exception(); // ¡crashea el proceso!
}
// CORRECTO: devuelve Task
async Task EnviarAsync() {
await Task.Delay(100);
}
await EnviarAsync(); // puede esperar y capturarEvita async void: no se puede hacer await y las excepciones escapan (pueden crashear la aplicación). Usa async Task — async void solo en handlers de eventos.
Dicas e Boas Práticas
Null-coalescing (??)
string nombre = null; string s = nombre ?? "Anónimo"; // "Anónimo" // Asigna si es null List<int> lista = null; lista ??= new List<int>(); // Encadenar string r = a ?? b ?? c ?? "default";
El ?? devuelve el valor de la izquierda si no es null, si no el de la derecha. El ??= solo asigna si la variable es null.
Properties con lógica
private int _edad;
public int Edad {
get => _edad;
set => _edad = value > 0 ? value : 0;
}
// Solo lectura calculada
public string NomeCompleto => $"{Nombre} {Apellido}";Una propiedad puede tener lógica en el get/set (validación, cálculo). El value es el valor recibido en el set. => crea propiedades de solo lectura.
Pattern matching con switch
string Classificar(object obj) => obj switch {
int n when n < 0 => "negativo",
int n => "positivo",
string { Length: 0 } => "string vacia",
string s => $"texto: {s}",
null => "nulo",
_ => "outro tipo"
};
Classificar(-5); // "negativo"
Classificar(""); // "string vacia"switch expression con patrones: combina tipo (int n), propiedad ({ Length: 0 }), condición (when) y null. Mucho más potente que el switch tradicional.
Null-conditional (?.)
string nombre = null; int? len = nombre?.Length; // null (¡sin error!) lista?.Add(1); // solo añade si lista != null // Encadenar string ciudad = persona?.Direccion?.Ciudad;
El ?. accede a un miembro solo si el objeto no es null — si no devuelve null. Evita la temida NullReferenceException.
using static
using static System.Console;
using static System.Math;
WriteLine("Hola"); // en vez de Console.WriteLine
double r = Sqrt(16); // en vez de Math.Sqrtusing static importa los miembros estáticos de una clase. Permite usarlos sin repetir el nombre de la clase.
Pattern matching
object obj = 42;
// Patrón con condición
if (obj is int n and > 0) {
Console.WriteLine($"positivo: {n}");
}
// Patrón de tipo con propiedad
if (obj is string { Length: > 3 } s) {
Console.WriteLine(s);
}El pattern matching verifica tipo, valor y propiedades en una sola expresión. Se combina con and, or y not.
var vs tipo explícito
// var: tipo inferido var lista = new List<int>(); var cliente = GetClient(); // Tipo explícito: más claro int total = Calcular(); List<int> nums = new(); // target-typed new (C# 9+)
Usa var cuando el tipo es obvio (ej.: new List<int>()). Usa el tipo explícito cuando hace el código más legible. new() omite el tipo a la derecha.
Strings literales (C# 11+)
string json = """
{
"nombre": "Ana",
"edad": 30
}
""";
// Con interpolación
string s = $"""
Hola {nombre}
""";Las strings con """ (tres comillas) preservan saltos de línea y comillas sin escapar. Ideales para JSON, SQL o HTML en el código.
Rangos 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} (del inicio hasta 3)
nums[3..] // {3, 4, 5} (del 3 hasta el final)
nums[..] // copia de todos
Range r = 2..5;
nums[r] // {2, 3, 4}El operador ^ cuenta a partir del final (^1 es el último). .. crea un intervalo (Range) para extraer sub-arrays. Disponible desde C# 8.
Strings
StringBuilder
using System.Text;
var sb = new StringBuilder();
sb.Append("Hola"); // junta sin crear un nuevo objeto
sb.Append(' ');
sb.AppendLine("Mundo"); // + salto de línea
sb.Insert(0, ">>> "); // inserta en una posición
sb.Replace("Mundo", "C#"); // reemplaza
string resultado = sb.ToString();StringBuilder construye texto de forma eficiente cuando hay muchas concatenaciones (no crea un objeto nuevo en cada Append, a diferencia de +). Esencial 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'
// Verificar si una string tiene dígitos
"abc123".Any(char.IsDigit) // trueLa clase char tiene utilidades para validar caracteres: IsDigit, IsLetter, IsUpper, IsWhiteSpace. ToUpper/ToLower convierten.
Split y Join
string csv = "manzana,pera,uvas";
string[] frutas = csv.Split(','); // divide
// Ignorar entradas vacías
"a,,b".Split(',', StringSplitOptions.RemoveEmptyEntries);
// Juntar de vuelta
string unido = string.Join(" | ", frutas);
// "manzana | pera | uvas"
string linea = string.Join(",", new[] { "x", "y" });Split divide una string por un separador. string.Join junta partes con un delimitador. RemoveEmptyEntries ignora las partes vacías.
Comparación de strings
string a = "Hola";
string b = "hola";
a == b // false (las mayúsculas importan)
a.Equals(b) // false
// Ignorar mayúsculas/minúsculas
a.Equals(b, StringComparison.OrdinalIgnoreCase); // true
a.StartsWith("Ho") // true
a.EndsWith("a") // true
a.Contains("OLA", StringComparison.OrdinalIgnoreCase); // truePor defecto, la comparación distingue mayúsculas. Usa StringComparison.OrdinalIgnoreCase para ignorarlas. StartsWith/EndsWith verifican los extremos.
Substring e IndexOf
string s = "Hola Mundo C#";
s.Substring(4) // "Mundo C#"
s.Substring(4, 5) // "Mundo"
s.IndexOf("Mundo") // 4 (posición)
s.LastIndexOf("o") // última ocurrencia
// Extraer el dominio de un email
string email = "ana@mail.com";
string dominio = email.Substring(email.IndexOf('@') + 1);Substring extrae una parte (posición inicial y, opcionalmente, longitud). IndexOf devuelve la posición de la primera ocurrencia o -1 si no existe.
string.Format
double precio = 1234.5;
string.Format("Precio: {0:C}", precio); // moneda
string.Format("{0,10}", "dir"); // alineado a la derecha
string.Format("{0} e {1}", "A", "B"); // múltiples
// Equivalente con interpolación
$"Precio: {precio:C}";
// Formatos numéricos comunes
$"{1234.5:N2}" // "1,234.50"
$"{0.85:P0}" // "85%"
$"{255:X}" // "FF" (hexadecimal)string.Format sustituye {0}, {1} por los valores. Los formatos :C (moneda), :N2 (numero), :P0 (porcentaje) y :X (hex) controlan la salida.
Padding y Trim
"42".PadLeft(5, '0') // "00042"
"Hola".PadRight(8, '.') // "Hola....."
" texto ".Trim() // "texto"
" texto ".TrimStart() // "texto "
" texto ".TrimEnd() // " texto"
// Alineación en la interpolación
$"{"esq",-8}|" // "esq |" (izquierda)
$"{42,5}" // " 42" (derecha)PadLeft/PadRight rellenan con un carácter hasta la longitud. Trim elimina espacios. En la interpolación, ,N alinea (negativo = izquierda).
Concatenación
string a = "Hola";
string b = "Mundo";
string r1 = a + " " + b; // operador +
string r2 = string.Concat(a, " ", b); // método
string r3 = $"{a} {b}"; // interpolación (preferida)
// Juntar varias strings
var partes = new[] { "a", "b", "c" };
string r4 = string.Concat(partes); // "abc"
// En loops, usa StringBuilderPara casos simples usa + o interpolación ($""). string.Concat junta varias strings. Dentro de loops, prefiere StringBuilder por rendimiento.
Genéricos
Clases genéricas
public class Caja<T> {
public T Contenido { get; set; }
}
var caixaInt = new Caja<int> { Contenido = 42 };
var caixaStr = new Caja<string> { Contenido = "hola" };
// El tipo se define en el uso
int n = caixaInt.Contenido; // int (sin cast)
string s = caixaStr.Contenido; // stringUna clase genérica usa un parámetro de tipo <T> definido en el momento del uso. El mismo código funciona para cualquier tipo, con seguridad (sin object ni casts).
Interfaces genéricas
// Las colecciones de .NET se basan en 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 la interface (flexible) int Sumar(IEnumerable<int> nums) => nums.Sum();
Las colecciones de .NET se construyen sobre interfaces genéricas: IEnumerable<T>, IList<T>, IDictionary<K,V>. Programar contra la interface hace el código flexible.
Métodos genéricos
// El tipo es inferido por el argumento
T Primero<T>(T[] ítems) => ítems[0];
Primero(new[] { 1, 2, 3 }); // int
Primero(new[] { "a", "b" }); // string
// Intercambiar dos valores de cualquier tipo
void Trocar<T>(ref T a, ref T b) {
(a, b) = (b, a);
}Un método genérico declara <T> después del nombre. El compilador infiere el tipo a partir de los argumentos — no necesitas indicarlo. Funciona para cualquier tipo.
Constraints (where)
// T tiene que ser una clase
public class Repositorio<T> where T : class { }
// T tiene que implementar IComparable
T Max<T>(T a, T b) where T : IComparable<T> {
return a.CompareTo(b) >= 0 ? a : b;
}
// Varias restricciones a la vez
void Procesar<T>(T item)
where T : class, IDisposable, new() { }where restringe los tipos aceptados: class, struct, new() (constructor sin parámetros), interfaces o una clase base. Permite usar miembros específicos con seguridad.
default(T)
default(int) // 0
default(string) // null
default(bool) // false
default(DateTime) // 01/01/0001
// Esencial en genéricos (tipo desconocido)
T? Encontrar<T>(List<T> lista, Func<T, bool> filtro) {
foreach (var item in lista) {
if (filtro(item)) return item;
}
return default; // no encontrado
}default(T) devuelve el valor por defecto de un tipo: 0 para numeros, null para referencias, false para bool. Indispensable en código genérico.
Delegates e Eventos
Expresiones lambda
// Lambda: función anónima compacta
Func<int, int> doble = x => x * 2;
doble(5); // 10
// Con varios parámetros
Func<int, int, int> suma = (a, b) => a + b;
// Con cuerpo (bloque)
Func<int, string> classifica = n => {
if (n >= 10) return "aprovado";
return "reprovado";
};
// Muy usadas en LINQ
nums.Where(x => x > 5);Una lambda (x => expresión) es una función anónima y concisa. Es muy usada en LINQ y como argumento de métodos que reciben Func/Action.
Delegates personalizados
// Definir un delegate (firma de método) public delegate int Operación(int a, int b); int Sumar(int a, int b) => a + b; int Multiplicar(int a, int b) => a * b; Operación op = Sumar; // apunta a Sumar op(2, 3); // 5 op = Multiplicar; // ahora apunta a Multiplicar op(2, 3); // 6 // En código nuevo, prefiere Func<int, int, int>
Un delegate define una firma de método que puede ser referenciada. En código nuevo prefiere Func/Action — los delegates propios aún se usan en APIs públicas y eventos.
Action
// Action: delegate sin retorno (void)
Action<string> imprimir = msg => Console.WriteLine(msg);
imprimir("Hola");
// Con varios parámetros
Action<int, int> sumar = (a, b) => Console.WriteLine(a + b);
// Pasar comportamiento como argumento
void Executar(Action acción) => acción();
Executar(() => Console.WriteLine("hecho"));
// List.ForEach usa Action
new List<int> { 1, 2 }.ForEach(n => Console.WriteLine(n));Action es un delegate que representa un método sin retorno (void), con 0 a 16 parámetros. Ideal para pasar comportamiento como argumento.
Eventos (event)
public class Temporizador {
// Evento (usa el delegate EventHandler)
public event EventHandler? Tick;
public void Disparar() {
Tick?.Invoke(this, EventArgs.Empty);
}
}
// Suscribir (+=) y cancelar suscripción (-=)
var t = new Temporizador();
t.Tick += (s, e) => Console.WriteLine("tick!");
t.Disparar(); // "tick!"event expone una notificación que el código externo puede suscribir (+=) o cancelar (-=). Se dispara con Invoke. Es la base del patrón observador en .NET.
Func
// Func: delegate CON retorno (último tipo = retorno)
Func<int> obtener = () => 42;
obtener(); // 42
Func<int, int> doble = x => x * 2; // int -> int
Func<string, int, bool> mayor = (s, n) => s.Length > n;
// Uso práctico: recibir un criterio
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> es un delegate con retorno — el último tipo genérico es el tipo devuelto. Se usa para pasar transformaciones y criterios a métodos.
Predicate
// Predicate: recibe T, devuelve bool
Predicate<int> isPar = n => n % 2 == 0;
isPar(4); // true
// Métodos de lista que usan Predicate
var nums = new List<int> { 1, 2, 3, 4, 5 };
nums.Find(isPar); // 2 (primero par)
nums.FindAll(isPar); // {2, 4}
nums.Exists(isPar); // true
nums.RemoveAll(n => n < 3); // elimina 1 y 2Predicate<T> equivale a Func<T, bool>: recibe un valor y devuelve true/false. Se usa en Find, FindAll, Exists y RemoveAll.
Ficheiros e JSON
Leer fichero
using System.IO;
// Leer todo de una vez (ficheros pequeños)
string texto = File.ReadAllText("notas.txt");
// Versión asíncrona (no bloquea la UI)
string texto2 = await File.ReadAllTextAsync("notas.txt");
// Leer como bytes
byte[] datos = File.ReadAllBytes("imagen.png");File.ReadAllText lee el fichero entero a una string. Para ficheros grandes usa ReadLines o StreamReader. La versión Async no bloquea.
Path
string camino = @"C:\datos\foto.jpg";
Path.GetFileName(camino) // "foto.jpg"
Path.GetExtension(camino) // ".jpg"
Path.GetFileNameWithoutExtension(camino) // "foto"
Path.GetDirectoryName(camino) // "C:\datos"
// Unir rutas con seguridad
string nuevo = Path.Combine("C:", "datos", "f.txt");
// Carpeta temporal del sistema
Path.GetTempPath();Path maneja rutas: GetFileName, GetExtension, GetDirectoryName. Path.Combine une partes con el separador correcto.
Escribir fichero
// Sobrescribe (o crea) el fichero
File.WriteAllText("salida.txt", "Hola Mundo");
// Añadir al final (append)
File.AppendAllText("log.txt", "nueva linea\n");
// Versión asíncrona
await File.WriteAllTextAsync("salida.txt", "contenido");
// Escribir bytes
File.WriteAllBytes("datos.bin", new byte[] { 1, 2, 3 });File.WriteAllText crea o sobrescribe; File.AppendAllText añade al final. Ambos cierran el fichero automáticamente.
Verificar existencia
// Verificar antes de usar
if (File.Exists("config.json")) {
var json = File.ReadAllText("config.json");
}
if (!Directory.Exists("logs")) {
Directory.CreateDirectory("logs");
}
// Detalles sobre el fichero
var info = new FileInfo("foto.jpg");
info.Length; // tamaño en bytes
info.LastWriteTime; // última modificaciónFile.Exists y Directory.Exists verifican la existencia. FileInfo da detalles como el tamaño (Length) y la fecha (LastWriteTime).
Líneas (Read/WriteAllLines)
// Leer como array de líneas
string[] lineas = File.ReadAllLines("datos.csv");
// Leer de forma perezosa (eficiente en memoria)
foreach (var linea in File.ReadLines("grande.csv")) {
if (linea.StartsWith("#")) continue;
}
// Escribir varias líneas (cada elemento = 1 línea)
File.WriteAllLines("lista.txt", new[] { "a", "b", "c" });ReadAllLines devuelve un array de líneas. ReadLines lee línea a línea de forma perezosa — ideal para ficheros grandes. WriteAllLines escribe cada elemento como una línea.
JSON: serializar
using System.Text.Json;
var persona = new { Nombre = "Ana", Edad = 30 };
// Objeto -> JSON
string json = JsonSerializer.Serialize(persona);
// {"Nombre":"Ana","Edad":30}
// Con opciones (indentado)
var opciones = new JsonSerializerOptions { WriteIndented = true };
string bonito = JsonSerializer.Serialize(persona, opciones);JsonSerializer.Serialize (de System.Text.Json) convierte un objeto en JSON. La opción WriteIndented lo formatea con indentación.
Directory
// Crear carpeta
Directory.CreateDirectory("datos/backup");
// Listar ficheros (con filtro opcional)
string[] ficheros = Directory.GetFiles("datos");
string[] soTxt = Directory.GetFiles("datos", "*.txt");
// Listar subcarpetas
string[] carpetas = Directory.GetDirectories("datos");
// Borrar carpeta (true = recursivo)
Directory.Delete("datos/backup", true);Directory gestiona carpetas: CreateDirectory, GetFiles (acepta filtros como *.txt), GetDirectories y Delete.
JSON: deserializar
using System.Text.Json;
string json = """{"Nombre":"Ana","Edad":30}""";
// JSON -> objeto (clase o record)
var persona = JsonSerializer.Deserialize<Persona>(json);
Console.WriteLine(persona!.Nombre); // "Ana"
// Ignorar mayúsculas/minúsculas en los nombres
var opciones = new JsonSerializerOptions {
PropertyNameCaseInsensitive = true
};JsonSerializer.Deserialize<T> convierte JSON en un objeto del tipo T. PropertyNameCaseInsensitive acepta nombres de propiedades en cualquier caja.
Datas e Tempo
DateTime actual
DateTime ahora = DateTime.Now; // fecha y hora local DateTime hoy = DateTime.Today; // solo la fecha (00:00) DateTime utc = DateTime.UtcNow; // hora UTC ahora.Year // 2026 ahora.Month // 7 ahora.Day // 24 ahora.Hour // 14 ahora.Minute // 30 ahora.DayOfWeek // Thursday ahora.DayOfYear // día del año
DateTime.Now es la fecha/hora local y DateTime.UtcNow es UTC. Accede a los componentes con Year, Month, Day, Hour y DayOfWeek.
Formatear fechas
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") // "viernes"
d.ToString("MMM") // "jul"
// Formatos estándar
d.ToShortDateString() // "24/07/2026"
d.ToLongTimeString() // "15:30:00"
// En la interpolación
$"{d:dd/MM/yyyy}";Formatea con ToString("..."): dd día, MM mes, yyyy año, HH:mm hora. Los mismos formatos funcionan en la interpolación ({d:dd/MM/yyyy}).
Crear fechas
// Crear una fecha específica var natal = new DateTime(2026, 12, 25); var reuniao = new DateTime(2026, 7, 24, 15, 30, 0); // DateOnly y TimeOnly (C# 10+) var data = new DateOnly(2026, 12, 25); var hora = new TimeOnly(15, 30); // Útiles natal.DayOfWeek // Friday DateTime.DaysInMonth(2026, 2) // 28 (feb)
Crea una fecha con new DateTime(año, mes, día, hora, min, seg). DateOnly y TimeOnly (C# 10+) representan solo la fecha o solo la hora.
Parse de fechas
// Convertir string en DateTime
DateTime d = DateTime.Parse("24/07/2026");
// Más seguro (no lanza excepción)
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 convierte una string en fecha. Usa TryParse para entradas inválidas (no lanza excepción). ParseExact exige el formato exacto.
Sumar y restar tiempo
var hoy = DateTime.Now; hoy.AddDays(7) // dentro de una semana hoy.AddHours(-2) // hace 2 horas hoy.AddMonths(1) hoy.AddMinutes(90) // Diferencia entre fechas (TimeSpan) var natal = new DateTime(2026, 12, 25); TimeSpan falta = natal - hoy; falta.Days // días restantes
Suma o resta tiempo con AddDays, AddHours, AddMonths — devuelven un nuevo DateTime (no alteran el original). Restar dos fechas da un TimeSpan.
TimeSpan
// Representa una duración (no una fecha) var duración = new TimeSpan(2, 30, 0); // 2h30m var umDia = TimeSpan.FromDays(1); var meiaHora = TimeSpan.FromMinutes(30); duración.Hours // 2 duración.TotalMinutes // 150 // Operaciones var suma = umDia + meiaHora; var doble = duración * 2; // Usar con DateTime DateTime mañana = DateTime.Now + umDia;
TimeSpan representa una duración. Se crea con FromDays/FromMinutes o con el constructor. TotalMinutes da el valor total en esa unidad.
Tuplos
Tuplas básicas
// Tupla: agrupar valores sin crear una clase
var ponto = (1, 2);
ponto.Item1 // 1
ponto.Item2 // 2
var persona = ("Ana", 30, true);
persona.Item1 // "Ana"
// Con tipos explícitos
(int x, int y) pos = (5, 10);Una tupla agrupa varios valores en una única estructura sin crear una clase. Sin nombres, se accede por Item1, Item2, etc.
Devolver múltiples valores
// Método que devuelve varios 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
// O deconstruir de inmediato
var (menor, mayor) = MinMax(new[] { 3, 1, 7, 4 });Las tuplas son ideales para devolver varios valores de un método sin usar parámetros out ni crear una clase. Se deconstruye en el destino.
Campos con nombre
// Dar nombres a los campos
var persona = (Nombre: "Ana", Edad: 30);
persona.Nombre // "Ana"
persona.Edad // 30
// En retornos de métodos
(string ciudad, int cp) GetLocation() {
return ("Lisbon", 1000);
}
var local = GetLocation();
local.ciudad // "Lisbon"Da nombres a los campos (Nombre:, Edad:) para hacer la tupla más legible. Se accede por el nombre en vez de Item1.
Deconstruction
var persona = (Nombre: "Ana", Edad: 30);
// Extraer a variables separadas
var (nombre, edad) = persona;
Console.WriteLine($"{nombre}, {edad}");
// Ignorar campos con _
var (n, _) = persona;
// Deconstruir un KeyValuePair
foreach (var (clave, valor) in dictionary) {
Console.WriteLine($"{clave} = {valor}");
}La deconstrucción extrae los campos de la tupla a variables individuales: var (nombre, edad) = persona. Usa _ para ignorar campos.
Intercambiar valores (swap)
int a = 1, b = 2; // Intercambiar sin variable temporal (a, b) = (b, a); // ahora a = 2, b = 1 string x = "hola", y = "mundo"; (x, y) = (y, x);
Con tuplas, intercambiar dos valores es trivial: (a, b) = (b, a) hace el swap sin necesitar una variable temporal.