Primeros Pasos
Hola Mundo
Los programas de C# comienzan en Main(). using System; importa el espacio de nombres System (Console, Math, etc.). C# 9+ admite instrucciones de nivel superior: un archivo con solo Console.WriteLine("Hello"); es un programa válido. El compilador genera la clase y Main automáticamente.
using System;
class Program {
static void Main() {
Console.WriteLine("Hello, World!");
}
}Variables y Tipos
C# es de tipado estático. var permite al compilador inferir el tipo (sigue siendo de tipo seguro en tiempo de compilación). decimal (sufijo m) es para cálculos financieros con precisión exacta. const es una constante en tiempo de compilación; readonly es una constante en tiempo de ejecución establecida en el constructor.
int age = 30;
double pi = 3.14159;
decimal price = 19.99m;
char grade = 'A';
bool isDev = true;
string name = "Alice";
var x = 42; // implicitly typed (int)
const double TAX = 0.08;Interpolación de Cadenas
La interpolación de cadenas ($"...") incrusta expresiones entre llaves. Los especificadores de formato después de : controlan la salida (F2 para 2 decimales, yyyy-MM-dd para fechas). Las cadenas textuales (@"") tratan las barras invertidas literalmente—útiles para rutas de archivos y regex. Combine ambos: $@"...".
string name = "Alice";
int age = 30;
Console.WriteLine($"Name: {name}, Age: {age}");
// Formatting
double pi = 3.14159;
Console.WriteLine($"Pi: {pi:F2}"); // Pi: 3.14
Console.WriteLine($"Date: {DateTime.Now:yyyy-MM-dd}");
// Verbatim strings (@"") preserve backslashes
string path = @"C:\Users\name";Entrada y Salida
Console.ReadLine() lee una línea completa como cadena. int.Parse convierte pero lanza una excepción con entrada inválida; int.TryParse es más seguro—devuelve bool y usa un parámetro out. Siempre use TryParse para la entrada del usuario para evitar excepciones por datos incorrectos.
Console.Write("Enter name: ");
string name = Console.ReadLine();
Console.Write("Enter age: ");
int age = int.Parse(Console.ReadLine());
Console.WriteLine($"Hi {name}, you are {age}");
// Safe parsing with TryParse
if (int.TryParse(Console.ReadLine(), out int num)) {
Console.WriteLine($"Valid: {num}");
} else {
Console.WriteLine("Invalid number");
}Tipos Nullable
int? es un tipo de valor nullable (Nullable<int>). El operador ?. (condicional nulo) accede a miembros de forma segura—devuelve null en lugar de lanzar una excepción. El operador ?? (fusión nula) proporciona un valor predeterminado para null. Los tipos de referencia nullable de C# 8+ advierten sobre posibles nulos en tiempo de compilación.
int? maybeAge = null; // nullable int
if (maybeAge.HasValue) {
Console.WriteLine(maybeAge.Value);
}
string? maybeName = null; // nullable reference (C# 8+)
int length = maybeName?.Length ?? 0; // null-conditional + null-coalescing
// ValueOrDefault
int age = maybeAge ?? 0; // 0 if nullCadenas
Métodos de Cadena
Las cadenas de C# son inmutables—los métodos devuelven nuevas cadenas en lugar de modificar la original. Métodos comunes: Length, ToUpper/ToLower, Substring, IndexOf, Replace, Contains, Split. Para manipulación intensiva de cadenas, use StringBuilder para evitar crear muchas cadenas intermedias.
string s = "Hello, World!";
Console.WriteLine(s.Length); // 13
Console.WriteLine(s.ToUpper()); // HELLO, WORLD!
Console.WriteLine(s.Substring(0, 5)); // Hello
Console.WriteLine(s.IndexOf("World")); // 7
Console.WriteLine(s.Replace("World", "C#"));
Console.WriteLine(s.Contains("Hello")); // True
Console.WriteLine(s.Split(", ")); // ["Hello", "World!"]StringBuilder
StringBuilder es mutable y eficiente para construir cadenas en bucles. La concatenación de cadenas (+) crea una nueva cadena cada vez, por lo que la concatenación repetida es O(n^2). StringBuilder se amortiza a O(n). Use StringBuilder cuando tenga más de unas pocas concatenaciones.
using System.Text;
var sb = new StringBuilder();
for (int i = 0; i < 100; i++) {
sb.Append("Line ").Append(i).Append("\n");
}
string result = sb.ToString();
// vs string concatenation (creates many temporary strings)
// string result = "";
// for (int i = 0; i < 100; i++) result += "Line " + i;Formato y Análisis
string.Format usa marcadores {0}, {1}. Especificadores de formato: F (fijo), N (número con separadores), P (porcentaje), X (hexadecimal). El análisis es sensible a la cultura—los separadores decimales difieren según la configuración regional. Use CultureInfo para control explícito, o TryParse para mayor seguridad.
// String formatting
string s1 = string.Format("{0} is {1} years old", "Alice", 30);
string s2 = $"{3.14159:F2}"; // "3.14"
string s3 = $"{1234567:N0}"; // "1,234,567" (thousands separator)
// Parsing
int n = int.Parse("42");
double d = double.Parse("3.14");
bool b = bool.Parse("true");
// Culture-aware parsing
double euro = double.Parse("3,14",
System.Globalization.CultureInfo.GetCultureInfo("de-DE"));Comparación de Cadenas
Use StringComparison.OrdinalIgnoreCase para comparación sin distinción de mayúsculas y minúsculas—es más claro y rápido que ToLower() y luego ==. Evite == para comparaciones sensibles a la cultura; use string.Compare con un CultureInfo. Para claves de diccionario, use StringComparer.OrdinalIgnoreCase.
string a = "Hello", b = "hello";
// Case-sensitive
bool equal = a == b; // false
bool equal2 = a.Equals(b); // false
// Case-insensitive
bool equalCI = a.Equals(b, StringComparison.OrdinalIgnoreCase); // true
int cmp = string.Compare(a, b, StringComparison.OrdinalIgnoreCase);
// Best practice for comparison
bool same = string.Equals(a, b,
StringComparison.OrdinalIgnoreCase);Expresiones Regulares
Regex (de System.Text.RegularExpressions) proporciona coincidencia de patrones. Use cadenas textuales @"" para que las barras invertidas no necesiten doble escape. Para uso repetido, compile una instancia de Regex una vez y reutilícela (RegexOptions.Compiled) para mejor rendimiento.
using System.Text.RegularExpressions;
string text = "Phone: 123-456-7890";
var match = Regex.Match(text, @"\d{3}-\d{3}-\d{4}");
if (match.Success) Console.WriteLine(match.Value); // 123-456-7890
// Replace
string clean = Regex.Replace(text, @"\d", "X");
// "Phone: XXX-XXX-XXXX"
// Extract all matches
var emails = Regex.Matches("[email protected], [email protected]", @"\S+@\S+");Números y Matemáticas
Tipos Numéricos
C# tiene tipos de tamaño fijo: int (32 bits), long (64 bits), double (flotante de 64 bits), decimal (128 bits, para finanzas). Los guiones bajos (9_000_000) mejoran la legibilidad. Use decimal para dinero—evita errores de redondeo de coma flotante. Sufijos: L (long), f (float), m (decimal).
int i = 42;
long big = 9_000_000_000L; // underscores for readability
double d = 3.14159265;
float f = 3.14f;
decimal price = 19.99m; // exact decimal for money
byte b = 255;
Console.WriteLine(sizeof(int)); // 4
Console.WriteLine(int.MaxValue); // 2147483647Clase Math
Math proporciona métodos estáticos para operaciones comunes. Math.Round usa redondeo bancario (redondea al par) por defecto—use MidpointRounding.AwayFromZero para redondeo escolar. Math.BigMul maneja multiplicación long para evitar desbordamiento.
double x = 2.5;
Math.Pow(x, 3); // 15.625
Math.Sqrt(x); // 1.581
Math.Abs(-5); // 5
Math.Floor(3.7); // 3
Math.Ceiling(3.2); // 4
Math.Round(3.5); // 4 (banker's rounding)
Math.Max(3, 7); // 7
Math.Min(3, 7); // 3
Math.PI; // 3.14159...Números Aleatorios
Random genera números pseudoaleatorios. Cree una instancia y reutilícela (crear muchas en un bucle puede producir duplicados debido a la siembra basada en tiempo). Para aleatoriedad criptográfica, use System.Security.Cryptography.RandomNumberGenerator. C# 8+ proporciona Random.Shared para seguridad de subprocesos.
var random = new Random();
int r = random.Next(1, 101); // 1-100
double d = random.NextDouble(); // 0.0-1.0
byte[] bytes = new byte[4];
random.NextBytes(bytes);
// C# 8+: thread-safe
// var rng = Random.Shared;
// int n = rng.Next(1, 101);
// Array of random items
int[] nums = Enumerable.Range(0, 5)
.Select(_ => random.Next(1, 100)).ToArray();Conversión de Tipos
Las conversiones implícitas ocurren automáticamente cuando no hay pérdida de datos (int a double). Los casts explícitos (tipo) son necesarios cuando puede haber pérdida de precisión. Convert.ToInt32 maneja muchos tipos y redondea (a diferencia del cast que trunca). Siempre prefiera TryParse para conversión de cadena a número para evitar excepciones.
// Implicit (safe, no data loss)
int i = 42;
double d = i; // int -> double
// Explicit cast (may lose data)
double pi = 3.14;
int truncated = (int)pi; // 3
// Convert class
string s = "42";
int n = Convert.ToInt32(s);
string str = Convert.ToString(42);
// Parse / TryParse
int parsed = int.Parse("100");
bool ok = int.TryParse("abc", out int result); // falseDesbordamiento de Enteros y Checked
Por defecto, el desbordamiento de enteros se envuelve silenciosamente (unchecked). El bloque checked lanza OverflowException en caso de desbordamiento—úselo para código crítico de seguridad. Para números verdaderamente grandes, BigInteger (System.Numerics) maneja enteros de precisión arbitraria sin desbordamiento.
int a = int.MaxValue; // 2147483647
// a++; // overflow: wraps to -2147483648 (unchecked by default)
checked {
a++; // throws OverflowException!
}
// BigInteger for arbitrary precision
using System.Numerics;
var big = BigInteger.Pow(2, 100);
Console.WriteLine(big); // 1267650...376 (31 digits)Flujo de Control
If / Else
if/else if/else es estándar. El operador ternario (condición ? a : b) es una expresión if/else concisa. C# requiere condiciones booleanas—a diferencia de C, los enteros no se convierten implícitamente a bool. Use llaves incluso para instrucciones únicas para prevenir errores de mantenimiento.
int score = 85;
if (score >= 90) {
Console.WriteLine("A");
} else if (score >= 80) {
Console.WriteLine("B");
} else {
Console.WriteLine("C");
}
// Ternary operator
string grade = score >= 60 ? "Pass" : "Fail";Switch y Coincidencia de Patrones
Las expresiones switch de C# 8+ (=>) son concisas y devuelven valores. El patrón _ es el caso predeterminado. La coincidencia de patrones (is, switch) admite patrones de tipo, patrones de propiedad y patrones relacionales (> 18). Esto es más potente que el switch tradicional y reduce el código repetitivo.
// Classic switch
int day = 3;
string name = day switch {
1 => "Mon", 2 => "Tue", 3 => "Wed",
6 or 7 => "Weekend",
_ => "Invalid" // default
};
// Pattern matching with types
object obj = "hello";
if (obj is string s && s.Length > 3) {
Console.WriteLine(s); // "hello"
}
// Property patterns
if (person is { Age: > 18, Name: "Alice" }) {
// matches if Age > 18 and Name == "Alice"
}Bucles
for es para iteraciones contadas; foreach itera colecciones (arrays, listas, IEnumerable); while se ejecuta hasta que una condición sea falsa. foreach es de solo lectura—no puede modificar la colección durante la iteración. Use for si necesita el índice o modificar elementos.
// for loop
for (int i = 0; i < 5; i++) {
Console.WriteLine(i);
}
// foreach
string[] fruits = { "apple", "banana", "cherry" };
foreach (string fruit in fruits) {
Console.WriteLine(fruit);
}
// while
int n = 5;
while (n > 0) Console.WriteLine(n--);Break, Continue y goto
break sale del bucle o switch más cercano; continue salta a la siguiente iteración. C# no tiene break etiquetado para bucles anidados—use una bandera, extraiga a un método con return, o use LINQ. goto es válido en switch para caer en cascada pero por lo demás se desaconseja.
for (int i = 0; i < 10; i++) {
if (i == 3) continue; // skip 3
if (i == 7) break; // stop at 7
Console.WriteLine(i); // 0 1 2 4 5 6
}
// goto in switch (rare but valid)
switch (code) {
case 1:
DoSomething();
goto case 2; // fall through
case 2:
DoMore();
break;
}Iteradores y yield
yield return convierte un método en un iterador que produce valores de forma perezosa—uno a la vez según se soliciten. Esto es eficiente en memoria para secuencias grandes o infinitas. El compilador genera una máquina de estados. yield break termina la iteración temprano. LINQ usa iteradores extensivamente.
// yield return creates an iterator (lazy evaluation)
IEnumerable<int> GetNumbers() {
for (int i = 0; i < 5; i++) {
yield return i;
}
}
foreach (int n in GetNumbers()) {
Console.WriteLine(n); // 0 1 2 3 4
}
// Infinite sequence (consumed lazily)
IEnumerable<int> Naturals() {
int n = 0;
while (true) yield return n++;
}Métodos y Delegados
Definición de Métodos
Los métodos pueden tener cuerpo de expresión (=>) para funciones de una línea. Los parámetros predeterminados hacen que los argumentos sean opcionales. Los argumentos con nombre (nombre:) mejoran la legibilidad para llamadas con muchos parámetros y permiten omitir los opcionales. C# no admite la sobrecarga de métodos solo con diferentes tipos de retorno.
static int Add(int a, int b) {
return a + b;
}
// Expression-bodied (C# 6+)
static int Multiply(int a, int b) => a * b;
// Default parameters
static void Greet(string name = "Guest", int times = 1) {
for (int i = 0; i < times; i++)
Console.WriteLine($"Hi {name}!");
}
Greet(); // Hi Guest!
Greet("Alice", 3); // Hi Alice! x3
Greet(times: 2); // named argumentout, ref y params
Los parámetros out deben ser asignados por el método (el llamador no necesita inicializarlos). Los parámetros ref deben ser inicializados por el llamador y pueden ser modificados. params permite un número variable de argumentos. C# 7+ permite la declaración out var en línea: TryParse(s, out var result).
// out: must be assigned in method (caller doesn't init)
bool TryParse(string s, out int result) {
result = 0;
return int.TryParse(s, out result);
}
// ref: caller must initialize
void Increment(ref int n) { n++; }
int x = 5;
Increment(ref x); // x is now 6
// params: variable arguments
int Sum(params int[] nums) => nums.Sum();
Sum(1, 2, 3); // 6
Sum(); // 0Delegados y Eventos
Los delegados son punteros a funciones de tipo seguro. Func<T,TResult> toma entradas y devuelve un valor; Action<T> devuelve void; Predicate<T> devuelve bool. Los eventos son delegados con suscripción +=/-= y ?.Invoke para invocación segura (verificaci ón de nulo). Use eventos para el patrón observador (pub/sub).
// Delegate type
delegate int MathOp(int a, int b);
MathOp add = (a, b) => a + b;
MathOp mul = (a, b) => a * b;
Console.WriteLine(add(3, 4)); // 7
// Built-in delegate types
Func<int, int, int> divide = (a, b) => a / b;
Action<string> log = msg => Console.WriteLine(msg);
Predicate<int> isEven = n => n % 2 == 0;
// Events
event Action<string> OnMessage;
OnMessage += msg => Console.WriteLine($"Got: {msg}");
OnMessage?.Invoke("Hello");Expresiones Lambda
Las lambdas son métodos anónimos que usan =>. Capturan variables del ámbito circundante (closures). Las variables capturadas se evalúan cuando la lambda se ejecuta, no cuando se crea—tenga cuidado con esto en bucles. Use lambdas extensivamente con LINQ, eventos y callbacks.
// Lambda with explicit types
Func<int, int> square = (int x) => x * x;
// Type inference
Func<int, int, int> add = (a, b) => a + b;
// Statement lambda
Action<string> log = msg => {
Console.WriteLine($"[{DateTime.Now}] {msg}");
};
// Capturing variables (closures)
int factor = 3;
Func<int, int> multiply = x => x * factor;
Console.WriteLine(multiply(5)); // 15Métodos de Extensión
Los métodos de extensión añaden métodos a tipos existentes sin modificarlos. La palabra clave this antes del primer parámetro lo marca como método de extensión. Deben estar en una clase estática. LINQ está completamente implementado como métodos de extensión en IEnumerable. Esta es una forma potente de añadir métodos de utilidad.
static class StringExtensions {
public static int WordCount(this string s) {
return s.Split(new[] { ' ' },
StringSplitOptions.RemoveEmptyEntries).Length;
}
}
string text = "Hello World C#";
Console.WriteLine(text.WordCount()); // 3
// LINQ is built on extension methods
var evens = nums.Where(n => n % 2 == 0);Colecciones
List<T>
List<T> es un array dinámico (como vector en C++ o ArrayList en Java). Add/Remove son O(1) amortizado / O(n). Count da el número de elementos (no la capacidad). Find/FindAll usan predicados. Para inserciones/eliminaciones frecuentes en el medio, LinkedList<T> puede ser mejor.
using System.Collections.Generic;
var nums = new List<int> { 1, 2, 3 };
nums.Add(4);
nums.AddRange(new[] { 5, 6 });
nums.Remove(3);
nums.RemoveAt(0);
Console.WriteLine(nums.Count); // 4
Console.WriteLine(nums.Contains(4)); // true
// Find
int first = nums.Find(n => n > 3);
List<int> all = nums.FindAll(n => n > 3);Dictionary<TKey, TValue>
Dictionary es una tabla hash con búsqueda O(1) promedio. TryGetValue es más seguro que el indexador (que lanza KeyNotFoundException). Para claves de cadena sin distinción de mayúsculas y minúsculas, pase StringComparer.OrdinalIgnoreCase al constructor. Dictionary no preserva el orden de inserción (use OrderedDictionary si lo necesita).
var ages = new Dictionary<string, int> {
["Alice"] = 30,
["Bob"] = 25
};
ages["Charlie"] = 35;
// Safe access
if (ages.TryGetValue("Alice", out int age)) {
Console.WriteLine($"Alice: {age}");
}
// Iterate
foreach (var kv in ages) {
Console.WriteLine($"{kv.Key}: {kv.Value}");
}
// Case-insensitive keys
var dict = new Dictionary<string, int>(
StringComparer.OrdinalIgnoreCase);HashSet<T> y SortedSet<T>
HashSet<T> almacena elementos únicos con búsqueda O(1)—úselo para desduplicación y prueba de pertenencia. Admite operaciones de conjuntos (Union, Intersect, Except). SortedSet<T> (árbol rojo-negro) mantiene los elementos ordenados con operaciones O(log n). Use HashSet cuando el orden no importe, SortedSet cuando sí.
var set = new HashSet<int> { 1, 2, 3 };
set.Add(2); // duplicate, ignored
set.Add(4);
Console.WriteLine(set.Count); // 4
Console.WriteLine(set.Contains(3)); // true
// Set operations
var other = new HashSet<int> { 3, 4, 5 };
set.IntersectWith(other); // set: {3, 4}
set.UnionWith(other); // set: {3, 4, 5}
// SortedSet keeps elements sorted
var sorted = new SortedSet<int> { 3, 1, 2 }; // {1, 2, 3}Queue<T> y Stack<T>
Queue<T> es FIFO (Enqueue/Dequeue)—úselo para programación de tareas, BFS. Stack<T> es LIFO (Push/Pop)—úselo para deshacer/rehacer, DFS, evaluación de expresiones. Ambos son O(1) para sus operaciones principales. Para escenarios concurrentes, use ConcurrentQueue y ConcurrentStack de System.Collections.Concurrent.
// Queue: FIFO (first in, first out)
var queue = new Queue<string>();
queue.Enqueue("first");
queue.Enqueue("second");
string next = queue.Dequeue(); // "first"
string peek = queue.Peek(); // "second"
// Stack: LIFO (last in, first out)
var stack = new Stack<int>();
stack.Push(1);
stack.Push(2);
int top = stack.Pop(); // 2
int peek = stack.Peek(); // 1Arrays
Los arrays son de tamaño fijo. Array.Sort ordena en el lugar. Los arrays multidimensionales ([,]) son rectangulares (columnas uniformes). Los arrays escalonados ([][]) son arrays de arrays (las filas pueden tener diferentes longitudes). Use List<T> para tamaño dinámico; use arrays para datos de tamaño fijo y rendimiento crítico.
int[] nums = { 1, 2, 3, 4, 5 };
Console.WriteLine(nums.Length); // 5
Array.Sort(nums);
Array.Reverse(nums);
int found = Array.IndexOf(nums, 3);
int[] copy = new int[5];
Array.Copy(nums, copy, 5);
// Multidimensional
int[,] grid = { { 1, 2 }, { 3, 4 } };
Console.WriteLine(grid[1, 0]); // 3
// Jagged array (array of arrays)
int[][] jagged = { new[] { 1, 2 }, new[] { 3, 4, 5 } };Clases y POO
Clase y Propiedades
Las auto-propiedades ({ get; set; }) generan automáticamente campos de respaldo. Las propiedades de cuerpo de expresión (=>) se calculan al acceder. Las propiedades de solo init de C# 9+ ({ get; init; }) solo se pueden establecer durante la construcción, permitiendo inmutabilidad. Inicializadores de objeto: new Person { Name = "X" }.
class Person {
// Auto-properties (C# 3+)
public string Name { get; set; }
public int Age { get; set; }
// Read-only property
public bool IsAdult => Age >= 18;
// Constructor
public Person(string name, int age) {
Name = name;
Age = age;
}
}
var p = new Person("Alice", 30);
Console.WriteLine(p.IsAdult); // trueHerencia y Virtual
virtual marca un método para ser sobrescrito; override en subclases proporciona la nueva implementación. C# requiere override explícito (a diferencia de Java). sealed evita que una clase sea heredada o que un método sea sobrescrito. Todas las clases heredan implícitamente de object (System.Object).
class Animal {
public virtual void Speak() {
Console.WriteLine("...");
}
}
class Dog : Animal {
public override void Speak() {
Console.WriteLine("Woof");
}
}
Animal a = new Dog();
a.Speak(); // Woof (polymorphism)
// sealed prevents further inheritance
sealed class Puppy : Dog { }Interfaces
Las interfaces definen contratos sin implementación. Una clase puede implementar múltiples interfaces (a diferencia de la herencia única de clases). C# 8+ permite métodos de interfaz por defecto. Use interfaces para polimorfismo e inyección de dependencias. Convención de nomenclatura: prefijo con I (IShape, ILogger).
interface IShape {
double Area(); // interface member
double Perimeter { get; }
}
class Circle : IShape {
public double Radius { get; set; }
public double Area() => Math.PI * Radius * Radius;
public double Perimeter => 2 * Math.PI * Radius;
}
// Default interface methods (C# 8+)
interface ILogger {
void Log(string msg);
void Error(string msg) => Log($"ERROR: {msg}"); // default
}Clases Abstractas
Las clases abstractas no pueden ser instanciadas y pueden tener tanto miembros abstractos (deben sobrescribirse) como concretos (virtuales). Use clases abstractas cuando hay implementación compartida; use interfaces para contratos puros. Una clase puede heredar solo una clase abstracta pero implementar muchas interfaces.
abstract class Shape {
public abstract double Area(); // must be overridden
public virtual void Describe() {
Console.WriteLine($"Area: {Area()}");
}
}
class Rectangle : Shape {
public double Width { get; set; }
public double Height { get; set; }
public override double Area() => Width * Height;
}
// Cannot instantiate abstract class
// var s = new Shape(); // ERROR
var r = new Rectangle { Width = 3, Height = 4 };Records y Structs
Los Records (C# 9+) proporcionan igualdad basada en valor, inmutabilidad y sintaxis concisa—ideales para DTOs y modelos de datos. La expresión with crea una copia con propiedades modificadas. Los Structs son tipos de valor (copiados en la asignación, asignados en la pila)—úselos para datos pequeños y ligeros. Las clases son tipos de referencia (asignadas en el montículo).
// Record (C# 9+): value-based equality, immutability
record Point(int X, int Y);
var p1 = new Point(3, 4);
var p2 = new Point(3, 4);
Console.WriteLine(p1 == p2); // true (value equality)
// with expression (non-destructive mutation)
var p3 = p1 with { X = 10 };
// Struct: value type (stack-allocated)
struct Vector {
public double X, Y;
public Vector(double x, double y) { X = x; Y = y; }
}LINQ
Where y Select
LINQ (Language Integrated Query) proporciona consultas tipo SQL sobre colecciones. Where filtra, Select transforma (map). LINQ es perezoso—las consultas se ejecutan solo al enumerar (por ejemplo, vía ToList()). Esto permite encadenamiento eficiente sin colecciones intermedias. La sintaxis de métodos (arriba) es la más común; la sintaxis de consulta también está disponible.
using System.Linq;
int[] nums = { 1, 2, 3, 4, 5, 6 };
// Filter
var evens = nums.Where(n => n % 2 == 0); // 2, 4, 6
// Map/Transform
var squares = nums.Select(n => n * n); // 1, 4, 9, 16, 25, 36
// Chain operations
var result = nums
.Where(n => n > 2)
.Select(n => n * 10)
.ToList(); // 30, 40, 50, 60Ordenamiento y Agrupación
OrderBy/OrderByDescending ordenan; ThenBy añade criterios de ordenamiento secundarios. GroupBy agrupa elementos por una clave, devolviendo grupos IGrouping<key, element>. Cada grupo tiene una Key y es en sí mismo un IEnumerable de sus miembros. Esto reemplaza bucles y diccionarios manuales para agrupar.
var people = new[] {
new { Name = "Alice", Age = 30 },
new { Name = "Bob", Age = 25 },
new { Name = "Charlie", Age = 30 },
};
// Order
var sorted = people.OrderBy(p => p.Age).ThenBy(p => p.Name);
var desc = people.OrderByDescending(p => p.Age);
// Group
var grouped = people.GroupBy(p => p.Age);
foreach (var g in grouped) {
Console.WriteLine($"Age {g.Key}: {g.Count()} people");
}
// Age 25: 1 people
// Age 30: 2 peopleAgregación
Los métodos de agregación de LINQ (Sum, Average, Min, Max, Count) calculan valores únicos a partir de colecciones. Aggregate es el más general—es un fold/reduce que aplica una función acumulativamente. Estos lanzan excepción en secuencias vacías; use las variantes *OrDefault o verifique primero si está vacío.
int[] nums = { 1, 2, 3, 4, 5 };
Console.WriteLine(nums.Sum()); // 15
Console.WriteLine(nums.Average()); // 3
Console.WriteLine(nums.Min()); // 1
Console.WriteLine(nums.Max()); // 5
Console.WriteLine(nums.Count()); // 5
// Count with predicate
int evens = nums.Count(n => n % 2 == 0); // 2
// Aggregate (fold/reduce)
int product = nums.Aggregate((a, b) => a * b); // 120
string joined = nums.Aggregate("", (s, n) => s + n); // "12345"First, Single y ElementAt
First devuelve el primer elemento (lanza excepción si está vacío); FirstOrDefault devuelve el valor predeterminado (0 para int, null para tipos de referencia) si no se encuentra. Single requiere exactamente un elemento (lanza excepción en caso contrario)—úselo para validación. Any/All devuelven bool sin enumerar toda la colección (cortocircuito).
int[] nums = { 1, 2, 3, 4, 5 };
int first = nums.First(); // 1
int firstEven = nums.First(n => n % 2 == 0); // 2
int firstOrDef = nums.FirstOrDefault(n => n > 10); // 0 (default)
int single = new[] { 42 }.Single(); // 42
// nums.Single(); // throws! more than one element
int third = nums.ElementAt(2); // 3 (0-indexed)
bool any = nums.Any(n => n > 3); // true
bool all = nums.All(n => n > 0); // trueJoin y Zip
Join realiza una unión interna (como SQL) emparejando elementos por clave. Zip empareja elementos de dos secuencias por posición. Ambos producen nuevas secuencias. LINQ también admite GroupJoin (unión izquierda con agrupación). Estos son potentes para combinar datos relacionados de múltiples fuentes.
var users = new[] {
new { Id = 1, Name = "Alice" },
new { Id = 2, Name = "Bob" },
};
var orders = new[] {
new { UserId = 1, Item = "Book" },
new { Id = 2, Item = "Pen" },
};
// Join (inner join)
var joined = users.Join(orders,
u => u.Id, o => o.UserId,
(u, o) => new { u.Name, o.Item });
// { Alice, Book }
// Zip (pair elements by position)
var names = new[] { "Alice", "Bob" };
var ages = new[] { 30, 25 };
var pairs = names.Zip(ages, (n, a) => $"{n}: {a}");
// "Alice: 30", "Bob: 25"Manejo de Errores
Try / Catch / Finally
Capture primero excepciones específicas, luego la Exception general al final (de más específica a menos específica). finally siempre se ejecuta—úselo para limpieza (cerrar archivos, liberar recursos). Evite catch (Exception) para control de flujo; capture solo lo que pueda manejar. Use throw; (no throw e;) para preservar el stack trace.
try {
int x = 10;
int y = 0;
int z = x / y; // throws DivideByZeroException
} catch (DivideByZeroException e) {
Console.WriteLine($"Error: {e.Message}");
} catch (Exception e) {
Console.WriteLine($"Unexpected: {e.Message}");
} finally {
// always runs, even with return/throw
Console.WriteLine("Cleanup");
}Excepciones Personalizadas
Derive excepciones personalizadas de Exception (o una base más específica). Añada campos de contexto que ayuden a la depuración. Siempre llame al constructor base con el mensaje. Por convención, los nombres de clases de excepción terminan con Exception. Use excepciones personalizadas para condiciones de error específicas del dominio que los llamadores puedan manejar.
class ValidationError : Exception {
public string Field { get; }
public ValidationError(string field, string msg)
: base(msg) {
Field = field;
}
}
throw new ValidationError("email", "Invalid format");
try {
// ...
} catch (ValidationError e) {
Console.WriteLine($"{e.Field}: {e.Message}");
}using y IDisposable
using asegura que Dispose() se llame incluso si ocurre una excepción—este es el equivalente en C# de RAII para gestión de recursos. La declaración using (C# 8+) es más limpia—Dispose se llama al final del ámbito. Implemente IDisposable para clases que contienen recursos no administrados (manejadores de archivos, conexiones de base de datos).
// using statement (C# 1+)
using (var file = new StreamReader("data.txt")) {
string content = file.ReadToEnd();
} // file.Dispose() called automatically
// using declaration (C# 8+)
using var file2 = new StreamReader("data.txt");
string content2 = file2.ReadToEnd();
// Dispose called at end of scope
// Implementing IDisposable
class Resource : IDisposable {
public void Dispose() {
// release unmanaged resources
GC.SuppressFinalize(this);
}
}Manejo de Nulos
Los tipos de referencia nullable de C# 8+ (string?) habilitan seguridad de nulos en tiempo de compilación. ?. devuelve null en lugar de lanzar NullReferenceException. ?? proporciona un valor alternativo. La expresión throw (?? throw) es concisa para validación. Habilite <Nullable>enable</Nullable> en .csproj para verificación de nulos en todo el proyecto.
string? name = null;
// Null-conditional operator
int? length = name?.Length; // null (no exception)
// Null-coalescing operator
string display = name ?? "Unknown"; // "Unknown"
// Throw if null
string required = name ?? throw new ArgumentNullException(nameof(name));
// Pattern matching null check
if (name is null) { /* ... */ }
if (name is { Length: > 5 }) { /* not null and length > 5 */ }Filtros de Excepciones
Los filtros de excepciones (when) añaden condiciones a los bloques catch—la excepción solo se captura si el filtro es verdadero. Esto es más potente que capturar y relanzar porque preserva el stack trace. El patrón de registro (when devuelve false) le permite observar excepciones sin manejarlas.
try {
// ...
} catch (HttpRequestException e) when (e.StatusCode == 404) {
Console.WriteLine("Not found");
} catch (HttpRequestException e) when (e.StatusCode >= 500) {
Console.WriteLine("Server error");
} catch (HttpRequestException e) {
Console.WriteLine($"HTTP error: {e.Message}");
}
// Log without catching
catch (Exception e) when (LogError(e)) {
// never enters if LogError returns false
}
private bool LogError(Exception e) { /* log */ return false; }E/S de Archivos y Async
Clase File (E/S Rápida)
File proporciona métodos estáticos para operaciones rápidas de archivos de una sola vez—simple pero carga todo el archivo en memoria. Para archivos grandes, use StreamReader/StreamWriter para procesar línea por línea. File.Exists verifica la existencia pero hay una condición de carrera TOCTOU—siempre maneje las excepciones de las operaciones de archivos.
using System.IO;
// Read all text at once
string content = File.ReadAllText("input.txt");
string[] lines = File.ReadAllLines("input.txt");
// Write
File.WriteAllText("output.txt", "Hello\n");
File.WriteAllLines("output.txt", new[] { "Line 1", "Line 2" });
// Append
File.AppendAllText("log.txt", $"[{DateTime.Now}] Event\n");
// File info
bool exists = File.Exists("data.txt");
var info = new FileInfo("data.txt");
Console.WriteLine(info.Length); // bytesStreams (StreamReader/Writer)
StreamReader/StreamWriter procesan archivos línea por línea sin cargar todo en memoria—úselos para archivos grandes. La instrucción using asegura que el archivo se cierre incluso si ocurre una excepción. StreamWriter almacena datos en búfer; llame a Flush() para escribir inmediatamente, o deje que Dispose lo maneje.
using System.IO;
// Reading line by line
using var reader = new StreamReader("input.txt");
string? line;
while ((line = reader.ReadLine()) != null) {
Console.WriteLine(line);
}
// Writing
using var writer = new StreamWriter("output.txt");
writer.WriteLine("First line");
writer.WriteLine("Second line");
writer.Flush(); // optional, using disposes and flushesasync / await
async/await habilita operaciones asíncronas no bloqueantes. El método devuelve Task<T> (o Task para void). await suspende el método sin bloquear el subproceso—el subproceso se libera para otro trabajo. Esto es esencial para operaciones limitadas por E/S (archivos, red, base de datos) en aplicaciones web y de UI.
async Task<string> ReadFileAsync(string path) {
using var reader = new StreamReader(path);
return await reader.ReadToEndAsync();
}
// Calling async method
string content = await ReadFileAsync("data.txt");
Console.WriteLine(content.Length);
// Async doesn't block the calling thread
// The thread is freed while waiting for I/OTask y Parallel
Task.Run programa trabajo en el pool de subprocesos. Task.WhenAll espera múltiples tareas concurrentemente (no use async/await para trabajo limitado por CPU—use Task.Run). Parallel.For es para bucles paralelos limitados por CPU. PLINQ (AsParallel) paraleliza consultas LINQ. Use async para E/S, Parallel/PLINQ para CPU.
// Run work on a thread pool thread
var task = Task.Run(() => HeavyComputation());
int result = await task;
// Run multiple tasks concurrently
var t1 = Task.Run(() => DownloadAsync("url1"));
var t2 = Task.Run(() => DownloadAsync("url2"));
await Task.WhenAll(t1, t2); // wait for both
// Parallel.For for CPU-bound parallelism
Parallel.For(0, 100, i => {
Process(i); // runs on multiple threads
});
// PLINQ
var results = nums.AsParallel()
.Where(n => IsPrime(n))
.ToArray();Serialización JSON
System.Text.Json es el serializador JSON moderno y de alto rendimiento (reemplaza Newtonsoft.Json para la mayoría de los casos). Serialize/Deserialize manejan objeto<->JSON. Use JsonSerializerOptions para formato (indentado, camelCase). Para async, use los métodos basados en streams para evitar cargar JSON grande en memoria.
using System.Text.Json;
var person = new { Name = "Alice", Age = 30 };
// Serialize
string json = JsonSerializer.Serialize(person);
// {"Name":"Alice","Age":30}
// Deserialize
Person p = JsonSerializer.Deserialize<Person>(json);
// With options
var options = new JsonSerializerOptions {
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
string pretty = JsonSerializer.Serialize(person, options);
// Async (for streams)
using var stream = File.OpenRead("data.json");
var data = await JsonSerializer.DeserializeAsync<Person>(stream);LINQ en Profundidad
Sintaxis de Consulta vs Método
LINQ tiene dos sintaxis equivalentes. La sintaxis de consulta es tipo SQL y más legible para uniones/agrupación compleja. La sintaxis de métodos (fluent) es más común, admite todos los operadores y se encadena naturalmente. Ambas compilan al mismo IL. Use sintaxis de consulta para consultas complejas de múltiples cláusulas; sintaxis de métodos para cadenas simples. Ambas son de evaluación perezosa (ejecución diferida).
List<int> nums = new() { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
// Query syntax (SQL-like)
var evens = from n in nums
where n % 2 == 0
orderby n descending
select n * 2;
// Method syntax (fluent) — equivalent
var evens2 = nums.Where(n => n % 2 == 0)
.OrderByDescending(n => n)
.Select(n => n * 2);
// Query syntax supports: from, where, orderby, select, group, join, let
// Method syntax supports everything query does, plus moreEjecución Diferida vs Inmediata
LINQ usa ejecución diferida—Where/Select/OrderBy solo construyen una consulta; se ejecuta al enumerar. Esto significa que los resultados reflejan el estado de la fuente en el momento de la enumeración, no en la creación de la consulta. ToList/ToArray/Count/etc. fuerzan la ejecución inmediata, capturando una instantánea. Tenga cuidado: enumerar una consulta diferida dos veces la ejecuta dos veces. Almacene en caché con ToList si necesita resultados estables.
var nums = new List<int> { 1, 2, 3 };
// Deferred: query not run until enumerated
var query = nums.Where(n => n > 1);
nums.Add(4); // query sees this!
Console.WriteLine(query.Count()); // 3 (2, 3, 4)
// Immediate: forces execution now
var list = nums.Where(n => n > 1).ToList(); // runs now
nums.Add(5);
Console.WriteLine(list.Count); // still 3 (snapshot)
// Operators that force immediate execution:
// ToList, ToArray, ToDictionary, ToLookup,
// Count, Sum, Min, Max, Average, First, Last, Any, AllAgrupación y Unión
GroupBy agrupa elementos por una clave, devolviendo secuencias IGrouping<key, element>. Join realiza uniones internas (emparejando claves). La unión de grupo (join...into) crea resultados jerárquicos y permite uniones externas izquierdas vía DefaultIfEmpty(). Estas operaciones son potentes para análisis de datos. Para conjuntos de datos grandes, considere usar ToLookup para búsquedas repetidas (es una agrupación precomputada).
var students = new[] {
new { Name = "Alice", Grade = "A", Dept = "CS" },
new { Name = "Bob", Grade = "B", Dept = "Math" },
new { Name = "Carol", Grade = "A", Dept = "CS" },
};
// Group by department
var byDept = students.GroupBy(s => s.Dept);
foreach (var g in byDept) {
Console.WriteLine(`{g.Key}: {g.Count()}`);
// CS: 2, Math: 1
}
// Inner join
var courses = new[] { new { Dept = "CS", Course = "Algo" } };
var joined = from s in students
join c in courses on s.Dept equals c.Dept
select new { s.Name, c.Course };
// Group join (left outer join with DefaultIfEmpty)
var groupJoin = from s in students
join c in courses on s.Dept equals c.Dept into sc
from c in sc.DefaultIfEmpty()
select new { s.Name, Course = c?.Course };Agregación y Cuantificadores
LINQ proporciona agregaciones estándar (Sum, Min, Max, Average, Count) y un Aggregate genérico para reducciones personalizadas (como reduce de JavaScript). Los cuantificadores (Any, All, Contains) devuelven booleanos y cortocircuitan—Any se detiene en la primera coincidencia, All se detiene en el primer no coincidente. Use Any() (no Count() > 0) para verificar existencia—es más eficiente y legible.
var nums = new[] { 1, 2, 3, 4, 5 };
// Aggregations
int sum = nums.Sum(); // 15
double avg = nums.Average(); // 3.0
int min = nums.Min(); // 1
int max = nums.Max(); // 5
int count = nums.Count(); // 5
// Custom aggregation (like reduce/fold)
int product = nums.Aggregate((a, b) => a * b); // 120
int withSeed = nums.Aggregate(10, (a, b) => a + b); // 25
// Quantifiers (return bool)
bool anyEven = nums.Any(n => n % 2 == 0); // true
bool allPos = nums.All(n => n > 0); // true
bool contains3 = nums.Contains(3); // true
// LongCount for > int.MaxValue elements
long big = largeList.LongCount();IEnumerable e IQueryable
IEnumerable<T> es para colecciones en memoria (LINQ to Objects)—usa delegados, se ejecuta localmente. IQueryable<T> es para fuentes remotas (Entity Framework, LINQ to SQL)—usa árboles de expresión, se traduce al lenguaje de consulta de la fuente (SQL). IQueryable compone consultas del lado del servidor eficientemente. Cambie a IEnumerable con AsEnumerable() cuando necesite lógica del lado del cliente que no se pueda traducir.
// IEnumerable<T>: in-memory, LINQ to Objects
List<int> nums = new() { 1, 2, 3 };
var q1 = nums.Where(n => n > 1); // executes in memory
// IQueryable<T>: translates to query language (SQL)
// Expression trees, not delegates
using var db = new MyDbContext();
var q2 = db.Users.Where(u => u.Age > 18); // generates SQL
// SELECT * FROM Users WHERE Age > 18
// IQueryable composes queries that execute server-side
var query = db.Users
.Where(u => u.Active)
.OrderBy(u => u.Name)
.Take(10);
// All translated to one SQL query
// Force client-side with AsEnumerable()
var clientSide = db.Users.AsEnumerable()
.Where(u => ExpensiveCheck(u));Async/Await en Profundidad
Task y Task<T>
Task representa una operación asíncrona sin resultado; Task<T> devuelve T. ValueTask<T> (C# 7) evita la asignación en el montículo cuando el resultado suele estar disponible sincrónicamente (escenarios de caché)—pero solo se puede esperar una vez. Use Task.FromResult para resultados sincrónicos en APIs asíncronas. Prefiera Task sobre void para métodos asíncronos (void es solo para manejadores de eventos).
// Task: represents an async operation (no result)
async Task DoWorkAsync() {
await Task.Delay(1000);
Console.WriteLine("Done");
}
// Task<T>: async operation returning T
async Task<int> GetCountAsync() {
await Task.Delay(500);
return 42;
}
// ValueTask<T>: lightweight, avoids allocation
ValueTask<int> GetCachedAsync() {
if (_cache != null) return new ValueTask<int>(_cache);
return new ValueTask<int>(LoadFromDbAsync());
}
// Converting to Task
Task t = Task.Run(() => Console.WriteLine("hi"));
Task<int> t2 = Task.FromResult(42); // already complete
Task completed = Task.CompletedTask;CancellationToken
CancellationToken habilita la cancelación cooperativa. Páselo a métodos asíncronos y verifique ThrowIfCancellationRequested() en bucles. El llamador cancela vía CancellationTokenSource.Cancel(); el método decide cuándo/cómo responder. Siempre acepte un parámetro CancellationToken en APIs asíncronas (especialmente código de biblioteca). Use CancellationTokenSource con un TimeSpan para tiempos de espera. La cancelación lanza OperationCanceledException.
async Task DownloadAsync(string url, CancellationToken ct) {
using var client = new HttpClient();
// Pass token to async operations
var data = await client.GetByteArrayAsync(url, ct);
// Check manually in loops
foreach (var chunk in data) {
ct.ThrowIfCancellationRequested();
Process(chunk);
}
}
// Usage with timeout
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
try {
await DownloadAsync("https://example.com", cts.Token);
} catch (OperationCanceledException) {
Console.WriteLine("Timed out or cancelled");
}
// Cooperative cancellation: caller cancels, method checks
cts.Cancel(); // request cancellationTask.WhenAll vs WhenAny
Task.WhenAll espera múltiples tareas concurrentemente y continúa cuando todas completan (paralelismo). Task.WhenAny continúa cuando la primera tarea completa (carrera/redundancia). WhenAll lanza solo la primera excepción por defecto; inspeccione IsFaulted/Exception de cada tarea para ver todos los fallos. Para fire-and-forget, use Task.Run y maneje las excepciones internamente para evitar excepciones no observadas.
// WhenAll: wait for ALL to complete (like Promise.all)
var tasks = new[] {
FetchAsync("url1"),
FetchAsync("url2"),
FetchAsync("url3"),
};
string[] results = await Task.WhenAll(tasks);
// Continues when all 3 done; throws AggregateException if any fails
// WhenAny: wait for FIRST to complete (race)
var racing = new[] { PrimaryAsync(), FallbackAsync() };
Task<string> firstDone = await Task.WhenAny(racing);
string result = await firstDone;
// WhenAll with exception handling
try {
await Task.WhenAll(tasks);
} catch (Exception ex) {
// Only first exception is thrown; access all via Task.Exception
foreach (var inner in tasks.Where(t => t.IsFaulted))
Console.WriteLine(inner.Exception);
}Streams Asíncronos (IAsyncEnumerable)
IAsyncEnumerable<T> (C# 8) es un stream asíncrono—produce elementos a medida que están disponibles, con awaits entre producciones. Se consume con await foreach. Perfecto para streaming de grandes conjuntos de datos o datos en tiempo real sin almacenar todo en búfer en memoria. El atributo [EnumeratorCancellation] asegura que el token de cancelación fluya correctamente cuando el stream se consume con WithCancellation.
// C# 8: async iterator (like async generator)
async IAsyncEnumerable<int> GenerateAsync(
[EnumeratorCancellation] CancellationToken ct = default)
{
for (int i = 0; i < 10; i++) {
await Task.Delay(100, ct);
yield return i; // async yield
}
}
// Consume with await foreach
await foreach (int n in GenerateAsync().WithCancellation(ct)) {
Console.WriteLine(n);
}
// Use case: streaming data from a database or API
async IAsyncEnumerable<Record> StreamRecordsAsync() {
await using var reader = await cmd.ExecuteReaderAsync();
while (await reader.ReadAsync())
yield return MapRecord(reader);
}Problemas de Async (Interbloqueos)
El problema #1 de async: llamar .Result o .Wait() en un Task puede causar interbloqueo cuando hay un SynchronizationContext presente (aplicaciones de UI, ASP.NET legacy). await captura el contexto; .Result bloquea el subproceso que la continuación necesita. Solución: haga los métodos async hasta arriba, o use ConfigureAwait(false) en código de biblioteca. async void es peligroso—las excepciones no se pueden capturar y no se puede esperar. Use async Task en su lugar.
// BAD: .Result blocks (can deadlock in UI/ASP.NET classic)
public string BadMethod() {
string data = GetDataAsync().Result; // DEADLOCK risk!
return data;
}
// GOOD: async all the way
public async Task<string> GoodMethodAsync() {
string data = await GetDataAsync();
return data;
}
// Why deadlock: in UI/ASP.NET context, await captures
// SynchronizationContext. .Result blocks the thread,
// but the continuation needs that same thread → deadlock.
// Fix if you MUST block (last resort):
string data = Task.Run(GetDataAsync).Result; // offloads
// Or ConfigureAwait(false) in library code:
await GetDataAsync().ConfigureAwait(false);
// async void is EVIL (except event handlers)
async void Bad() { await Task.Delay(1); throw new Exception(); }
// Exception can't be caught by caller!Eventos y Delegados
Fundamentos de Delegados
Los delegados son punteros a funciones de tipo seguro. Los delegados personalizados (delegate int MathOp(int,int)) han sido reemplazados en gran medida por los integrados Action (devuelve void) y Func<T> (devuelve T). Los delegados son multicast—pueden contener múltiples métodos (combinados con +). Al invocarse, todos los métodos se ejecutan. El valor de retorno de un delegado multicast es el resultado del último método. Use Action/Func para la mayoría del código moderno.
// Declare a delegate type
public delegate int MathOp(int a, int b);
// Methods matching the signature
static int Add(int a, int b) => a + b;
static int Mul(int a, int b) => a * b;
MathOp op = Add;
Console.WriteLine(op(2, 3)); // 5
op = Mul;
Console.WriteLine(op(2, 3)); // 6
// Multicast: combine delegates
MathOp multi = (MathOp)Add + Mul;
// Note: returns last result (Add's), but both run
// (return value of multicast is the last one's)
// Built-in: Action (void), Func<T> (returns T)
Action<string> log = s => Console.WriteLine(s);
Func<int, int, int> add = (a, b) => a + b;
Predicate<int> isEven = n => n % 2 == 0; // returns boolEventos (Publicador/Suscriptor)
Los eventos son una forma restringida de delegados—solo la clase declarante puede invocarlos, pero el código externo puede suscribirse/desuscribirse (+=/-=). Use EventHandler<TArgs> para el patrón estándar. Siempre use el operador condicional nulo (Click?.Invoke) ya que un evento sin suscriptores es null. Siempre desuscríbase para prevenir fugas de memoria (el evento mantiene una referencia fuerte al manejador).
public class Button {
// Event: restricted multicast delegate
public event EventHandler<EventArgs> Click;
public void SimulateClick() {
// Null-conditional invoke pattern (thread-safe)
Click?.Invoke(this, EventArgs.Empty);
}
}
// Custom event args
public class SearchEventArgs : EventArgs {
public string Query { get; init; }
}
// Subscribe
var btn = new Button();
btn.Click += (sender, e) => Console.WriteLine("Clicked!");
btn.Click += HandlerMethod; // method group
// Unsubscribe (MUST to avoid memory leaks)
btn.Click -= HandlerMethod;
void HandlerMethod(object sender, EventArgs e) { /* ... */ }INotifyPropertyChanged (Enlace de Datos)
INotifyPropertyChanged es el patrón estándar para enlace de datos en WPF, MAUI y WinForms. Cuando una propiedad cambia, lance PropertyChanged para que la UI enlazada se actualice. [CallerMemberName] (C# 5) pasa automáticamente el nombre de la propiedad que llama—sin cadenas mágicas. La verificación (if != value) previene notificaciones innecesarias. Los ViewModels usan esto extensivamente; los generadores de código fuente (CommunityToolkit.Mvvm) pueden generar automáticamente este código repetitivo.
using System.ComponentModel;
using System.Runtime.CompilerServices;
public class ViewModel : INotifyPropertyChanged {
private string _name;
public string Name {
get => _name;
set {
if (_name != value) {
_name = value;
OnPropertyChanged(); // raises event
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
// CallerMemberName auto-fills the property name
protected void OnPropertyChanged([CallerMemberName] string prop = null) {
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(prop));
}
}
// WPF/MAUI bindings automatically update UI when Name changesEvento vs Delegado
La diferencia clave: un campo delegado público puede ser invocado, reasignado o borrado por cualquiera. Un evento restringe el acceso externo a solo += (suscribirse) y -= (desuscribirse)—solo la clase declarante puede invocarlo o borrarlo. Esta encapsulación es por qué los eventos son el estándar para el patrón observador. Los eventos también generan accesores add/remove seguros para subprocesos y se integran con herramientas de diseño.
// Public delegate: anyone can invoke or clear
public delegate void Notify();
public class Bad {
public Notify OnDone; // field-like, public
}
// External code can do:
// bad.OnDone(); // invoke (BAD: not the owner)
// bad.OnDone = null; // clear all subscribers (BAD)
// Event: only owner can invoke/clear
public class Good {
public event Notify OnDone; // restricted
public void Complete() {
OnDone?.Invoke(); // OK: owner invokes
}
}
// External code can ONLY += or -= (subscribe/unsubscribe)
// Cannot invoke or clear from outside
// Event = delegate + access restriction
// Under the hood: event generates add/remove accessorsEventos Débiles (Prevención de Fugas de Memoria)
Los eventos mantienen referencias fuertes a los suscriptores, causando fugas de memoria si el suscriptor debería ser recolectado por el recolector de basura pero el publicador sigue vivo. Soluciones: desuscribirse explícitamente (mejor), usar WeakEventManager (WPF), o implementar un patrón de evento débil con WeakReference. Esta es una fuente común de fugas en aplicaciones de larga duración (servidores, aplicaciones de escritorio). Siempre desuscríbase en Dispose o cuando termine el ciclo de vida del suscriptor.
// Problem: event holds strong ref to subscriber → leak
// if subscriber should be GC'd but publisher lives long
// Solution 1: WeakEventManager (WPF)
WeakEventManager<Publisher, EventArgs>.AddHandler(pub, "Event", Handler);
// Solution 2: WeakReference in custom pattern
public class WeakEvent<TArgs> where TArgs : EventArgs {
private List<WeakReference<EventHandler<TArgs>>> _handlers = new();
public void Subscribe(EventHandler<TArgs> h) =>
_handlers.Add(new WeakReference<EventHandler<TArgs>>(h));
public void Raise(object sender, TArgs args) {
_handlers.RemoveAll(wr => !wr.TryGetTarget(out _));
foreach (var wr in _handlers) {
if (wr.TryGetTarget(out var h)) h(sender, args);
}
}
}
// Best practice: unsubscribe (-=) when done, or use
// IDisposable pattern with weak references for long-lived publishersReflexión
Información de Tipo
La reflexión inspecciona los metadatos de tipo en tiempo de ejecución. typeof(T) obtiene un Type en tiempo de compilación; obj.GetType() lo obtiene en tiempo de ejecución. Type proporciona Name, IsClass, IsValueType y métodos para enumerar miembros (GetMethods, GetProperties, GetFields). BindingFlags controlan qué se devuelve (NonPublic, Instance, Static, Public). La reflexión es potente pero lenta—almacene en caché los resultados cuando sea posible.
using System;
Type t = typeof(string);
// Or: Type t = "hello".GetType();
Console.WriteLine(t.Name); // String
Console.WriteLine(t.FullName); // System.String
Console.WriteLine(t.IsClass); // true
Console.WriteLine(t.IsValueType); // false
Console.WriteLine(t.IsAbstract); // false
// Get methods
foreach (var m in t.GetMethods())
Console.WriteLine(m.Name);
// Get properties
var props = t.GetProperties();
// Get fields
var fields = t.GetFields(BindingFlags.NonPublic | BindingFlags.Instance);
// Check if assignable
Type ienum = typeof(IEnumerable<int>);
Console.WriteLine(ienum.IsAssignableFrom(t)); // falseInstanciación e Invocación
Activator.CreateInstance crea objetos dinámicamente. MethodInfo.Invoke llama métodos vía reflexión (lento debido al boxing de argumentos y verificaciones de seguridad). Para llamadas repetidas, CreateDelegate es mucho más rápido—crea un delegado fuertemente tipado que omite la sobrecarga de reflexión. La reflexión rompe la seguridad de tipos en tiempo de compilación y es más lenta que las llamadas directas; úsela para frameworks (serialización, DI, ORMs) y escenarios de caché.
using System.Reflection;
// Create instance
Type t = typeof(StringBuilder);
object obj = Activator.CreateInstance(t);
// Or with args: Activator.CreateInstance(t, "initial")
// Invoke method
MethodInfo method = t.GetMethod("Append", new[] { typeof(string) });
object result = method.Invoke(obj, new object[] { "hello" });
// Get/set property
PropertyInfo prop = t.GetProperty("Length");
int len = (int)prop.GetValue(obj);
// Get/set field
FieldInfo field = t.GetField("_field", BindingFlags.NonPublic | BindingFlags.Instance);
field.SetValue(obj, 42);
// Create delegate from MethodInfo (faster than Invoke)
var appendDel = (Func<StringBuilder, string, StringBuilder>)
method.CreateDelegate(typeof(Func<StringBuilder, string, StringBuilder>));Atributos
Los atributos adjuntan metadatos a elementos de código (clases, métodos, propiedades). Defina con [AttributeUsage(...)] especificando los destinos válidos. Recupere vía GetCustomAttributes<T>() (genérico, moderno) o Attribute.GetCustomAttribute. Atributos integrados: [Obsolete], [Serializable], [DllImport], [Conditional]. Los frameworks usan atributos extensivamente: [HttpGet] (ASP.NET), [JsonProperty] (JSON), [Table] (EF Core).
// Define an attribute
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
public class AuthorAttribute : Attribute {
public string Name { get; }
public AuthorAttribute(string name) => Name = name;
}
// Apply
[Author("Alice")]
[Author("Bob")]
public class Widget {
[Author("Charlie")]
public void DoWork() { }
}
// Read via reflection
var attrs = typeof(Widget).GetCustomAttributes<AuthorAttribute>();
foreach (var a in attrs) Console.WriteLine(a.Name); // Alice, Bob
// Check if attribute is applied
bool hasAuthor = Attribute.IsDefined(typeof(Widget), typeof(AuthorAttribute));Emit y Generación de Código Dinámico
Reflection.Emit genera IL en tiempo de ejecución para máximo rendimiento en escenarios dinámicos (compilación de expresiones, serializadores, frameworks de mock). Construye tipos/métodos y emite opcodes IL directamente. Esto es avanzado—use árboles de expresión (Expression.Compile) para código dinámico más simple. Emit es usado por DLR (lenguajes dinámicos), ORMs y bibliotecas de serialización para generar código tipado rápido en tiempo de ejecución.
using System.Reflection;
using System.Reflection.Emit;
// Create a dynamic assembly/module/type
var asmName = new AssemblyName("Dynamic");
var asm = AssemblyBuilder.DefineDynamicAssembly(asmName, AssemblyBuilderAccess.Run);
var mod = asm.DefineDynamicModule("Main");
var type = mod.DefineType("Calculator", TypeAttributes.Public);
// Add a method
var method = type.DefineMethod("Add",
MethodAttributes.Public | MethodAttributes.Static,
typeof(int), new[] { typeof(int), typeof(int) });
// Emit IL
var il = method.GetILGenerator();
il.Emit(OpCodes.Ldarg_0); // load first arg
il.Emit(OpCodes.Ldarg_1); // load second arg
il.Emit(OpCodes.Add); // add
il.Emit(OpCodes.Ret); // return
Type created = type.CreateType();
int result = (int)created.GetMethod("Add").Invoke(null, new object[] { 3, 4 }); // 7Árboles de Expresión
Los árboles de expresión representan código como datos (un árbol de nodos Expression). Son la base de IQueryable (los proveedores LINQ los traducen a SQL, etc.). Compile() convierte una expresión en un delegado ejecutable. Puede construir expresiones manualmente para generación de código dinámico (más legible que Reflection.Emit). Los árboles de expresión habilitan ORMs, motores de reglas y LINQ dinámico.
using System.Linq.Expressions;
// Expression<Func<T>>: code as data (tree), not executable
Expression<Func<int, int, int>> expr = (a, b) => a + b + 1;
// Inspect the tree
Console.WriteLine(expr.Body.NodeType); // Add
Console.WriteLine(expr.Body.Type); // int
// Compile to a delegate (fast, runs in memory)
Func<int, int, int> compiled = expr.Compile();
int result = compiled(2, 3); // 6
// EF Core translates expressions to SQL
Expression<Func<User, bool>> filter = u => u.Age > 18;
// → WHERE Age > 18
// Build expressions manually (advanced)
ParameterExpression x = Expression.Parameter(typeof(int), "x");
Expression body = Expression.Multiply(x, Expression.Constant(2));
var lambda = Expression.Lambda<Func<int, int>>(body, x);
Func<int, int> doubler = lambda.Compile();Métodos de Extensión y Patrones
Métodos de Extensión
Los métodos de extensión le permiten 'añadir' métodos a tipos existentes sin modificarlos. Definidos en una clase estática con 'this' en el primer parámetro. Son azúcar sintáctico—el compilador reescribe email.IsEmail() a StringExtensions.IsEmail(email). LINQ es completamente métodos de extensión en IEnumerable. Use extensiones para añadir métodos de utilidad a clases selladas, interfaces o tipos de terceros. Tráigalos al ámbito con using.
public static class StringExtensions {
// Must be static, in static class, first param with 'this'
public static bool IsEmail(this string s) =>
s.Contains('@') && s.Contains('.');
public static string Repeat(this string s, int n) =>
string.Concat(Enumerable.Repeat(s, n));
// Chainable
public static string Truncate(this string s, int max) =>
s.Length <= max ? s : s[..max];
}
// Usage: appears as instance method
string email = "[email protected]";
bool valid = email.IsEmail();
string repeated = "ab".Repeat(3); // "ababab"
string short_ = "hello world".Truncate(5); // "hello"
// LINQ is built on extension methods (Enumerable class)
var evens = nums.Where(n => n % 2 == 0); // extensionInterfaces Fluidas
Las interfaces fluidas devuelven 'this' para habilitar el encadenamiento de métodos, produciendo código legible que se lee como una oración. Común en builders (QueryBuilder), configuración (ASP.NET, EF) y DSLs. Los métodos de extensión pueden añadir métodos fluidos a tipos primitivos (5.Seconds()). Mantenga los métodos puros (devolver nueva instancia) para APIs fluidas inmutables, o mute y devuelva this para builders.
public class QueryBuilder {
private string _table = "";
private string _where = "";
private int _limit;
public QueryBuilder From(string table) { _table = table; return this; }
public QueryBuilder Where(string clause) { _where = clause; return this; }
public QueryBuilder Limit(int n) { _limit = n; return this; }
public string Build() =>
$"SELECT * FROM {_table} WHERE {_where} LIMIT {_limit}";
}
// Fluent chaining
var sql = new QueryBuilder()
.From("users")
.Where("age > 18")
.Limit(10)
.Build();
// Extension methods enable fluent APIs on existing types
public static class IntExtensions {
public static TimeSpan Seconds(this int n) => TimeSpan.FromSeconds(n);
public static TimeSpan Minutes(this int n) => TimeSpan.FromMinutes(n);
}
var delay = 5.Seconds(); // TimeSpan
var timeout = 2.Minutes();Métodos Iteradores (yield)
yield return convierte un método en un iterador—el compilador genera una máquina de estados. La ejecución es perezosa: el código se ejecuta solo a medida que se extraen elementos. Esto habilita secuencias infinitas y streaming eficiente (procesar un elemento a la vez sin almacenar en búfer). yield break termina la iteración. El método debe devolver IEnumerable<T> o IEnumerator<T>. Los operadores LINQ se construyen sobre yield. Reenumerar vuelve a ejecutar el método.
// yield return: lazy iterator (state machine generated)
public static IEnumerable<int> Evens(int max) {
for (int i = 0; i <= max; i += 2)
yield return i;
}
// Lazy: nothing runs until enumerated
var evens = Evens(100); // no execution yet
foreach (var e in evens) { /* runs here */ }
// yield break: end iteration
public static IEnumerable<T> TakeWhile<T>(IEnumerable<T> src, Func<T, bool> pred) {
foreach (var item in src) {
if (!pred(item)) yield break;
yield return item;
}
}
// Infinite sequence (lazy, safe)
public static IEnumerable<int> Naturals() {
int n = 0;
while (true) yield return n++;
}
var first10 = Naturals().Take(10); // 0..9Inyección de Dependencias
La Inyección de Dependencias (DI) pasa dependencias vía constructores en lugar de crearlas internamente. Beneficios: testeabilidad (cambiar implementaciones en pruebas), bajo acoplamiento, responsabilidad única. El contenedor DI integrado de .NET (Microsoft.Extensions.DependencyInjection) maneja el registro y ciclo de vida (AddTransient, AddScoped, AddSingleton). Siempre dependa de interfaces, no clases concretas, para flexibilidad.
// Interface for the dependency
public interface IEmailSender {
Task SendAsync(string to, string body);
}
// Implementation
public class SmtpEmailSender : IEmailSender {
public async Task SendAsync(string to, string body) { /* SMTP */ }
}
// Consumer: depends on abstraction, not concretion
public class UserService {
private readonly IEmailSender _email;
public UserService(IEmailSender email) => _email = email; // inject
public async Task RegisterAsync(string email) {
// ... save user ...
await _email.SendAsync(email, "Welcome!");
}
}
// Register in DI container (ASP.NET Core)
builder.Services.AddScoped<IEmailSender, SmtpEmailSender>();
builder.Services.AddScoped<UserService>();
// Resolve
var userService = app.Services.GetRequiredService<UserService>();Patrones Factory y Builder
El patrón Factory centraliza la creación de objetos, ocultando clases concretas a los llamadores (útil cuando la creación es compleja o el tipo se elige en tiempo de ejecución). El patrón Builder construye objetos complejos paso a paso con métodos fluidos, evitando 'constructores telescópicos' (muchos parámetros). Ambos mejoran la legibilidad y mantenibilidad. Los records de C# con expresiones 'with' a menudo reemplazan a los builders para datos inmutables.
// Factory: create objects without specifying exact class
public interface IShape { void Draw(); }
public class Circle : IShape { public void Draw() => Console.WriteLine("○"); }
public class Square : IShape { public void Draw() => Console.WriteLine("□"); }
public static class ShapeFactory {
public static IShape Create(string type) => type switch {
"circle" => new Circle(),
"square" => new Square(),
_ => throw new ArgumentException()
};
}
// Builder: step-by-step construction of complex objects
public class Pizza {
public string Size { get; set; }
public bool Cheese { get; set; }
public bool Pepperoni { get; set; }
}
public class PizzaBuilder {
private readonly Pizza _p = new();
public PizzaBuilder WithSize(string s) { _p.Size = s; return this; }
public PizzaBuilder AddCheese() { _p.Cheese = true; return this; }
public PizzaBuilder AddPepperoni() { _p.Pepperoni = true; return this; }
public Pizza Build() => _p;
}
var pizza = new PizzaBuilder()
.WithSize("large").AddCheese().AddPepperoni().Build();Coincidencia de Patrones (C# 7-10+)
Patrones de Tipo e is
La coincidencia de patrones (C# 7+) combina verificación de tipo y vinculación de variables en un paso: 'o is string s' verifica y asigna. Los patrones de propiedad ({ Length: > 3 }) coinciden con propiedades de objeto. C# 9 añade los combinadores 'not', 'and', 'or'. Las expresiones switch (C# 8) devuelven valores y usan patrones—mucho más limpio que cadenas if-else. El _ es el patrón de descarte (caso predeterminado).
object o = "hello";
// Type pattern (C# 7)
if (o is string s) {
Console.WriteLine(s.Length); // s is string here
}
// Combined with null check
if (o is string { Length: > 3 } longStr) {
Console.WriteLine(longStr); // string with length > 3
}
// Negation and conjunction (C# 9)
if (o is not null) { /* ... */ }
if (o is string and not null) { /* ... */ }
// Switch on type
string Describe(object o) => o switch {
int i => $"Integer: {i}",
string s => $"String: {s}",
double d => $"Double: {d}",
null => "null",
_ => "Unknown"
};Patrones de Propiedad y Posicionales
Los patrones de propiedad coinciden con propiedades de objeto: { Prop: pattern }. Los patrones posicionales deconstruyen vía el método Deconstruct (los records lo tienen automáticamente). Los patrones se componen: puede anidarlos y combinarlos con operadores relacionales (<, >, etc.). C# 10 permite abreviatura de propiedad anidada (Name.Length en lugar de Name: { Length: }). La coincidencia de patrones hace que la lógica condicional compleja sea declarativa y legible.
public record Point(int X, int Y);
public record Person(string Name, int Age);
// Property pattern
string Describe(Person p) => p switch {
{ Age: < 18 } => "Minor",
{ Age: >= 65 } => "Senior",
{ Name.Length: > 10 } => "Long name",
{ } => "Adult" // non-null Person
};
// Positional pattern (deconstruction)
string Quadrant(Point p) => p switch {
(0, 0) => "Origin",
( > 0, > 0) => "Q1",
( < 0, > 0) => "Q2",
( < 0, < 0) => "Q3",
( > 0, < 0) => "Q4",
_ => "On axis"
};
// Nested patterns
bool IsValid(Person p) => p is { Name: { Length: > 0 }, Age: > 0 };
// C# 10: nested property shorthand
bool IsValid2(Person p) => p is { Name.Length: > 0, Age: > 0 };Patrones Relacionales y Lógicos (C# 9)
C# 9 introdujo patrones relacionales (<, >, <=, >=) y combinadores lógicos (and, or, not) para patrones. Estos hacen que las expresiones switch sean expresivas para verificaciones de rango y lógica de categorías. 'or' combina patrones (coincide si alguno); 'and' requiere ambos; 'not' niega. Los paréntesis agrupan para precedencia. Esto reemplaza cadenas if-else verbosas con lógica declarativa y exhaustiva (verificada por el compilador).
// Relational patterns: <, >, <=, >=
string Classify(int n) => n switch {
< 0 => "Negative",
0 => "Zero",
> 0 and < 10 => "Small positive",
>= 10 and < 100 => "Medium",
>= 100 => "Large"
};
// Logical patterns: and, or, not
bool IsWeekend(DayOfWeek d) => d is DayOfWeek.Saturday or DayOfWeek.Sunday;
// Combined with type patterns
decimal GetDiscount(object customer) => customer switch {
Student => 0.2m,
Senior or Veteran => 0.15m,
not null => 0.0m,
null => throw new ArgumentNullException()
};
// Parenthesized patterns for grouping
bool IsValid(int n) => n is (> 0 and < 10) or (> 100 and < 200);Patrones de Lista (C# 11)
Los patrones de lista (C# 11) coinciden con la forma de arrays y listas: vacío [], un elemento [x], exacto [a, b], o con slices [first, .., last]. El patrón slice (..) captura cero o más elementos del medio en una variable. Combinado con var y guards (when), esto habilita coincidencia expresiva en secuencias. Útil para análisis, validación e implementación de algoritmos.
// List patterns match array/list shapes
int[] nums = { 1, 2, 3, 4, 5 };
string Describe(int[] arr) => arr switch {
[] => "Empty",
[single] => $"Single: {single}",
[first, second] => $"Pair: {first}, {second}",
[first, .., last] => $"First: {first}, Last: {last}", // slice
[.., var last] => $"Last: {last}",
_ => "Other"
};
// Slice pattern (..) captures middle elements
if (nums is [var first, .. var middle, var last]) {
Console.WriteLine(`First {first}, middle count {middle.Length}, last {last}`);
}
// With guards
string Classify(int[] arr) => arr switch {
[var a, var b] when a == b => "Equal pair",
[var a, var b] => "Unequal pair",
_ => "Other"
};Expresiones Switch
Las expresiones switch (C# 8) son expresiones que devuelven valores, a diferencia de las instrucciones switch. Usan coincidencia de patrones (=>) y son más concisas. El switch de tuplas ((a, b) switch) maneja múltiples valores. El compilador verifica la exhaustividad para enums (advierte si falta un caso). Siempre incluya un caso predeterminado (_) a menos que quiera una excepción en tiempo de ejecución. Las expresiones switch se prefieren sobre las instrucciones para lógica que devuelve valores.
// Traditional switch statement (verbose)
string Old(int n) {
switch (n) {
case 1: return "one";
case 2: return "two";
default: return "many";
}
}
// Switch expression (C# 8): expression, returns value
string New(int n) => n switch {
1 => "one",
2 => "two",
_ => "many"
};
// With multiple values per arm
string Describe(int day, bool holiday) => (day, holiday) switch {
(6 or 7, _) => "Weekend",
(_, true) => "Holiday",
(1, false) => "Monday",
_ => "Weekday"
};
// Exhaustiveness: compiler warns if not all cases covered
// (especially with enums)Records e Inmutabilidad
Fundamentos de Records (C# 9)
Los Records (C# 9) son tipos de referencia con igualdad basada en valor—dos records son iguales si sus datos son iguales (a diferencia de las clases, que usan igualdad de referencia). El compilador genera Equals, GetHashCode, ToString y Deconstruct automáticamente. La expresión 'with' crea una copia con propiedades modificadas (mutación no destructiva). Los records son ideales para DTOs, objetos de valor y modelos de datos inmutables.
// Record: value-equality, immutable, concise
public record Person(string Name, int Age);
// Create
var p1 = new Person("Alice", 30);
var p2 = new Person("Alice", 30);
// Value equality (auto-generated)
Console.WriteLine(p1 == p2); // true (compares properties)
Console.WriteLine(p1.Equals(p2)); // true
// ToString (auto-generated, readable)
Console.WriteLine(p1); // Person { Name = Alice, Age = 30 }
// Deconstruction
var (name, age) = p1;
// Non-destructive mutation (with-expression)
var older = p1 with { Age = 31 };
// p1 is unchanged, older is a copy with Age = 31Record Structs (C# 10)
C# 10 añadió record structs (tipos de valor con características de record) y readonly record structs (tipos de valor inmutables). Elija: record class (tipo de referencia, para datos grandes/compartidos), record struct (tipo de valor, para datos pequeños, evita asignación en montículo), readonly record struct (tipo de valor inmutable, el más seguro). Los record structs tienen igualdad de valor y soporte 'with' como los record classes. Use readonly record struct para valores inmutables pequeños como Point, Money.
// record class (default, reference type)
public record PersonRef(string Name, int Age);
// record struct (C# 10): value type with record features
public record struct Point(int X, int Y);
// readonly record struct: fully immutable value type
public readonly record struct Money(decimal Amount, string Currency);
// Mutable record struct (allowed but discouraged)
public record struct Counter(int Value) {
public int Value { get; set; } = Value;
}
var p = new Point(1, 2);
var p2 = p with { X = 5 }; // copy with X=5
Console.WriteLine(p == p2); // false
// Records can have additional members
public record Person(string Name) {
public int Age { get; init; } // extra init-only prop
public string DisplayName => Name.ToUpper();
}Setters de Solo Init
Los setters init-only (C# 9) permiten establecer durante la inicialización del objeto pero no después—inmutabilidad después de la construcción sin código repetitivo de constructor. 'required' (C# 11) fuerza al llamador a establecer una propiedad en el inicializador (verificación en tiempo de compilación). Los records usan init setters por defecto. Esto habilita patrones de objetos inmutables con sintaxis de inicializador de objeto, que es más legible que los parámetros de constructor.
public class Config {
// init: settable only during construction (object initializer)
public string Name { get; init; } = "";
public int Timeout { get; init; } = 30;
}
var c = new Config { Name = "MyApp", Timeout = 60 };
// c.Name = "Other"; // ERROR: init-only after construction
// Required properties (C# 11): must be set in initializer
public class User {
public required string Email { get; init; }
public string? DisplayName { get; init; }
}
// Must set Email, DisplayName optional
var u = new User { Email = "[email protected]" };
// Records use init by default (positional parameters)
public record Point(int X, int Y);
// Equivalent to: public int X { get; init; }
// public int Y { get; init; }Herencia de Records
Los records admiten herencia: un record derivado incluye los parámetros del record base en su constructor. La expresión 'with' preserva el tipo en tiempo de ejecución (creando un Dog de un Dog, no un Animal). La igualdad verifica el tipo en tiempo de ejecución—dos records son iguales solo si son del mismo tipo con datos iguales. Esto hace que los records funcionen correctamente en colecciones polimórficas, a diferencia de la igualdad de valor ingenua.
// Base record
public record Animal(string Name);
// Derived record: must include base parameters
public record Dog(string Name, string Breed) : Animal(Name);
// Derived can add to base
public record Teacher(string Name, string Subject) : Person(Name);
var d = new Dog("Rex", "Labrador");
Console.WriteLine(d); // Dog { Name = Rex, Breed = Labrador }
// with preserves the runtime type
var d2 = d with { Name = "Buddy" };
Console.WriteLine(d2.GetType().Name); // Dog (not Animal)
// Value equality across hierarchy
Animal a = new Dog("Rex", "Lab");
Animal b = new Dog("Rex", "Lab");
Console.WriteLine(a == b); // true
// Different types are not equal even with same data
Animal c = new Animal("Rex");
Console.WriteLine(a == c); // false (different runtime types)Records vs Clases vs Structs
Las clases usan igualdad de referencia (== compara referencias); los records usan igualdad de valor (comparan datos); los structs usan igualdad de valor por defecto pero son tipos de valor (copiados en la asignación). Use records para datos inmutables con semántica de valor (DTOs, objetos de valor, mensajes). Use clases para entidades mutables con identidad (User, Order) y jerarquías de herencia. Use structs para valores pequeños (≤16 bytes) que deban copiarse (Point, Color, Money).
// Class: reference type, reference equality
public class PersonClass {
public string Name { get; set; }
public PersonClass(string name) => Name = name;
}
var c1 = new PersonClass("Alice");
var c2 = new PersonClass("Alice");
Console.WriteLine(c1 == c2); // false (different references)
// Record: reference type, value equality
public record PersonRec(string Name);
var r1 = new PersonRec("Alice");
var r2 = new PersonRec("Alice");
Console.WriteLine(r1 == r2); // true (value equality)
// Struct: value type, value equality (manual)
public struct PointStruct {
public int X, Y;
public PointStruct(int x, int y) { X = x; Y = y; }
}
var s1 = new PointStruct(1, 2);
var s2 = new PointStruct(1, 2);
Console.WriteLine(s1.Equals(s2)); // true (default struct equality)
// Choose:
// - record: immutable data with value equality (DTOs, value objects)
// - class: mutable, reference semantics, inheritance (entities, services)
// - struct: small, value type, avoid heap allocation (Point, Color)Coincidencia de Patrones en Profundidad
Patrones de Propiedad
Los patrones de propiedad coinciden con propiedades de objeto en línea. La expresión switch devuelve un valor directamente. Los patrones combinados (and/or) crean condiciones flexibles. null y _ (descarte) manejan casos límite. Esto reemplaza cadenas if-else verbosas con código declarativo y legible.
static decimal CalcDiscount(Customer c) => c switch
{
{ IsPremium: true, Orders.Count: > 100 } => 0.20m,
{ IsPremium: true } => 0.10m,
{ Orders.Count: > 50 } => 0.05m,
{ Country: "US" or "CA" } => 0.03m,
null => 0m,
_ => 0m
};Patrones de Lista
Los patrones de lista (C# 11) coinciden con arrays y secuencias indexables. [] coincide con vacío, [single] con un elemento, [first, .. rest] usa el patrón slice para capturar elementos restantes. Útil para analizar comandos, validar secuencias y algoritmos recursivos.
int Sum(int[] n) => n switch
{
[] => 0,
[int single] => single,
[int first, int second] => first + second,
[int first, .. int[] rest] => first + Sum(rest),
};
// Matching array shapes
var arr = new[] { 1, 2, 3, 4 };
// [1, ..] matches array starting with 1
// [.., 4] matches array ending with 4
// [1, 2, .., 4] matches 1,2,anything,4Patrones de Tupla
Los patrones de tupla coinciden múltiples valores simultáneamente. Cada caso prueba una tupla de valores. Combinado con patrones relacionales (>, <, >=) y patrones lógicos (and, or, not), esto crea coincidencia multidimensional potente sin instrucciones if anidadas.
static string Classify(int t, bool rain) => (t, rain) switch
{
(< 0, _) => "Freezing",
(>= 0 and < 15, true) => "Cold and wet",
(>= 0 and < 15, false) => "Cold",
(>= 15 and < 25, _) => "Mild",
(>= 25, true) => "Hot and wet",
(>= 25, false) => "Hot",
};Patrones de Tipo y Guards
Los patrones de tipo coinciden tipos en tiempo de ejecución. La cláusula when añade guards adicionales. var coincide con cualquier cosa (incluido null). El orden importa: los patrones más específicos deben ir antes que los generales. Esta es la forma idiomática de hacer despacho basado en tipo en C# moderno.
object Process(object input) => input switch
{
int i when i > 0 => $"Positive: {i}",
int i => $"Non-positive: {i}",
string s when s.Length > 10 => s.Substring(0, 10),
string s => s,
IList list => $"List with {list.Count} items",
var x => x?.ToString() ?? "null"
};Patrones Anidados
Los patrones se anidan arbitrariamente profundo. var dentro de un patrón captura el valor coincidente para usar en guards o el cuerpo de la expresión. Este ejemplo coincide propiedades de Point dentro de records Line. Los patrones anidados son potentes para validar grafos de objetos complejos de forma concisa.
record Point(int X, int Y);
record Line(Point Start, Point End);
string Describe(Line line) => line switch
{
Line({ X: 0, Y: 0 }, { X: 0, Y: 0 }) => "Point at origin",
Line({ X: var x1, Y: var y1 }, { X: var x2, Y: var y2 })
when x1 == x2 && y1 == y2 => "Degenerate line",
Line({ X: var x1 }, { X: var x2 }) when x1 == x2 => "Vertical",
Line({ Y: var y1 }, { Y: var y2 }) when y1 == y2 => "Horizontal",
_ => "Diagonal line"
};Records y Expresiones With
Igualdad de Valor en Records
Los records proporcionan igualdad basada en valor por defecto: dos instancias con los mismos datos son iguales. El compilador genera Equals, GetHashCode, ToString y el operador ==. Los records son tipos de referencia pero diseñados para inmutabilidad. Úselos para DTOs, objetos de valor y datos que deban compararse por contenido.
public record Person(string Name, int Age);
var p1 = new Person("Alice", 30);
var p2 = new Person("Alice", 30);
Console.WriteLine(p1 == p2); // True (value equality)
Console.WriteLine(p1.GetHashCode() == p2.GetHashCode()); // True
Console.WriteLine(p1); // Person { Name = Alice, Age = 30 }Expresiones With
La expresión with crea una copia de un record con propiedades modificadas. El original permanece sin cambios (mutación no destructiva). Esta es la forma idiomática de actualizar datos inmutables. Bajo el capó, el compilador usa un constructor de copia protegido y setters de solo init.
var alice = new Person("Alice", 30);
var alice31 = alice with { Age = 31 };
var aliceMarried = alice with { Name = "Alice Smith" };
// Original is unchanged (immutable)
Console.WriteLine(alice.Age); // 30
Console.WriteLine(alice31.Age); // 31
Console.WriteLine(aliceMarried.Name); // Alice SmithPosicional vs Solo Init
Los records posicionales usan sintaxis de constructor primario y generan métodos deconstruct. Los records de solo init usan sintaxis de inicializador de objeto, permitiendo valores predeterminados y modificadores required. Elija posicional para tipos de valor simples; solo init para records complejos con propiedades opcionales o calculadas.
// Positional record (primary constructor)
public record Point(int X, int Y);
// Init-only record (more flexible properties)
public record Employee
{
public required string Name { get; init; }
public decimal Salary { get; init; }
public string Dept { get; init; } = "General";
}
var emp = new Employee { Name = "Bob", Salary = 50000 };
var promoted = emp with { Salary = 60000 };Record Structs y Herencia
Los record structs son tipos de valor (copiados en la asignación) y no pueden heredar de otros record structs. Los records de tipo de referencia admiten herencia. readonly record struct previene la mutación. Use record struct para valores inmutables pequeños; record class para objetos más grandes que se benefician de semántica de referencia y herencia.
// Record struct (value type)
public readonly record struct Money(decimal Amount, string Currency);
// Record inheritance (reference type only)
public record Animal(string Name);
public record Dog(string Name, string Breed) : Animal(Name);
var dog = new Dog("Rex", "Labrador");
Console.WriteLine(dog is Animal); // True
var copied = dog with { Breed = "Poodle" };Deconstruct y Coincidencia
Los records posicionales generan automáticamente un método Deconstruct, habilitando la deconstrucción de tuplas. Esto se integra perfectamente con la coincidencia de patrones: puede coincidir propiedades de record posicionalmente o por nombre. La deconstrucción funciona en patrones var, patrones de tupla y expresiones switch.
var person = new Person("Alice", 30);
var (name, age) = person; // Deconstruct
Console.WriteLine($"{name} is {age}");
// Works with pattern matching
string Greet(Person p) => p switch
{
("Alice", _) => "Hi Alice!",
(_, < 18) => "Hello young one",
var (n, a) => $"Hello {n}, age {a}"
};Generadores de Código Fuente
Generador de Código Fuente Básico
Los generadores de código fuente se ejecutan en tiempo de compilación y añaden archivos de código fuente C# a la compilación. IIncrementalGenerator es la API moderna (C# 9+). RegisterPostInitializationOutput añade código estático sin analizar el código del usuario. Los generadores son de solo lectura: pueden añadir archivos pero no modificar código existente.
[Generator]
public class HelloGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
context.RegisterPostInitializationOutput(ctx =>
{
ctx.AddSource("Hello.g.cs", """
namespace Generated;
public static class Hello
{
public static string World => "Hello!";
}
""");
});
}
}Generación desde Atributos
ForAttributeWithMetadataName encuentra tipos marcados con un atributo específico. El pipeline: el proveedor de sintaxis filtra nodos, la transformación extrae datos, Collect agrupa resultados, RegisterSourceOutput emite código. Este pipeline incremental almacena en caché los resultados y solo regenera cuando las entradas cambian, manteniendo las compilaciones rápidas.
// Find types marked with an attribute
context.SyntaxProvider.ForAttributeWithMetadataName(
"MyApp.AutoToStringAttribute",
predicate: (node, _) => node is ClassDeclarationSyntax,
transform: (ctx, _) => (ClassDeclarationSyntax)ctx.TargetNode)
.Collect()
.RegisterSourceOutput((spc, classes) =>
{
foreach (var cls in classes)
GenerateToString(spc, cls);
});Salida StringBuilder
Los generadores producen código fuente como cadenas. StringBuilder es eficiente para salida multilínea. La clase objetivo debe ser partial para que el generador añada miembros. Los archivos generados aparecen en el IDE bajo Dependencies > Analyzers. Use el sufijo .g.cs por convención para distinguir archivos generados.
void GenToString(SourceProductionContext ctx, ClassDeclarationSyntax cls)
{
var sb = new StringBuilder();
sb.AppendLine($"namespace {cls.ContainingNamespace};");
sb.AppendLine($"public partial class {cls.Identifier}");
sb.AppendLine("{");
sb.AppendLine(" public override string ToString()");
sb.AppendLine(" {");
var props = cls.Members.OfType<PropertyDeclarationSyntax>();
sb.AppendLine($" return $"{cls.Identifier}: " +
string.Join(", ", props.Select(p => $"{p.Identifier}={{{p.Identifier}}}")) + "";");
sb.AppendLine(" }");
sb.AppendLine("}");
ctx.AddSource($"{cls.Identifier}_ToString.g.cs", sb.ToString());
}JsonSerializerContext
System.Text.Json incluye un generador de código fuente integrado que precompila la lógica de serialización, eliminando la reflexión en tiempo de ejecución. Esto mejora el rendimiento y admite la compilación AOT (Native AOT). Declare tipos con JsonSerializable, luego use el contexto generado para llamadas de serialización/deserialización.
[JsonSourceGenerationOptions(
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
WriteIndented = true)]
[JsonSerializable(typeof(List<Person>))]
[JsonSerializable(typeof(Dictionary<string, object>))]
public partial class AppJsonContext : JsonSerializerContext { }
// Usage (no runtime reflection):
var json = JsonSerializer.Serialize(people, AppJsonContext.Default.ListPerson);
var data = JsonSerializer.Deserialize(json, AppJsonContext.Default.ListPerson);Casos de Uso Comunes
Casos de uso populares de generadores: generación de DTOs, registro de DI, configuración fuertemente tipada, perfiles de AutoMapper, plantillas de logging y serialización JSON. Bibliotecas como MediatR, AutoMapper y Microsoft.Extensions.Logging usan generadores para reducir código repetitivo y mejorar la compatibilidad AOT.
// 1. DTO generation from interfaces
public interface IUserDto { string Name { get; } int Age { get; } }
// Generator creates: public class UserDto : IUserDto { ... }
// 2. Dependency injection registration
[RegisterService(ServiceLifetime.Scoped)]
public class MyService : IService { }
// Generator creates: services.AddScoped<MyService>();
// 3. Strongly-typed configuration
[ConfigurationSection("App:Db")]
public class DbSettings { public string Conn { get; set; } }
// Generator creates: services.Configure<DbSettings>(...)Span y Memory
Fundamentos de Span
Span<T> proporciona una vista de tipo seguro y memoria segura sobre memoria contigua sin copiar. Puede envolver arrays, memoria stackalloc, memoria no administrada o cadenas. Span es un ref struct (solo pila), por lo que no puede ser empaquetado (boxed), almacenado en campos o capturado por lambdas. Úselo para slicing y análisis de alto rendimiento.
// Span<T> is a stack-only view over contiguous memory
Span<int> numbers = stackalloc int[] { 1, 2, 3, 4, 5 };
// From array
int[] arr = { 10, 20, 30, 40, 50 };
Span<int> slice = arr.AsSpan(1, 3); // [20, 30, 40]
slice[0] = 99; // Modifies original array
// From string
ReadOnlySpan<char> text = "Hello, World".AsSpan();
ReadOnlySpan<char> hello = text.Slice(0, 5); // "Hello"Memory para Async
Memory<T> es la contraparte segura para el montículo de Span<T>. Puede almacenarse en campos, capturarse por lambdas y usarse a través de límites de await. Convierta a Span<T> vía .Span cuando haga trabajo real. Use Memory para APIs asíncronas y Span para rutas críticas sincrónicas.
// Memory<T> can live on the heap and cross async boundaries
async Task ProcessAsync(Memory<byte> buffer)
{
await Task.Delay(100);
Span<byte> span = buffer.Span;
for (int i = 0; i < span.Length; i++)
span[i] = (byte)(span[i] * 2);
}
byte[] data = new byte[1024];
await ProcessAsync(data); // Implicit conversion to Memory<byte>Evitar Asignaciones
Span elimina las asignaciones de cadenas intermedias durante el análisis y slicing. Para rutas críticas que procesan entradas grandes (análisis HTTP, JSON, archivos de log), esto puede reducir la presión del GC dramáticamente. int.TryParse y otras primitivas tienen sobrecargas de Span. Solo llame ToString() cuando realmente necesite una cadena.
// Traditional (allocates new string)
string Extract(string input, int start, int len)
=> input.Substring(start, len);
// Zero-allocation with Span
ReadOnlySpan<char> ExtractFast(ReadOnlySpan<char> input, int start, int len)
=> input.Slice(start, len);
// Parse without allocating substrings
bool TryParseInt(ReadOnlySpan<char> span, out int result)
=> int.TryParse(span, out result);Stackalloc y Unmanaged
stackalloc asigna en la pila de llamadas (auto-liberado, sin presión de GC) pero está limitado a tamaños pequeños. Para búferes grandes, use NativeMemory.Alloc/Free (C# 7+) o Marshal.AllocHGlobal. Siempre envuelva la memoria no administrada en un Span y libérela en un bloque finally para prevenir fugas.
// Stack allocation (small, fast, auto-freed)
Span<int> buffer = stackalloc int[128];
for (int i = 0; i < buffer.Length; i++)
buffer[i] = i;
// Unmanaged memory (large buffers, manual lifetime)
unsafe
{
int* ptr = (int*)NativeMemory.Alloc(1024, sizeof(int));
try
{
Span<int> span = new Span<int>(ptr, 1024);
span.Fill(42);
}
finally
{
NativeMemory.Free(ptr);
}
}APIs Amigables con Span
Diseñe APIs para aceptar Span<T> o ReadOnlySpan<T> para búferes, evitando asignaciones de arrays. El llamador decide si usar stackalloc, un array o memoria no administrada. Span.TryWrite (C# 10+) formatea directamente en un span sin asignar una cadena, útil para logging y serialización.
// Writing to a Span buffer
int WriteValues(Span<int> dest)
{
int count = 0;
foreach (var v in new[] { 1, 2, 3 })
{
if (count >= dest.Length) break;
dest[count++] = v;
}
return count;
}
// Span-based string formatting (C# 10+)
Span<char> dest = stackalloc char[64];
bool ok = dest.TryWrite($"Value: {42:N2}", out int charsWritten);
Console.WriteLine(dest.Slice(0, charsWritten).ToString());Cadenas Interpoladas en Profundidad
Especificadores de Formato
La interpolación de cadenas ($"...") incrusta expresiones entre llaves. Los especificadores de formato después de dos puntos controlan la salida: C para moneda, F para decimales fijos, X para hexadecimal, D para dígitos con relleno, P para porcentaje, yyyy-MM-dd para fechas. El compilador traduce esto a string.Format o FormattableString.
string name = "Alice";
int age = 30;
decimal price = 19.99m;
DateTime now = DateTime.Now;
Console.WriteLine($"Name: {name}, Age: {age}");
Console.WriteLine($"Price: {price:C}"); // $19.99
Console.WriteLine($"Date: {now:yyyy-MM-dd HH:mm:ss}");
Console.WriteLine($"Hex: {255:X}"); // FF
Console.WriteLine($"Padded: {age:D5}"); // 00030
Console.WriteLine($"Percent: {0.85:P0}"); // 85%Interpolación de Cadena Cruda
Las cadenas interpoladas textuales ($@) preservan las barras invertidas literalmente y permiten contenido multilínea. La interpolación de cadena cruda (C# 11) usa triples comillas y elimina todo el escape: no hay necesidad de escapar llaves o comillas. El número de signos $ controla cuántas llaves se necesitan para la interpolación.
// Verbatim interpolated ($@)
string path = $@"C:\Users\{name}\Documents";
string json = $@"{{
""name"": ""{name}"",
""age"": {age}
}}";
// Raw string interpolation (C# 11)
string query = $$"""
SELECT * FROM Users
WHERE Name = '{{name}}'
AND Age > {{age - 5}}
""";
// $$ means {{ }} for interpolation, no escaping neededFormattableString y Culture
FormattableString preserva la estructura de interpolación, permitiendo formato diferido con culturas específicas. Esto es crítico para bibliotecas que no deberían depender de la cultura actual. FormattableString.Invariant fuerza la cultura invariante, previniendo problemas específicos de configuración regional en serialización y logging.
// FormattableString captures the format for deferred execution
FormattableString msg = $"Hello {name}, you are {age} years old";
// Explicit culture (avoid CurrentCulture issues in libraries)
string german = msg.ToString(CultureInfo.GetCultureInfo("de-DE"));
string invariant = msg.ToString(CultureInfo.InvariantCulture);
// Invariant by default
string formatted = FormattableString.Invariant($"Total: {price}");Manejadores de Cadenas Interpoladas
Los manejadores de cadenas interpoladas (C# 10) permiten al compilador pasar componentes de interpolación directamente a un método, evitando la asignación de cadena cuando el resultado no se necesita. Así es como funciona el logging de alto rendimiento: si el nivel de log está deshabilitado, no se construye ninguna cadena. El manejador recibe literales y valores formateados por separado.
// Custom handler (C# 10+) for conditional allocation
[InterpolatedStringHandler]
public ref struct LogStringHandler
{
public LogStringHandler(int literal, int hole, Logger logger, out bool enabled)
{
enabled = logger.IsEnabled;
// Skip allocation if logging is disabled
}
public void AppendLiteral(string s) { /* ... */ }
public void AppendFormatted<T>(T value) { /* ... */ }
}
// Usage: no string allocation if log level is off
logger.Info($"Processing {item} with {count} items");Formato Basado en Span
Span.TryWrite formatea cadenas interpoladas directamente en un búfer asignado en la pila, evitando la asignación en el montículo por completo. string.Create con una cadena interpolada y cultura le da formato consciente de la cultura sin asignaciones intermedias. Estas son técnicas esenciales para rutas críticas en aplicaciones de alto rendimiento.
// Format directly into a buffer (C# 10+)
Span<char> buffer = stackalloc char[256];
if (buffer.TryWrite($"User: {name}, Age: {age}", out int chars))
{
var result = buffer[..chars];
Console.WriteLine(result);
}
// Concatenation with DefaultInterpolatedStringHandler
string built = string.Create(CultureInfo.InvariantCulture, $"Total={price}");Espacios de Nombres de Archivo
Sintaxis de Espacio de Archivo
Los espacios de nombres de archivo (C# 10) declaran el espacio de nombres para todo el archivo con un punto y coma en lugar de llaves. Esto elimina un nivel de indentación, haciendo el código más plano y fácil de leer. Solo se permite un espacio de nombres de archivo por archivo. Todos los tipos en el archivo pertenecen a ese espacio de nombres.
// Traditional (adds a level of indentation)
namespace MyApp.Services
{
public class UserService
{
public void Process() { }
}
}
// File-scoped (C# 10+, no indentation)
namespace MyApp.Services;
public class UserService
{
public void Process() { }
}Usings Globales
Los usings globales (C# 10) declarados en un archivo se aplican a todos los archivos del proyecto. Combine con espacios de nombres de archivo para un código limpio y con mínimo código repetitivo. Los proyectos .NET SDK generan automáticamente usings globales para espacios de nombres comunes (ImplicitUsings). Orden: usings globales, luego usings de archivo, luego espacio de nombres.
// Global usings (apply to all files in the project)
global using System.Collections.Generic;
global using System.Linq;
// File-scoped namespace with usings
using System.Text.Json;
using MyApp.Models;
namespace MyApp.Api;
public class UserController
{
// Can use List, LINQ, Json, Models without per-file usings
}Espacios de Nombres Anidados
Los espacios de nombres de archivo usan nombres punteados para jerarquía: MyApp.Services.Authentication es equivalente a bloques de espacio de nombres anidados. Múltiples archivos pueden compartir el mismo espacio de nombres. No puede anidar tipos en un espacio de nombres diferente dentro del mismo archivo; use archivos separados en su lugar.
// File-scoped: flat name with dots
namespace MyApp.Services.Authentication;
public class TokenService { }
// Multiple files can use the same namespace
// File 1: namespace MyApp.Services;
// File 2: namespace MyApp.Services; // Same namespaceConfiguración del Proyecto
ImplicitUsings en el archivo de proyecto genera automáticamente usings globales para espacios de nombres comunes de System. Combinado con espacios de nombres de archivo, esto elimina la mayoría del código repetitivo. Nullable habilita los tipos de referencia nullable. Estos son los valores predeterminados para nuevos proyectos .NET 6+.
<!-- .csproj -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
<!-- ImplicitUsings generates:
global using System;
global using System.Collections.Generic;
global using System.IO;
global using System.Linq;
global using System.Net.Http;
global using System.Threading;
global using System.Threading.Tasks;
-->Migración y Mejores Prácticas
Use .editorconfig para forzar espacios de nombres de archivo (IDE0161). El IDE proporciona migración con un clic de tradicional a espacio de archivo. Mejor práctica: un tipo público por archivo, con el nombre del archivo coincidiendo con el nombre del tipo. Esto mantiene los archivos pequeños y la navegación fácil.
// .editorconfig to enforce file-scoped namespaces
[*.{cs,vb}]
dotnet_diagnostic.IDE0161.severity = error // Prefer file-scoped
// Migration: use the IDE fixer to convert
// Right-click > Quick Actions > Convert to file-scoped namespace
// Best practice: one type per file
// UserService.cs
namespace MyApp.Services;
public class UserService { }Miembros Required
Propiedades Required
El modificador required (C# 11) fuerza a los llamadores a inicializar propiedades durante la creación del objeto. El compilador emite CS9035 si un miembro required no se establece. Esto reemplaza la validación en constructor para propiedades obligatorias. Funciona con setters de solo init e inicializadores de objeto, proporcionando garantías en tiempo de compilación.
public class User
{
public required Guid Id { get; init; }
public required string Name { get; init; }
public required string Email { get; init; }
public string? Phone { get; init; } // Optional
}
// Compiler enforces initialization
var user = new User
{
Id = Guid.NewGuid(),
Name = "Alice",
Email = "[email protected]"
};
// Error CS9035: Required member 'Name' must be set
// var bad = new User { Id = Guid.NewGuid() };SetsRequiredMembers
El atributo SetsRequiredMembers le dice al compilador que un constructor inicializa todos los miembros required, permitiendo a los llamadores usar el constructor sin un inicializador de objeto. Esto cierra la brecha entre propiedades required y constructores tradicionales. El atributo no verifica en tiempo de ejecución; es una promesa en tiempo de compilación.
public class Configuration
{
public required string ConnectionString { get; init; }
public required int Port { get; init; }
[SetsRequiredMembers]
public Configuration(string conn, int port)
{
ConnectionString = conn;
Port = port;
}
}
// Both work: constructor or initializer
var c1 = new Configuration("localhost", 5432);
var c2 = new Configuration
{
ConnectionString = "localhost",
Port = 5432
};Required con Records
Los miembros required funcionan con records. Para records posicionales, las propiedades required se declaran en el cuerpo, no en el constructor primario. La expresión with preserva los valores required automáticamente. Los valores predeterminados (como OpenedAt) hacen que las propiedades sean opcionales sin el modificador required.
public record Account
{
public required string AccountNumber { get; init; }
public required decimal Balance { get; init; }
public DateTime OpenedAt { get; init; } = DateTime.UtcNow;
}
// Positional record with required
public record Customer(string Name)
{
public required string Email { get; init; }
}
var cust = new Customer("Alice") { Email = "[email protected]" };Init vs Required vs Set
init hace que una propiedad solo se pueda establecer durante la construcción (inmutable después). required fuerza la inicialización. Juntos (required + init) crean propiedades inmutables obligatorias. required + set permite modificación después de la construcción. Elija según si el valor debería cambiar después de la creación.
// Init-only: can be set in initializer, but not required
public class Settings
{
public string Name { get; init; } = "Default";
}
// Required + init: must be set, immutable after
public class SecureSettings
{
public required string ApiKey { get; init; }
}
// Required + set: must be set, but can be modified later
public class MutableConfig
{
public required string Environment { get; set; }
}Serialización JSON
System.Text.Json respeta el modificador required: la deserialización lanza JsonException si falta una propiedad required en el JSON. Esto proporciona validación en tiempo de ejecución que coincide con el contrato en tiempo de compilación. Combine con init para DTOs inmutables que tienen garantizados todos los campos obligatorios poblados.
using System.Text.Json;
public class ApiResponse
{
public required string Status { get; init; }
public required int Code { get; init; }
public string? Message { get; init; }
}
// Deserialization enforces required at runtime
var json = """{"Status":"OK","Code":200}""";
var response = JsonSerializer.Deserialize<ApiResponse>(json);
// Throws JsonException if required member is missing
// var bad = JsonSerializer.Deserialize<ApiResponse>("""{"Code":200}""");Expresiones de Colección
Sintaxis de Expresión de Colección
Las expresiones de colección (C# 12) proporcionan una sintaxis uniforme [..] para inicializar cualquier tipo de colección. El compilador infiere el tipo y usa el builder apropiado. Esto reemplaza la verbosa sintaxis new List<int> { 1, 2, 3 }. Funciona con arrays, List<T>, Span<T>, IEnumerable<T>, y colecciones personalizadas con un atributo CollectionBuilder.
// C# 12 collection expressions
int[] numbers = [1, 2, 3, 4, 5];
List<string> names = ["Alice", "Bob", "Charlie"];
Span<int> span = [10, 20, 30];
IEnumerable<int> seq = [1, 2, 3];
// Empty collections
int[] empty = [];
List<int> emptyList = [];
// Works with any collection type that has a builder
HashSet<string> set = ["a", "b", "c"];Operador Spread
El operador spread (..) aplana una colección en la expresión de colección envolvente. Esto reemplaza Concat, AddRange y bucles manuales. Puede mezclar spreads con elementos individuales. El spread funciona con cualquier IEnumerable<T>, facilitando combinar múltiples fuentes de datos de forma concisa.
int[] a = [1, 2, 3];
int[] b = [4, 5, 6];
// Spread elements with ..
int[] combined = [..a, ..b, 7, 8]; // [1,2,3,4,5,6,7,8]
// Mix spreads and literals
List<int> result = [0, ..a, 100, ..b];
// Spread any IEnumerable
List<int> evens = [.. Enumerable.Range(0, 10).Where(x => x % 2 == 0)];Patrones de Retorno y Parámetros
Las expresiones de colección funcionan en instrucciones de retorno, argumentos y asignaciones. El compilador convierte al tipo objetivo automáticamente. Los spreads condicionales (ternario con ..) le permiten incluir elementos opcionalmente. Esto hace que la construcción de colecciones desde múltiples fuentes opcionales sea limpia y legible.
// Return collection expressions
List<int> GetNumbers() => [1, 2, 3];
int[] GetArray() => [10, 20];
// Pass as arguments
void Process(IEnumerable<int> data) { }
Process([1, 2, 3]);
// Conditional spread
bool includeExtra = true;
var items = [
1, 2, 3,
.. includeExtra ? [4, 5] : []
];Builders de Colección Personalizados
El atributo CollectionBuilder conecta un tipo de colección a un método factory, habilitando el soporte de expresiones de colección. El método Create recibe un ReadOnlySpan<T> de elementos. Esto permite que colecciones de terceros participen en la sintaxis uniforme [..]. El patrón builder mantiene la inicialización eficiente.
[CollectionBuilder(typeof(MyCollection), "Create")]
public class MyCollection<T> : IEnumerable<T>
{
private readonly List<T> _items = new();
public IEnumerator<T> GetEnumerator() => _items.GetEnumerator();
public static MyCollection<T> Create(ReadOnlySpan<T> items)
{
var col = new MyCollection<T>();
foreach (var item in items) col._items.Add(item);
return col;
}
}
// Now usable with collection expressions
MyCollection<int> custom = [1, 2, 3, 4];Colecciones Params
C# 12 permite params con cualquier tipo de colección, no solo arrays. Las expresiones de colección se pueden pasar a métodos params. Cuando el objetivo es Span<T>, el compilador puede usar asignación en pila para colecciones pequeñas, evitando la asignación en montículo por completo. Esto hace que los métodos variádicos sean tanto ergonómicos como eficientes.
// C# 12: params with collection expressions
void Log(params string[] messages)
{
foreach (var m in messages) Console.WriteLine(m);
}
// Traditional params call
Log("Error", "Warning", "Info");
// Collection expression call (C# 12)
Log(["Error", "Warning", "Info"]);
// params with Span for zero-allocation
void ProcessFast(params Span<int> values) { }
ProcessFast([1, 2, 3]); // May use stackalloc internallyFragmentos de C# relacionados
Copy-paste ready code for common tasks.
LINQ Query
Filter, project, sort, group, and aggregate sequences with LINQ.
Async and Await
Run I/O concurrently with async methods, await, and Task.WhenAll.
Properties
Encapsulate state with auto, computed, validated, and init-only properties.
Generics
Write type-parameterized methods and classes with constraints.
Delegates and Events
Define delegate types and publish events with safe subscription.
Reflection
Inspect type metadata and invoke members at runtime via System.Reflection.
Collections
Use List, Dictionary, HashSet, Queue, Stack, and read-only views.
File I/O
Read, write, append, copy, and JSON-serialize files with File and streams.
Was this helpful?