En este post vamos a ver las novedades que nos trae la última versión de C#, C#10, Recuerda que para utilizar esta versión, necesitas instalarte .NET 6.
La cual será lanzada oficialmente en un par de semanas, pero ya tenemos la “release candidate” disponible.
Índice
1 - Global usings en c#
En C#9 vimos cómo se introducen los programas de alto nivel en el que se reduce muchísimo la cantidad de código necesaria para ejecutar una aplicación de consola.
El objetivo de los global usings es básicamente el mismo, reducir la cantidad de código que necesitamos. Cabe destacar que esta funcionalidad ya la tenemos disponible (mas o menos) en Blazor desde .NET 5 (c# 9).
Y consiste en tener nuestros usings
(osea los namespaces
que vamos a utilizar) en un sitio común.
Para ello tenemos dos opciones:
1.1 - Fichero global usings
Podemos crear un fichero en nuestro proyecto para incluir esos global usings
, simplemente indicándonos, y así podemos utilizarlos desde cualquier parte de ese proyecto.
global using System;
global using System.Collections.Generic;
global using System.Linq;
1.2 - Actualizar el fichero csproj del proyecto
Opcionalmente, podemos incluirlos en el .csproj
de la aplicación, si activamos la anotación <ImplicitUsings>enable</ImplicitUsings>
y los incluimos en un ItemGroup
.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<Using Include="System.Threading" />
</ItemGroup>
</Project>
Personalmente no tengo claro qué opción prefiero, lo que sí que tengo claro es que no quiero una combinación de ambas y posiblemente, para evitar que los gobal usings
se especifiquen por todo el código, sería buena idea centralizarlo en el .csproj
.
2 - Declaración del namespace por fichero
A estas alturas todos sabemos que es el namespace
o el espacio de nombre, es básicamente el espacio de nombre donde se ubican lógicamente nuestras clases, métodos, etc.
Hasta ahora (c# 9 y anteriores) lo representaremos así:
namespace csharp10
{
class NamespacePorFichero
{
}
}
Pero con la llegada de C# 10, y con el mismo objetivo, reducir el código, podemos indicar el namespace en una sola línea, sin necesidad de poner los corchetes, lo que nos ahorra, tanto código como una tabulación.
namespace csharp10;
class NamespacePorFichero
{
}
3 - Minimal apis
Uno de los grandes cambios en esta versión de C# son las minimal API, de las que realicé un post en detalle sobre ellas.
Para resumir podemos decir que Microsoft nos provee de la posibilidad de hacer APIs con muy pocas líneas de código, lo cual está bien.
using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Hosting;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hola NetMentor!");
app.Run();
4 - DateOnly y TimeOnly
En C#, bueno y en cualquier otro lenguaje de programación siempre hay problemas con la fecha y la hora, que a cuando corresponde una fecha, etc.
Hace tiempo hice un pequeño short en youtube explicando el porqué es importante utilizar UTC
cuando almacenamos horas, pero, existe la posibilidad de que únicamente necesitemos ya bien sea o la fecha o la hora.
Para ello disponemos de los tipos DateOnly
y de TimeOnly
.
4.1 - DateOnly en C#
Utilizar y comprender DateOnly
es muy sencillo, únicamente almacena la fecha, y puede ser bastante útil, por ejemplo para indicar que días alguien está de vacaciones en un software de recursos humanos. Todos hemos tenido escenarios en el trabajo donde hemos almacenado el tiempo como 00:00:00.
Para ello ahora únicamente debemos utilizar DateOnly y es muy sencillo, de hecho, podemos utilizar DateTime.Now como referencia:
DateOnly date = DateOnly.FromDateTime(DateTime.Now);
4.2 - TimeOnly en C#
Similar al caso anterior, podemos partir del objeto DateTime
para obtener el tiempo, podemos pasar un string
y parsear el tiempo, o la opción que probablemente es la más sensata, hacerlo desde un TimeSpan
.
TimeOnly time = TimeOnly.FromTimeSpan(TimeSpan.FromHours(13));
5 - Mejoras en las lambas
Esta funcionalidad viene ligada a los delegados así como a las expresiones lambda. Sabemos que podemos indicar un delegado en una sola línea, pero, si indicamos var, en vez del tipo de delegado que es, yo personalmente prefiero especificar el tipo, pero también entiendo que muchos desarrolladores prefieren utilizar var
.
Func<string> lambdaExpression = () => "hola Netmentor.";
var ejemplo = () => "hola Netmentor."; //En C#9 no deja compilar.
- Nota: además c# 10 nos permite especificar el tipo de retorno de dicha expresión, que en otras versiones no nos dejaba
Func<string?> conRetorno = string? () => null;
6 - Colección PriorityQueue
Un nuevo tipo de colección, el cual es una cola que nos permite indicar la prioridad de los ítems que incluimos en ella.
- Puedes más detalles en este post.
PriorityQueue<string, int> colaPrioridad = new PriorityQueue<string, int>();
colaPrioridad.Enqueue("Opel", 2);
colaPrioridad.Enqueue("Audi", 1);
colaPrioridad.Enqueue("BMW", 3);
7 - Mejoras en LINQ
7.1 - Max/Maxby y Min/MinBy
Después de mucho tiempo, nos llega a LINQ la funcionalidad de coger el máximo o el mínimo valor de una lista, basándonos en un atributo en caso de ser un objeto.
Lo que antiguamente hacíamos con .OrderBy(x=>x.Prioridad).First();
ahora lo podemos hacer con MaxBy()
/MinBy()
.
La diferencia entre utilizar .Max()
/.Min()
a .Min()
/.MinBy()
radica en el resultado, cuando utilziamos .Max()
, nos devuelve el máximo del elemento comparador, mientras que si hacemos .MaxBy()
nos devuelve el objeto entero:
List<MaxMinEjemplo> ejemploLista = new()
{
new(3, "prioridad 3"),
new(2, "prioridad 2"),
new(10, "prioridad 10"),
new(8, "prioridad 8")
};
int max = ejemploLista.Max(x=>x.Prioridad); //Devuleve 10 (como entero)
MaxMinEjemplo? maxBy = ejemploLista.MaxBy(x => x.Prioridad);//Devuelve el objeto con prioridad 10
7.2 - IEnumerable Chunk
Finalmente llega una opción que nos permite coger una lista (o bueno, tipo que implemente IEnumerable) y hacer chunks
de la misma, que quiere decir, que si tenemos una lista de 1000 items, podemos cogerlos por bloques de 10, 100, 500 o los que especifiquemos.
Personalmente es una funcionalidad que me alegra verla, ya que yo la he tenido que implementar manualmente en el pasado, en más de una empresa.
Su funcionamiento es bastante sencillo, tenemos una lista y le indicamos el método .chunk()
al que le pasamos el tamaño de elementos que queremos por bloque.
IEnumerable<int> listInts = Enumerable.Range(0, 1000);//lista de 1000 elementos
int chunkSize = 50;
foreach (var chunk in listInts.Chunk(chunkSize)) //devuelve una lista con el tamano indicado.
{
//Llamadas paralelas
Parallel.ForEach(chunk, (item) =>
{
Console.WriteLine($"Simulamos get con id {item}");
});
}
7.3 - Mejora en el método Take
Cuando queremos recuperar parte de una lista e imprimirla, siempre hemos tenido que indicar que se salte cierto número de elementos y luego recibir los elementos que queremos.
En C# 10 podemos especificar un rango directamente:
List<int> listInts = Enumerable.Range(0, 1000).ToList();//lista de 1000 elementos
var resultCsharp9 = listInts.Skip(10).Take(15);
var resultCsharp10 = listInts.Take(10..25);
8 - Property patterns
En las últimas versiones de C#, el equipo de microsoft nos ha proporcionado lo que se denomina como “pattern matching”, y funcionalidades para el mismo. en este caso la mejora consiste en que podemos acceder a las propiedades del elemento hijo a través del punto, mientras que en C#9 era bastante más largo y complejo, sobre todo si el elemento a acceder está ubicado varios subniveles dentro del objeto principal.
public record Subtipo(int Edad);
public record Persona(string Nombre, Subtipo subtipo);
public void Ejemplo()
{
Persona persona = new Persona("Ivan", new(29));
string resultado = ExpressionCsharp10(persona);
}
private static string ExpressionCsharp10(Persona persona) =>
persona switch
{
{ subtipo.Edad: >= 18 } => "es mayor de edad",
{ subtipo.Edad: < 18 } => "es menor de edad",
_ => throw new NotSupportedException()
};
//Este ejemplo es en c#9, es muy verbose
private static string ExpressionCsharp9(Persona persona) =>
persona switch
{
{ subtipo: { Edad: >= 18 } } => "es mayor de edad",
{ subtipo: { Edad: < 18 } } => "es menor de edad",
_ => throw new NotSupportedException()
};
9 - Mejoras en los records
A lo largo de mis posts siempre recomiendo utilizar records cuando sea posible, ya que yo soy muy fan de la inmutabilidad, y la verdad me alegra ver las mejoras que desde el equipo de microsoft han traído al lenguaje para los records.
9.1 - Simplificación de la definición de los records en C# 10
La primera de ella, probablemente te hayas dado cuenta leyendo el post, ahora y ano debemos crearlos con las propiedades, únicamente en la definición del método podemos indicar el constructor, en una línea y el compilador por detrás entiende que son propiedades
public record Persona(string Nombre, int Edad);
public void Ejemplo()
{
Persona persona = new("Ivan", 28);
Console.WriteLine($"El nombre es: {persona.Nombre}");
}
Como ves, podemos acceder a las propiedades sin problemas.
9.2 - record structs en C# 10
Por defecto cuando definimos un record
se convierte en una clase por detrás al compilar, pero desde esta nueva versión de c# 10 podemos definir record structs
, simplemente indicandolo en la definición del mismo:
public record struct Notas(int Matematicas, int Ingles);
9.3 - ToString sellado en los records
Por defecto todos los objetos tienen un método llamado .ToString()
, el cual por ejemplo es utilizado cuando utilizamos string interpolation
, y en el mundo laboral no es extraño ver este método sobreescrito:
public record Persona(string Nombre, int Edad)
{
public override string ToString()
{
return $"El nombre es: {Nombre} y su edad es: {Edad}";
}
}
public void Ejemplo2()
{
Persona persona = new("Ivan", 28);
Console.WriteLine(persona);
}
Bueno pues ahora podemos marcar este método como sealed (sellado) para que si tenemos algún tipo que implemente nuestro record
no podamos sobre-escribir el método .ToString()
:
public record Persona(string Nombre, int Edad)
{
public sealed override string ToString()
{
return $"El nombre es: {Nombre} y su edad es: {Edad}";
}
}
public record Alumno : Persona
{
private Notas Notas;
public Alumno(string nombre, int edad, Notas notas) : base(nombre, edad)
{
Notas = notas;
}
//da un error y no deja compilar
public override string ToString()
{
return $"El nombre es: {Nombre} " +
$"y su nota en matematicas es {Notas.Matematicas}";
}
}
Como el tipo Persona
tiene el método .ToString()
sellado, el código no nos compila ya que el tipo alumno está intentando sobreescribir el método .ToString()
.
10 - Interpolación de strings en constantes
No hay mucho más que explicar, el nombre lo dice todo, desde C# 10 podemos hacer string interpolation
en las constantes, cosa que anteriormente no se podía, así que ahora podemos definir lo siguiente:
public const string Nombre = "Netmentor";
public const string Saludo = $"Hola {Nombre}!";
//así se hacia en c# 9
public static readonly string SaludoCsharp9 = $"Hola {Nombre}!";
Conclusión:
En este post hemos visto cuales son las mejoras de c# 10 con respecto a c# 9.