02 julio 2025

Características Destacadas de C# 11 y 12: Escribe Menos, Haz Más

En este artículo, exploraremos algunas de las nuevas características introducidas en C# 11 y 12 que ayudan a simplificar tu código y hacen que el desarrollo sea más fluido. Estas actualizaciones pueden no ser revolucionarias, pero son prácticas y están diseñadas para ahorrar tiempo al reducir la complejidad innecesaria. Veremos cómo pequeños cambios pueden llevar a soluciones más limpias y eficientes en las tareas de codificación diarias.

Literales de Cadena sin Procesar

Construir cadenas con contenido complejo ha sido históricamente un desafío en C#. Los desarrolladores frecuentemente tienen que lidiar con el escape de caracteres especiales, saltos de línea y comillas, lo que resulta en un código verbose y a menudo ilegible. Este proceso se vuelve particularmente engorroso al trabajar con formatos como JSON, XML o expresiones regulares incrustadas directamente en los archivos fuente.

C# 11 introdujo los literales de cadena sin procesar para abordar directamente este problema. Esta característica permite que las cadenas abarquen múltiples líneas y contengan prácticamente cualquier carácter, incluidas comillas y barras invertidas incrustadas, sin necesidad de secuencias de escape. Un literal de cadena sin procesar comienza y termina con al menos tres caracteres de comilla doble (""").

Antes de C# 11:

string oldJson = "{\r\n  \"name\": \"Alice\",\r\n  \"age\": 30\r\n}";
Console.WriteLine(oldJson);

Con C# 11:

string newJson = """
  {
    "name": "Alice",
    "age": 30
  }
  """;
Console.WriteLine(newJson);

Cualquier espacio en blanco que preceda a las comillas de cierre define la indentación mínima para la cadena, que el compilador elimina de la salida final. Los literales de cadena sin procesar mejoran drásticamente la legibilidad de las cadenas y reducen la probabilidad de errores de sintaxis.

Patrones de Lista

La coincidencia de patrones en C# ha evolucionado significativamente, y C# 11 introduce patrones de lista para permitir la coincidencia de secuencias dentro de arreglos o listas. Esta mejora permite a los desarrolladores inspeccionar la estructura y el contenido de colecciones de manera concisa y expresiva.

Anteriormente, validar la estructura de una colección requería verificaciones manuales de longitud e índices individuales, lo que resultaba en un código verbose y menos mantenible. Los patrones de lista abordan esto al soportar subpatrones como patrones constantes, de tipo, de propiedad y relacionales. Las características clave incluyen el patrón de descarte (_) para coincidir con cualquier elemento individual y el patrón de rango (..) para coincidir con cualquier secuencia de cero o más elementos.

Antes de C# 11:

int[] numbers = { 1, 2, 3 };

if (numbers != null && numbers.Length == 3 &&
    numbers[0] == 1 && numbers[1] == 2 && numbers[2] == 3)
{
    Console.WriteLine("El arreglo contiene exactamente 1, 2, 3.");
}

if (numbers != null && numbers.Length >= 2 && numbers[1] == 2)
{
    Console.WriteLine("El arreglo tiene 2 como su segundo elemento.");
}

Con C# 11:

int[] numbers = { 1, 2, 3 };

if (numbers is [1, 2, 3])
{
    Console.WriteLine("El arreglo contiene exactamente 1, 2, 3.");
}

if (numbers is [_, 2, ..])
{
    Console.WriteLine("El arreglo tiene 2 como su segundo elemento.");
}

Los patrones de lista simplifican la validación de secuencias en una forma compacta y legible, reduciendo significativamente las líneas de código necesarias para tales operaciones.

Miembros Requeridos

La inicialización de objetos a veces puede llevar a un estado indeseable donde propiedades o campos esenciales permanecen sin asignar. Tradicionalmente, los desarrolladores imponen la inicialización obligatoria a través de constructores que aceptan todos los parámetros requeridos o mediante la adición de verificaciones defensivas dentro de los métodos.

C# 11 introduce el modificador required para propiedades y campos, un mecanismo de aplicación en tiempo de compilación. Cuando un miembro se marca como required, el compilador asegura que reciba un valor durante la creación del objeto, ya sea a través de un constructor o un inicializador de objeto. Esto garantiza que las instancias de un tipo siempre estén en un estado válido y completamente inicializado, previniendo errores comunes relacionados con datos faltantes.

Antes de C# 11:

public class OldPerson
{
    public string FirstName { get; set; }
    public string LastName { get; set; }

    public void DisplayName() => Console.WriteLine($"Nombre: {FirstName} {LastName}");
}

// Uso:
var person = new OldPerson(); // Sin error en tiempo de compilación, pero crea un objeto potencialmente inválido
person.DisplayName();

Con C# 11:

public class NewPerson
{
    public required string FirstName { get; init; }
    public required string LastName { get; init; }

    public void DisplayName() => Console.WriteLine($"Nombre: {FirstName} {LastName}");
}

// Uso:
// var person = new NewPerson(); // Error de compilación - faltan propiedades requeridas
// var person = new NewPerson { FirstName = "John" }; // Error de compilación - falta LastName
var person = new NewPerson { FirstName = "Jane", LastName = "Doe" }; // Correcto
person.DisplayName();

Los miembros requeridos eliminan sorpresas en tiempo de ejecución al imponer la inicialización en tiempo de compilación, reduciendo la necesidad de verificaciones manuales. Esta característica mejora la confiabilidad del código con menos codificación defensiva, permitiendo a los desarrolladores centrarse en la funcionalidad en lugar de la validación.

Constructores Primarios

C# 12 introduce constructores primarios para todas las clases y estructuras, expandiendo una característica que antes era exclusiva de los tipos de registro. Esto permite que los parámetros del constructor se declaren directamente en la definición del tipo, asignándolos automáticamente como campos o propiedades en toda la clase. A diferencia de los constructores tradicionales, este enfoque omite declaraciones explícitas de campos y asignaciones manuales.

El problema clave que se resuelve aquí es el código repetitivo en la inicialización de objetos. Anteriormente, los desarrolladores tenían que definir campos privados y mapear explícitamente los argumentos del constructor a ellos, aumentando innecesariamente el tamaño del código. Los constructores primarios simplifican esto, integrando la lógica de inicialización directamente en la firma del tipo.

Antes de C# 12:

public class OldProduct
{
    private readonly int _productId;
    private readonly string _productName;

    public OldProduct(int productId, string productName)
    {
        _productId = productId;
        _productName = productName;
    }

    public string PrintDetails() => $"ID del Producto: {_productId}, Nombre: {_productName}";
}

// Uso:
OldProduct oldProd = new OldProduct(101, "Portátil");
oldProd.PrintDetails();

Con C# 12:

public class NewProduct(int productId, string productName)
{
    public string PrintDetails() => $"ID del Producto: {productId}, Nombre: {productName}";
}

// Uso:
NewProduct newProd = new NewProduct(102, "Teclado");
newProd.PrintDetails();

Los constructores primarios hacen que la definición de tipos centrados en datos sea increíblemente concisa. Mejoran la legibilidad al colocar los parámetros de construcción esenciales inmediatamente junto al nombre del tipo, dejando claras las dependencias de la clase o estructura de un vistazo.

Expresiones de Colección

Inicializar colecciones en C# históricamente ha involucrado varias sintaxis dependiendo del tipo de colección, como new List<T> { ... } para listas o new T[] { ... } para arreglos. Combinar o fusionar colecciones existentes en una nueva a menudo requería bucles iterativos o métodos LINQ como Concat(), añadiendo sobrecarga y verbosidad.

C# 12 introduce expresiones de colección, una sintaxis unificada y concisa para crear e inicializar una amplia gama de tipos de colección. Usando una simple sintaxis [...], los desarrolladores pueden crear arreglos, listas, Span<T> y otros tipos similares a colecciones. El nuevo elemento de dispersión (..) permite incorporar elementos de colecciones existentes directamente en una nueva expresión de colección, eliminando la necesidad de concatenación manual.

Antes de C# 12:

// Inicializando diferentes colecciones
int[] initialNumbers = new int[] { 1, 2, 3 };
List<int> moreNumbers = new List<int> { 4, 5 };

// Combinando colecciones
List<int> allNumbers = new List<int>();
allNumbers.AddRange(initialNumbers);
allNumbers.AddRange(moreNumbers);
allNumbers.Add(6);
allNumbers.Add(7);

Console.WriteLine(string.Join(", ", allNumbers));

Con C# 12:

// Inicializando diferentes colecciones
int[] initialNumbers = [1, 2, 3];
List<int> moreNumbers = [4, 5];

// Combinando colecciones usando el operador de dispersión
List<int> allNumbers = [..initialNumbers, ..moreNumbers, 6, 7];

Console.WriteLine(string.Join(", ", allNumbers));

Las expresiones de colección reducen la verbosidad de inicializar y combinar colecciones, ofreciendo una sintaxis más limpia e intuitiva. Esta eficiencia acelera la codificación y mejora la legibilidad, apoyando el principio de lograr un mayor impacto con menos líneas.

Parámetros Lambda Predeterminados

Las expresiones lambda, una piedra angular de la programación funcional en C#, históricamente carecían de la capacidad de definir valores predeterminados para sus parámetros. Si una lambda necesitaba manejar argumentos opcionales o proporcionar valores de respaldo, los desarrolladores tenían que recurrir a lógica condicional dentro del cuerpo de la lambda o definir múltiples sobrecargas, aunque las lambdas no admiten directamente sobrecargas.

C# 12 cierra esta brecha al permitir valores predeterminados para parámetros en expresiones lambda. La sintaxis y el comportamiento reflejan los de los parámetros de métodos o funciones locales, proporcionando una forma más fluida y concisa de definir funciones lambda flexibles.

Antes de C# 12:

// Lambda sin parámetros predeterminados.
// Si se deseaba un valor predeterminado para 'y', a menudo se necesitaba un envoltorio o lógica condicional:
Func<int, int, int> addOld = (x, y) => x + y;

Func<int, int> addWithDefaultOld = x => addOld(x, 10); // Una solución común

Console.WriteLine(addOld(5, 3));
Console.WriteLine(addWithDefaultOld(5));

Con C# 12:

// Lambda con parámetros predeterminados
Func<int, int, int> addNew = (x, y = 10) => x + y;

Console.WriteLine(addNew(5, 3)); // y es 3
Console.WriteLine(addNew(5));    // y toma el valor predeterminado 10

La introducción de parámetros predeterminados para lambdas mejora significativamente su flexibilidad y expresividad. Reduce la necesidad de definiciones de lambda redundantes o lógica condicional interna.

Conclusión

C# 11 y 12 ofrecen un conjunto convincente de características que cumplen con la promesa de “Escribe Menos, Haz Más”. Desde los literales de cadena sin procesar y los patrones de lista de C# 11 hasta los constructores primarios y las expresiones de colección de C# 12, estos avances abordan frustraciones reales en la codificación diaria. Eliminan sintaxis innecesaria, elevan la legibilidad y refuerzan patrones más seguros, mejorando directamente los flujos de trabajo en el desarrollo de software y proyectos de conversión de código. Cada innovación, ya sea imponiendo miembros requeridos o simplificando configuraciones de colecciones, reduce pulsaciones de teclas mientras maximiza la claridad y minimiza los riesgos de error.

Artículos relacionados