← maurobernal.com.ar

Etiqueta: collections

  • LINQ en .NET 9 y .NET 10: CountBy, AggregateBy, Index() y las mejoras que cambian cómo consultás datos

    LINQ es una de las features de C# que más uso cada día. Y con .NET 9 y .NET 10 recibió adiciones concretas que resuelven patrones que antes requerían GroupBy + Select + ToDictionary en cadena. CountBy, AggregateBy e Index son pequeños pero cambian bastante el código de consulta.

    Index(): el iterador con índice que debería haber existido siempre

    Cuántas veces escribiste .Select((item, i) => (i, item)) solo para tener el índice en un foreach? Index() hace exactamente eso con una API limpia.

    var productos = new[] { "Monitor", "Teclado", "Mouse", "Auriculares" };
    
    // ❌ Antes: Select con tupla o variable externa
    int i = 0;
    foreach (var p in productos)
        Console.WriteLine($"{i++}. {p}");
    
    // O la versión LINQ verbose:
    foreach (var (indice, producto) in productos.Select((p, i) => (i, p)))
        Console.WriteLine($"{indice}. {producto}");
    
    // ✅ .NET 9: Index() — limpio y declarativo
    foreach (var (indice, producto) in productos.Index())
        Console.WriteLine($"{indice}. {producto}");
    
    // También útil en proyecciones
    var conIndice = productos.Index()
        .Where(x => x.Index % 2 == 0)   // solo los pares
        .Select(x => $"[{x.Index}] {x.Item}");
    

    CountBy(): contar por clave sin GroupBy

    var pedidos = new[]
    {
        new { Cliente = "Ana",   Estado = "Pagado",    Monto = 100m },
        new { Cliente = "Juan",  Estado = "Pendiente", Monto = 200m },
        new { Cliente = "Ana",   Estado = "Pagado",    Monto = 150m },
        new { Cliente = "Pedro", Estado = "Pendiente", Monto = 300m },
        new { Cliente = "Juan",  Estado = "Cancelado", Monto = 50m  },
    };
    
    // ❌ Antes: GroupBy + Count — verboso
    var conteoAntes = pedidos
        .GroupBy(p => p.Cliente)
        .Select(g => new { Cliente = g.Key, Cantidad = g.Count() })
        .ToDictionary(x => x.Cliente, x => x.Cantidad);
    
    // ✅ .NET 9: CountBy — directo al punto
    var conteoPorCliente = pedidos.CountBy(p => p.Cliente);
    // { "Ana": 2, "Juan": 2, "Pedro": 1 }
    
    // CountBy devuelve IEnumerable<KeyValuePair<TKey, int>>
    foreach (var (cliente, cantidad) in conteoPorCliente)
        Console.WriteLine($"{cliente}: {cantidad} pedidos");
    
    // Contar por múltiples criterios con tipo anónimo
    var conteoPorEstado = pedidos.CountBy(p => p.Estado);
    // { "Pagado": 2, "Pendiente": 2, "Cancelado": 1 }

    AggregateBy(): acumulación agrupada sin GroupBy

    // ❌ Antes: GroupBy + Sum — varios pasos
    var totalesAntes = pedidos
        .GroupBy(p => p.Cliente)
        .ToDictionary(g => g.Key, g => g.Sum(p => p.Monto));
    
    // ✅ .NET 9: AggregateBy — acumulación directa
    var totalPorCliente = pedidos.AggregateBy(
        keySelector: p => p.Cliente,
        seed: 0m,
        func: (acumulado, pedido) => acumulado + pedido.Monto
    );
    // { "Ana": 250, "Juan": 250, "Pedro": 300 }
    
    // Caso más complejo: acumular un objeto personalizado
    var resumenPorCliente = pedidos.AggregateBy(
        keySelector: p => p.Cliente,
        seed: (Cantidad: 0, Total: 0m, UltimoPedido: ""),
        func: (acc, p) => (
            Cantidad: acc.Cantidad + 1,
            Total: acc.Total + p.Monto,
            UltimoPedido: p.Estado
        )
    );
    
    foreach (var (cliente, resumen) in resumenPorCliente)
        Console.WriteLine($"{cliente}: {resumen.Cantidad} pedidos, ${resumen.Total} total");

    Mejoras de rendimiento en LINQ (.NET 9 y .NET 10)

    Más allá de las nuevas APIs, .NET 9 y .NET 10 mejoran el rendimiento interno de operaciones LINQ existentes. Algunas que noté en benchmarks reales:

    • Order() / OrderDescending(): versiones sin selector — más rápidas que OrderBy(x => x)
    • GroupBy con span: reducción de allocations en agrupaciones de tipos por valor
    • Where + Select fusionados: el JIT puede combinar estas operaciones en algunos casos
    • ToFrozenDictionary() / ToFrozenSet(): colecciones inmutables optimizadas para lectura frecuente
    // ToFrozenDictionary: para lookups frecuentes de datos que no cambian
    // Más rápido que Dictionary en lecturas; más lento en construcción
    using System.Collections.Frozen;
    
    var codigosPais = new Dictionary<string, string>
    {
        { "AR", "Argentina" }, { "BR", "Brasil" }, { "CL", "Chile" }
    };
    
    // Convertir a FrozenDictionary — operación costosa una sola vez
    FrozenDictionary<string, string> lookup = codigosPais.ToFrozenDictionary();
    
    // Lookups subsiguientes son más rápidos que Dictionary estándar
    string pais = lookup["AR"];  // Acceso optimizado

    Mi cheat sheet de LINQ moderno

    NecesidadAntesAhora (.NET 9+)
    Iterar con índice.Select((x,i)=>(i,x)).Index()
    Contar por clave.GroupBy(k).Select(count).CountBy(k)
    Acumular por clave.GroupBy(k).ToDictionary(sum).AggregateBy(k, seed, func)
    Ordenar elementos.OrderBy(x => x).Order()
    Lookup inmutablenew Dictionary<>().ToFrozenDictionary()

    ← Extension Members en C# 14/15: propiedades y miembros estáticos de extensión como nunca antes | Serie .NET 8 → .NET 10 | Próximo: Rendimiento extremo en .NET 10: Stack allocation, Native AOT y el GC que trabaja menos →

  • C# 12: Primary Constructors, Collection Expressions y el código que debería haber existido siempre

    C# 12 llegó con .NET 8 y fue la versión que más cambió mi forma de escribir código en el día a día. Los Primary Constructors me sacaron dos tercios del boilerplate de mis servicios. Las Collection Expressions me hicieron olvidar cuándo usar new List<> vs new[] vs Array.Empty(). Repaso todo lo que realmente uso.

    Primary Constructors: adiós al boilerplate de inyección de dependencias

    Antes de C# 12, cada servicio que necesitaba inyección de dependencias requería declarar campos privados, un constructor y asignaciones manuales. Con Primary Constructors, todo eso desaparece.

    // ❌ Antes (C# 11 y anteriores): mucho código repetitivo
    public class PedidoService
    {
        private readonly IRepositorio _repo;
        private readonly ILogger<PedidoService> _logger;
        private readonly IEmailService _email;
    
        public PedidoService(
            IRepositorio repo,
            ILogger<PedidoService> logger,
            IEmailService email)
        {
            _repo   = repo;
            _logger = logger;
            _email  = email;
        }
    
        public async Task ProcesarAsync(Pedido pedido)
        {
            _logger.LogInformation("Procesando pedido {Id}", pedido.Id);
            await _repo.GuardarAsync(pedido);
            await _email.NotificarAsync(pedido.ClienteEmail);
        }
    }
    
    // ✅ C# 12: Primary Constructor — el compilador genera el backing field
    public class PedidoService(
        IRepositorio repo,
        ILogger<PedidoService> logger,
        IEmailService email)
    {
        public async Task ProcesarAsync(Pedido pedido)
        {
            logger.LogInformation("Procesando pedido {Id}", pedido.Id);
            await repo.GuardarAsync(pedido);
            await email.NotificarAsync(pedido.ClienteEmail);
        }
    }

    Los parámetros del primary constructor son accesibles en todo el cuerpo de la clase. No son campos públicos — son parámetros capturados. Si necesitás exponerlos como propiedades, lo hacés explícitamente:

    // Primary constructor en record (siempre existió)
    public record Producto(string Nombre, decimal Precio, int Stock);
    
    // Primary constructor en clase con propiedad explícita
    public class Configuracion(string connectionString, int timeout)
    {
        public string ConnectionString { get; } = connectionString;
        public int TimeoutSeconds { get; } = timeout > 0 ? timeout : 30;
    }

    Collection Expressions: una sintaxis para todos los tipos de colección

    Antes de C# 12, la forma de inicializar una colección dependía de su tipo. Un array era new[] { 1, 2, 3 }, una lista era new List<int> { 1, 2, 3 }, un span era algo diferente. C# 12 unifica todo con [].

    // ✅ C# 12: misma sintaxis para todos los tipos
    int[]          array   = [1, 2, 3];
    List<int>      lista   = [4, 5, 6];
    Span<int>      span    = [7, 8, 9];
    IEnumerable<int> seq  = [10, 11, 12];
    ReadOnlySpan<int> ros  = [13, 14, 15];
    
    // Colección vacía — sin ambigüedades
    List<string> vacia = [];   // antes: new List<string>() o Array.Empty<string>()
    
    // Spread operator (..) para combinar colecciones
    int[] primeros = [1, 2, 3];
    int[] segundos = [4, 5, 6];
    int[] todos    = [..primeros, ..segundos, 7, 8];  // [1,2,3,4,5,6,7,8]
    
    // Muy útil en APIs: combinar listas de validación
    var reglas = [..reglasBase, ..reglasEspecificas, nuevaRegla];

    Using aliases para cualquier tipo

    Antes de C# 12, using solo podía crear alias para tipos nombrados. Ahora puede crear alias para cualquier tipo: tuplas, arrays, punteros, tipos genéricos.

    // C# 12: aliases para tipos complejos
    using Coordenadas   = (double Latitud, double Longitud);
    using MatrizInt     = int[][];
    using DiccionarioId = System.Collections.Generic.Dictionary<int, string>;
    using Callback      = System.Action<string, int>;
    
    // Uso en el código — mucho más legible
    Coordenadas ubicacion = (Latitud: -34.6037, Longitud: -58.3816);
    DiccionarioId usuarios = new() { { 1, "Mauro" }, { 2, "Juan" } };

    Inline Arrays: buffers de alto rendimiento sin unsafe

    Los Inline Arrays son structs con un tamaño fijo definido en compilación. Se almacenan completamente en el stack, sin asignaciones en el heap. Útiles para código de sistema o cuando necesitás buffers de tamaño conocido con rendimiento máximo.

    "[System.Runtime.CompilerServices.InlineArray(8)]
    public struct Buffer8<T>
    {
        private T _element; // El compilador genera 8 slots contiguos en memoria
    }
    
    // Uso: se comporta como un array pero sin allocations
    Buffer8<int> buffer;
    buffer[0] = 100;
    buffer[7] = 800;
    // Todo en el stack — el GC no ve nada de esto

    Lambdas con parámetros opcionales y params

    // C# 12: parámetros opcionales en lambdas
    var saludar = (string nombre, string prefijo = "Hola") => $"{prefijo}, {nombre}!";
    Console.WriteLine(saludar("Mauro"));          // "Hola, Mauro!"
    Console.WriteLine(saludar("Mauro", "Buenas")); // "Buenas, Mauro!"
    
    // params en lambdas
    var sumar = (params int[] numeros) => numeros.Sum();
    Console.WriteLine(sumar(1, 2, 3, 4, 5));  // 15

    El impacto en el código real

    En un proyecto de tamaño mediano que migré a C# 12, los Primary Constructors redujeron el código de los servicios en aproximadamente un 30%. No es solo cosmético: menos código es menos superficie de error. Las Collection Expressions eliminaron la pregunta «¿new List<> o Array.Empty<>?» de los code reviews. El spread operator simplificó docenas de patrones de concatenación de listas que antes requerían AddRange o LINQ Concat.


    ← De .NET 8 a .NET 10: qué cambió, qué mejoró y por qué conviene actualizar ya | Serie .NET 8 → .NET 10 | Próximo: C# 13: params modernos, Lock de primera clase y Task.WhenEach →

Tags

tsql (27)mssql (26)sql (20)devops (20)dotnet (18)docker (15)performance (14)contenedores (11)dotnet10 (10)linux (9)csharp (8)microservicios (7)angular (7)angular21 (7)sql server (6)issabel (6)docker-compose (6)typescript (6)mysql (5).NET (5)