Table of Contents
Desde sus inicios en 2000, C# ha evolucionado considerablemente. Actualmente, es uno de los lenguajes favoritos de muchos desarrolladores de software. Durante más de veinte años, se han implementado numerosas funcionalidades para facilitar la codificación de forma más sencilla, comprensible y rápida. Cada vez que se publica una nueva versión, el equipo .NET impulsa C# para mejorarlo sin dificultar su uso.
C# 12 tuvo un fuerte lanzamiento con mejoras como structs de registro de sólo lectura, implementaciones de interfaz por defecto, miembros obligatorios y una aplicación más eficaz de las cadenas interpoladas. Pero C# 13 supone un avance considerable en el desarrollo. En lugar de limitarse a ampliar el lenguaje, esta versión pretende reforzar las prácticas de desarrollo modernas y facilitar la aplicación de arquitecturas limpias, patrones de diseño como DDD (Domain-Driven Design) y métodos de optimización del rendimiento como el uso de Span.
Uno de los principales objetivos de C# 13 es hacer que el lenguaje de programación sea más rico en expresiones, fiable y con mejor rendimiento, manteniendo al mismo tiempo la comprensibilidad. Para lograrlo, el lenguaje incorpora funcionalidades como constructores principales dentro de las clases, params Span para colecciones de alto rendimiento, mejores expresiones lambda, expresiones de colección y una importante ampliación en la forma de utilizar la concordancia de patrones. Estas funcionalidades no sólo son sintácticamente agradables, sino que además facilitan soluciones menos complejas y más manejables en aplicaciones del mundo real.
El propósito de este artículo es investigar las nuevas funciones de C# 13
Constructores primarios de clases
C# 13 proporciona Constructores Primarios a las clases (que han estado disponibles para structs en versiones anteriores). Esta implementación representa un avance significativo en la forma de organizar y comenzar las clases. Usando esta opción, eres capaz de definir el constructor directamente dentro de la definición de la clase. Esto reduce el código que se repite y simplifica el proceso, principalmente para tipos inmutables o clases que sólo requieren una configuración básica.
public class Product(string name, decimal price)
{
public string Name { get; } = name;
public decimal Price { get; } = price;
}
En este caso, nombre y precio constituyen parámetros primarios del constructor que puede utilizar como variables en toda la clase.
¿En qué se diferencian de las típicas empresas de construcción?
Antes de C# 13, tenías que codificar algo como esto:
public class Product
{
public string Name { get; }
public decimal Price { get; }
public Product(string name, decimal price)
{
Name = name;
Price = price;
}
}
Los constructores primarios reducen la repetición y contribuyen a que el contrato de creación de objetos sea más comprensible.
Caso aplicado en un proyecto real
Estas desarrollando una aplicación de comercio electrónico. Hay una entidad OrderLine que representa una partida de un pedido.
Con constructores primarios:
public class OrderLine(int quantity, Product product)
{
public int Quantity { get; } = quantity;
public Product Product { get; } = product;
public decimal Subtotal => Quantity * Product.Price;
}
Ventajas:
- Adquiera inmediatamente un conocimiento exhaustivo de los datos necesarios para un artículo.
- Los atributos inmutables son fáciles de establecer.
- Apropiado para modelos en DDD y Arquitectura Limpia.
Compatibilidad con otros elementos lingüísticos
✔ required & init:
A pesar de que los constructores primarios simplifican mucho el código, también siguen funcionando con campos requeridos e init.
✔ Atributos:
Es posible incorporar atributos a los parámetros del constructor principal si se pretende utilizarlos para serializar o validar datos.
public class Customer([Required] string name)
{
public string Name { get; } = name;
}
✔ Inyección de dependencia (DI):
Los constructores primarios son adecuados para clases instanciadas por contenedores IoC, siempre que los parámetros se ajusten a los servicios registrados.
Params Spans para un mejor rendimiento
¿Qué es params Span<T> y por qué es relevante?
C# 13 permite emplear params Span<T> y params ReadOnlySpan<T>. Se trata de una mejora sustancial en términos de velocidad de ejecución y de aprovechamiento de la memoria. Usando esta funcionalidad, los métodos que toman un número indefinido de argumentos pueden evitar generar un nuevo array (T[]) en el heap, lo que antes era necesario con params T[].
En lugar de solicitar memoria cada vez que se invoca un método parametrizado, ahora es posible interactuar directamente con la pila o los datos existentes en memoria. Esto alivia considerablemente la presión sobre el recolector de basura.
Sintaxis comparativa
Sintaxis comparativa antes de C# 13 (incluida la asignación de heap):
public void LogMessages(params string[] messages)
{
foreach (var message in messages)
Console.WriteLine(message);
}
En C# 13 (sin asignaciones innecesarias):
public void LogMessages(params ReadOnlySpan<string> messages)
{
foreach (var message in messages)
Console.WriteLine(message);
}
Ejemplo aplicado: Seguimiento de varios errores
public static class Logger
{
public static void LogErrors(params ReadOnlySpan<string> errors)
{
foreach (var error in errors)
Console.WriteLine($"[ERROR] {error}");
}
}
Usándolo desde otro lugar:
Logger.LogErrors(["Error en login", "Falta conexión", "Usuario no autorizado"]);
Este ejemplo es más eficiente ya que evita crear un nuevo array cada vez que se llama a LogErrors.
Mejoras en Lambda
¿Cuáles son las novedades de las expresiones lambda en C# 13?
Las lambdas han representado un componente crucial de C# durante muchos años, y C# 13 aporta mejoras significativas que les permiten ser más expresivas, claras y componibles. Estas modificaciones permiten a los desarrolladores crear código funcional más intuitivo y comprensible, lo que resulta beneficioso en LINQ, la programación reactiva y la lógica declarativa.
Principales noticias
1. Inferencia de tipos en parámetros múltiples.
Ya no es necesario que los desarrolladores especifiquen el tipo de cada parámetro en las lambdas que aceptan varios argumentos si el compilador puede deducirlo.
// Before
Func<int, int, int> sum = (int x, int y) => x + y;
// Now in C# 13
Func<int, int, int> sum = (x, y) => x + y;
2. Lambdas con atributos.
Ahora es posible aplicar atributos directamente a los parámetros lambda. Este método es útil para validaciones, serialización o anotaciones personalizadas.
var handler = ([MyCustom] string name) => Console.WriteLine(name);
3. Lambdas estáticas con mejoras.
En C# 13, las lambdas estáticas ahora se pueden inferir en más circunstancias. Este enfoque ayuda a reducir los errores y acelera el proceso más rápidamente, ya que no es necesario capturar el contexto.
Span<int> numbers = stackalloc[] { 1, 2, 3 };
var doubled = numbers.ToArray().Select(static n => n * 2);
4. Compatibilidad de grupos de métodos y delegados
Las lambdas mejoradas son ahora más sencillas de utilizar en situaciones en las que antes sólo se aceptaban delegados explícitos.
Un ejemplo práctico: Filtrado con lógica compleja
Considera que existe un conjunto de pedidos y necesitas localizar aquellos artículos que satisfacen varios requisitos:
var filtered = orders.Where((order) =>
order.Status == OrderStatus.Paid &&
order.Items.Any(item => item.Quantity > 10)
);
Con C# 13, puedes mejorar el rendimiento implementando una lambda estática en los casos en que no se capturen variables externas.
var filtered = orders.Where(static order =>
order.Status == OrderStatus.Paid &&
order.Items.Any(item => item.Quantity > 10)
);
Esta instrucción informa al compilador para que excluya el entorno circundante de la lambda, lo que permite una ejecución más rápida.
Lambdas y LINQ: más potentes que nunca
Con lambdas más limpias, inferencia de tipos y atributos, LINQ se vuelve considerablemente más expresivo.
var results = data
.Where((x, i) => i % 2 == 0)
.Select((x, i) => $"Item #{i}: {x.Name.ToUpper()}");
Los desarrolladores también pueden crear fácilmente árboles de expresiones más complejos para consultas dinámicas.
Collection Expressions
¿Qué son las Collection Expressions en C# 13?
Los Collection Expressions representan una característica que permite inicializar colecciones de forma más compacta, expresiva y fiable. Disponibles con C# 13, estas funciones unifican y simplifican la creación y combinación de colecciones mediante una sintaxis similar a la de las matrices, aplicable a casi cualquier tipo de colección compatible.
Se trata de una mejora clave en el lenguaje, cuyo objetivo es eliminar la necesidad de llamar directamente a constructores o métodos como .Add(), y ofrece una sintaxis más limpia y declarativa.
Ejemplo básico de sintaxis
int[] numbers = [1, 2, 3, 4]; // instead of: new int[] { 1, 2, 3, 4 }
Este patrón también se aplica a:
List<string> names = ["Alice", "Bob", "Charlie"];
El compilador infiere el tipo correcto como resultado del operador [], lo que hace que el código sea más sencillo y comprensible.
Funcionamiento interno
Haciendo uso de una Collection Expression como [a, b, c], el compilador traduce esto a una llamada al constructor para el tipo solicitado y hace una inicialización similar a un inicializador de colección.
En C# 13, también es posible utilizar estos constructores de colección en composiciones complejas como:
var allValues = [..primaryValues, ..fallbackValues, 99];
Aquí:
- ..primaryValues desempaqueta una colección.
- 99 añade un elemento literal
- Este enfoque crea una nueva colección sin necesidad de escribir lógica imperativa.
Ejemplo práctico: Combinar colecciones
IEnumerable<string> defaultTags = ["core", "base"];
IEnumerable<string> customTags = ["api", "v1"];
List<string> finalTags = [..defaultTags, ..customTags, "latest"];
Esta acción evita la necesidad de hacer:
var finalTags = new List<string>();
finalTags.AddRange(defaultTags);
finalTags.AddRange(customTags);
finalTags.Add("latest");
Span<t> and Array<t> Compatibility
Los desarrolladores también pueden utilizar expresiones de colección con estructuras de bajo nivel.
Span<byte> header = [0x01, 0xFF, 0x00];
Y combinarlos con datos durante el funcionamiento:
var dynamicData = new byte[] { 0xA1, 0xB2 };
var fullPayload = [0x00, ..dynamicData, 0xFF];
Aplicación con tipos personalizados.
Si defines una clase o estructura que implemente ICollection
Ejemplo con un registro:
public record MyContainer(params string[] Values);
var container = new MyContainer(["one", "two", "three"]);
Interceptores (experimental)
¿Qué son los interceptores en C# 13?
Los interceptores son una característica experimental de C# 13 que te permiten interceptar y reemplazar métodos realizados por el compilador. Esto incluye métodos en llamadas a constructores, métodos de autoinicialización, registros y patrones de deconstrucción.
La razón principal de esto es dar a los desarrolladores una forma de añadir su propia lógica en lugar del comportamiento por defecto del compilador, sin escribir el mismo código una y otra vez o utilizar técnicas de tejido IL (como Fody).
¿Qué importancia tiene?
Normalmente, los desarrolladores no podían controlar lo que hacía el compilador cuando creaba métodos automáticamente. Los interceptores abren la puerta a:
* Adaptar los constructores de registros.
* Cambiar los métodos equality y hashCode.
* Añadir validaciones o registros sin cambiar la lógica de negocio.
* Afina el código autogenerado.
Es como un compilador nativo AOP (Aspect-Oriented Programming).
Basic Syntax
En primer lugar, marca el interceptor con un atributo especial:
[Experimental("Interceptors")]
public static class MyInterceptors
{
[Interceptor]
public static Person InterceptConstructor(string name, int age)
{
Console.WriteLine($"Intercepted constructor: {name}, {age}");
return new Person(name.ToUpper(), age); // custom logic
}
}
Entonces interceptalo así:
var person = new Person("alice", 30); // Call the interceptor if it’s registered.
Ejemplo práctico: Validación en el Constructor
Imagina querer comprobar los datos al crear un objeto Usuario sin repetir la lógica o hacer fábricas manuales.
[Experimental("Interceptors")]
public static class UserInterceptors
{
[Interceptor]
public static User InterceptUser(string username, string email)
{
if (!email.Contains("@"))
throw new ArgumentException("Invalid email");
return new User(username, email.ToLower());
}
}
Ahora, cada vez que se llama a un nuevo User(…), el interceptor se activa.
¿Dónde pueden utilizarse los interceptores?
Los interceptores son para:
– Constructores
– Métodos autoimplementados
– Métodos de deconstrucción
– Métodos de grabación como Deconstruct o With
Esto significa que puedes tener un control total sobre los objetos creados en tiempo de compilación, sin necesidad de generar manualmente boilerplate.
Constructores primarios de clases
¿Qué son los constructores primarios?
Los constructores primarios son una característica de C# 13 que amplía una idea de los registros de C# 9: definir los parámetros del constructor directamente en la declaración de la clase.
Esto simplifica las clases al reducir la necesidad de campos, propiedades y constructores adicionales. Lo que antes ocupaba muchas líneas ahora puede definirse en una sola línea declarativa.
Ejemplo básico
Antes de C# 13, crear una clase con un constructor y propiedades se parecía a esto:
public class Person
{
public string Name { get; }
public int Age { get; }
public Person(string name, int age)
{
Name = name;
Age = age;
}
}
Con C# 13:
public class Person(string name, int age)
{
public string Name => name;
public int Age => age;
}
¡Mucho más limpio!
Principales diferencias con los registros
Aspect | Clase en el constructor principal | Constructor de registros |
Tipo | clase, sin registro | Sólo en el registro |
Inmutabilidad | Opcional | Implicit |
Deconstruyendo | Automático No | Si |
Semántica del valor | No | Sí (por defecto en los registros) |
Ejemplo práctico: Clase de configuración
public class DbConfig(string connectionString, int timeout)
{
public string ConnectionString => connectionString;
public int Timeout => timeout;
}
Puedes utilizar esta clase de la siguiente manera:
var config = new DbConfig("Server=myDb;", 30);
Console.WriteLine(config.ConnectionString); // Output: Server=myDb;
Cómo llegar a los parámetros
Los parámetros establecidos en el constructor principal se comportan como variables dentro de la clase. Puedes:
- Muéstralas como propiedades.
- Utilízalos en los métodos.
- Pasarlos a clases base o interfaces.
Ejemplo de clase básica:
public abstract class Entity(Guid id)
{
public Guid Id => id;
}
public class Customer(Guid id, string name) : Entity(id)
{
public string Name => name;
}
Usos dentro del cuerpo de la clase
public class LogEntry(DateTime timestamp, string message)
{
public string FullLog => $"[{timestamp:u}] {message}";
public void Print()
{
Console.WriteLine(FullLog);
}
}
Parámetros lambda por defecto
¿Qué son los parámetros lambda por defecto?
En C# 13, ahora puedes asignar valores por defecto a los parámetros de una expresión lambda. Esto hace que las lambdas actúen como métodos normales, algo que ya era posible desde hace tiempo.
Este cambio hace que las lambdas sean más versátiles y reutilizables, y significa que no tienes que escribir varias versiones de la misma función anónima.
Basic Syntax
Antes de C# 13:
Func<int, int> square = x => x * x;
Pero antes no podías:
Func<int, int> square = (int x = 5) => x * x; // Error in older versions
Ahora, en C# 13:
Func<int, int> square = (int x = 5) => x * x; // Valid
Puede llamarlo sin argumentos si utilizas una invocación dinámica o una sobrecarga:
var result = square(); // result = 25
Nota: Esto funciona cuando invocas la lambda dinámicamente, o cuando defines una función que la encapsula.
Ejemplo práctico: Generador de mensajes
Func<string, string> greet = (string name = "Guest") => $"Hello, {name}!";
Console.WriteLine(greet()); // Output: Hello, Guest!
Console.WriteLine(greet("Alice")); // Output: Hello, Alice!
Lambdas con valores por defecto
Puedes utilizar lambdas con parámetros que tengan valores por defecto dentro de otras funciones.
Action<string, int> log = (message = "No message", level = 1) =>
{
Console.WriteLine($"[Level {level}] {message}");
};
log(); // Output: [Level 1] No message
log("Fatal error", 5); // Output: [Level 5] Fatal error
log("Minor warning"); // Output: [Level 1] Minor warning
Cómo lo gestiona el compilador:
El compilador trata los parámetros con valores por defecto de la misma forma que lo haría en un método tradicional. Si no se da un argumento, utiliza el valor que se estableció como predeterminado.
Ten en cuenta: Sólo puedes usar esto cuando llames a la lambda directamente, o cuando pongas su llamada dentro de una función que mantenga los valores por defecto.
Uso de alias en los patrones
¿Qué son los alias en los patrones?
En C# 13, ahora puede asignar un nombre a una expresión dentro de un patrón de tipo is o switch. Esto le permite referirse al resultado del patrón sin tener que reevaluarlo o escribir lógica adicional.
Esto hace que los patrones complejos sean más claros, rápidos y fáciles de leer.
Ejemplo básico
Antes de C# 13:
if (obj is string s && s.Length > 5)
{
Console.WriteLine(s.ToUpper());
}
Con alias de patrones (C# 13):
if (obj is string s and { Length: > 5 } as longString)
{
Console.WriteLine(longString.ToUpper());
}
Aquí, longString funciona como un alias con nombre que representa el valor comprobado por el patrón.
¿Qué problema soluciona?
En patrones complicados con propiedades anidadas o muchas comprobaciones, era habitual:
- Repite el acceso a la misma propiedad.
- Utiliza variables adicionales.
- Volver a acceder a un objeto después de un patrón.
Ahora puedes tomar una referencia con “as” directamente dentro de sentencias switch o if.
Ejemplo práctico: Análisis de formas
object shape = new Rectangle(Width: 100, Height: 200);
if (shape is Rectangle { Width: > 50, Height: > 100 } as largeRectangle)
{
Console.WriteLine($"Large shape: {largeRectangle.Width}x{largeRectangle.Height}");
}
Esto resulta muy útil cuando se necesita volver a acceder al objeto validado, ya que evita tener que escribir varias condiciones o castings.
Ejemplo de interruptor:
string Process(object input) => input switch
{
string { Length: > 10 } as longString => $"Long string: {longString}",
int n => $"Integer: {n}",
_ => "Unknown"
};
Mejoras en params y Collection Expressions
¿Qué mejoras aporta C# 13 a los parámetros y colecciones?
Las funciones de C# 13 introducen mejoras clave en el uso de parámetros con expresiones de colecciones. Ahora puedes pasar colecciones de forma más flexible y expresiva.
Esto hace que el lenguaje sea más funcional y expresivo a la hora de construir listas, matrices y colecciones complejas con menos sintaxis y más claridad.
Recordatorio: params en C#
public void PrintMessages(params string[] messages)
{
foreach (var msg in messages)
{
Console.WriteLine(msg);
}
}
Call
PrintMessages("Hello", "World");
Novedades de C# 13
Ahora puede pasar expresiones de colección directamente como argumento de un parámetro params.
Expresiones de colección ([…]) como argumentos params
PrintMessages(["Hello", "world", "from C# 13"]);
This: PrintMessages(["A", "B", "C"]);
Is equal to: PrintMessages(new string[] { "A", "B", "C" });
Es mucho más limpio, especialmente para literales o resultados dinámicos.
Puedes utilizar params con otros valores y mezclar valores individuales con colecciones.
PrintMessages("First", ..["Second", "Third"], "Last");
Esto utiliza el operador de expansión (..), como en JavaScript o Python, para expandir la colección dentro de los params.
Este comportamiento forma parte del nuevo sistema Collection Expressions introducido en C# 12 y ampliado en C# 13.
Ejemplo práctico con parámetros y colecciones.
void LogErrors(params string[] errors)
{
Console.WriteLine("Errors:");
foreach (var e in errors) Console.WriteLine($"- {e}");
}
Puedes llamarlo así:
LogErrors(["Invalid token", "Timeout", "Unauthorized"]);
Collection Expressions en otros contextos
Además de params, puedes utilizar [ … ] para:
Crear listas con literales: var nums = [1, 2, 3, 4];
Combinar colecciones: var combined = [..primeraLista, 42, ..segundaLista];
Funciones que devuelven matrices: int[] GetDefaults() => [0, 1, 2];
Requisitos y soporte
Función | Requisitos |
[ … ] syntax | C# 12 o superior |
Spread .. in params | C# 13 |
Collection expressions | System.Span |
Interpolated String Improvements
¿Qué son los Interpolated Strings?
Las cadenas interpoladas ($…) permiten incrustar variables y expresiones directamente en cadenas de texto, lo que resulta importante para registros, mensajes, consultas SQL, etc.
C# 13 mejora aún más esta función haciendo que las interpolaciones sean más eficaces, seguras y personalizables, especialmente cuando se utilizan FormattableString, InterpolatedStringHandler y API personalizadas.
Nuevas funciones en C# 13
Smarter Interpolated String Handlers
Esto mejora el rendimiento en el registro, el formato condicional y el uso de memoria.
C# 13 sigue mejorando el uso de InterpolatedStringHandler, permitiendo capturar interpolaciones sin construir la cadena completa.
logger.LogDebug($"Request took {elapsedMilliseconds} ms"); // Efficient string creation
Durante la compilación, esta instrucción se puede ajustar para omitir la creación de cadenas si el registro de depuración no está activado.
¿Qué importancia tiene?
En versiones anteriores, las interpolaciones hacían una cadena final tanto si se utilizaba como si no. Con los manipuladores, la parte de la cadena se construye sólo cuando es necesario, que:
- Reduce la asignación de memoria.
- Mejora el resultado del registro.
- Permite una personalización avanzada (como traducción o censura).
Ejemplo con InterpolatedStringHandler
public void LogInfo([InterpolatedStringHandlerArgument("")] ref CustomLoggerHandler handler)
{
Console.WriteLine(handler.GetFormattedText());
}
Este patrón se utiliza en sistemas de registro como Serilog y Microsoft.Extensions.Logging.
Personalización avanzada.
Puedes crear tu propio InterpolatedStringHandler para supervisar y gestionar cómo se construyen las cadenas.
[InterpolatedStringHandler]
public ref struct MyHandler
{
private StringBuilder _sb;
public MyHandler(int literalLength, int formattedCount)
{
_sb = new();
}
public void AppendLiteral(string s) => _sb.Append(s);
public void AppendFormatted<T>(T value) => _sb.Append($"**{value}**");
public string GetResult() => _sb.ToString();
}
Usa:
var handler = new MyHandler(0, 0);
handler.AppendFormatted("Hello");
Console.WriteLine(handler.GetResult()); // Output: **Hello**
Interpolated Strings with Span<char> & stackalloc
Estas mejoras permiten mezclar cadenas interpoladas con Span
Span<char> buffer = stackalloc char[100];
var writer = new SpanWriter(buffer);
writer.Write($"Data: {value}, Time: {timestamp}");
Conclusión: Funciones de C #13
C# 13 no se limita a añadir más funciones al lenguaje. Se trata de seguir mejorando C# para convertirlo en un entorno de desarrollo más expresivo, seguro, eficiente y fácil de usar para los equipos que crean aplicaciones modernas.
A lo largo de este artículo, hemos analizado 9 funciones de C# 13 y lo que significan para ti como desarrollador o diseñador de software.
- C# 13 mejora la productividad al permitirte hacer más con menos código, lo que resulta útil para la claridad y el mantenimiento, principalmente en entornos de gran tamaño.
- Estas mejoras ofrecen claros beneficios en proyectos con Diseño Orientado al Dominio (DDD), Arquitectura Limpia o Arquitectura Hexagonal.
- Muchas de las actualizaciones pretenden perfeccionar la ejecución en tiempo real y la experiencia de los desarrolladores.
- Está en consonancia con la evolución del entorno .NET.
Un lenguaje de programación no es sólo una herramienta; es una forma de pensar».
Estas funciones de C# 13 introducen nuevas perspectivas en la programación. No interrumpe lo que ya entiende, y no hay necesidad de reescribirlo todo. Puedes beneficiarte de estos cambios sin microservicios, grandes frameworks ni transformaciones de proyectos.
Estas mejoras están aquí para ayudarle a escribir un código más limpio, fácil de mantener y eficiente, tanto si está creando API REST sencillas como aplicaciones empresariales complejas o sistemas distribuidos.
Author
-
I am a Computer Engineer by training, with more than 20 years of experience working in the IT sector, specifically in the entire life cycle of a software, acquired in national and multinational companies, from different sectors.
View all posts