Saltar al contenido

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript... y lo que venga ;)

18 años online

el blog de José M. Aguilar

Inicio El autor Contactar

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web
ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript...

¡Microsoft MVP!
Mostrando entradas con la etiqueta c#. Mostrar todas las entradas
Mostrando entradas con la etiqueta c#. Mostrar todas las entradas
martes, 29 de octubre de 2024
Piezas de un puzzle sugiriendo que todo va encajando en .NET

Seguimos analizando las novedades de .NET 9, y en esta ocasión le toca el turno a la actualización en miembros de clases parciales de C# 13.

Las clases o tipos parciales de C# están con nosotros casi desde los inicios de .NET, desde los tiempos de C# 2.0. Como sabemos, éstas permiten dividir la implementación de un tipo en varios archivos. Su existencia se debía a varios motivos:

  • Permitir a los desarrolladores trabajar en clases de forma concurrente sin bloquearse (recordemos que por aquellos tiempos aún pululaban por ahí infames sistemas de control de código fuente como SourceSafe).
  • Trocear clases extensas para facilitar su comprensión y mantenimiento.
  • Y, el que es más importante, posibilitar el uso sencillo de herramientas de generación de código que, de alguna forma, tuvieran que completar la implementación de código del usuario.

Acompañando a las clases parciales, se introdujo también el soporte para métodos parciales. Aunque su uso era limitado y podía depararnos algunas sorpresas, eran útiles para comunicar de forma segura distintos fragmentos de la clase. Básicamente, consistía en que una porción de una clase podía definir un método e invocarlo desde su código, delegando a otras porciones su implementación. Si no se implementaba en ninguna otra parte, simplemente no se generaba el código de la invocación.

Veamos un ejemplo del uso de estas características:

// Archivo: Ejemplo.Main.cs
public partial class Ejemplo
{    
    // El método parcial se declara sin implementación...
    partial void Log(string msg);

    public void RealizarAlgo()
    {
        hacerAlgoComplejo();
        Log("¡Ojo!"); // Usamos el método
                    // parcial declarado antes
    }
}

// Archivo: Ejemplo.Log.cs
public partial class Ejemplo
{
    partial void Log(string msg)
    {
        Console.WriteLine(msg);
    }
}

Pues bien, casi dos décadas más tarde, los tipos parciales van a recibir algunas actualizaciones interesantes en C# 13, posibilitando que, además de métodos, puedan definirse propiedades parciales.

martes, 15 de octubre de 2024
.NET 9 apareciendo en el horizonte

Cada vez que llega una nueva versión del framework, todos estamos ansiosos por conocer las novedades, especialmente las más espectaculares. Sin embargo, a veces hay pequeñas mejoras que, sin ser revolucionarias, nos pueden resultar útiles en algunos escenarios.

El modificador params permite especificar que un método o función acepta un número variable de argumentos, como en el siguiente ejemplo, donde vemos una función Sum() que puede ser invocada con cualquier número de valores:

// Declaración de la función:
int Sum(params int[] values) 
{
    return values.Sum();
}

// Ejemplos de uso:
Console.WriteLine(Sum(1)); // Enviamos un único valor -> "1"
Console.WriteLine(Sum(1, 2, 3, 4, 5)); // Enviamos cinco valores -> "15"
Console.WriteLine(Sum()); // No enviamos elementos, se recibirá un array vacío -> "0"
Console.WriteLine(Sum([1, 2, 3])); // Aquí pasamos un array directamente -> "6"

Hasta ahora, el parámetro param sólo podía ser un array, lo que limitaba un poco su uso. Por ejemplo, no podríamos llamar a la función Sum() pasándole directamente un List<int> o un IEnumerable<int> sin convertirlos antes a array porque fallarían en tiempo de compilación, al no tratarse de un int[], que es el tipo del parámetro esperado.

¿Qué ha cambiado?

martes, 2 de julio de 2024
C#

Cuando en un bucle for necesitamos más de una variable de control, lo habitual es inicializarlas fuera del bucle y luego usarlas en su interior, por ejemplo, como en el siguiente código:

int j = 0;
for (int i = 0; i < 10 && j < 100; i++)
{
    Console.WriteLine($"i = {i}, j = {j}");
    j+= 10;
}
martes, 18 de junio de 2024
C#

Llevamos ya varias entregas de la serie C# bizarro, donde, como sabéis, ponemos a prueba nuestros conocimientos de C# llevando al extremo el uso de algunas de sus características, lo que de paso nos ayuda a conocer mejor aún el lenguaje.

Hoy vamos a proponer un nuevo reto, mostrando un comportamiento que quizás muchos conozcáis, pero seguro que otros nunca habéis tenido ocasión de verlo. A ver qué tal se os da ;)

Observad atentamente esta sencilla aplicación de consola:

int i = 1;
int accum = 0;
while (i <= 5)
{
    accum += i;
    Debug.WriteLine($"Adding {i++}, Accumulated = {accum}");
}
Console.WriteLine($"Total: {accum}");

A la vista del código, las preguntas son dos: ¿Qué resultado obtenemos al ejecutar la aplicación? ¿Será siempre así?

Obviamente, tiene algo de truco, pero si lo pensáis un poco seguro que podéis responder a las preguntas. Y si no, pulsad aquí para ver la solución; 👇👇

En principio, lo lógico es pensar que deberíamos ver en el canal de depuración (la ventana Output>Debug en Visual Studio)  el valor actual y el total acumulado en cada iteración, lo que nos permite ir siguiendo en detalle el funcionamiento interno:

Adding 1, Accumulated = 1
Adding 2, Accumulated = 3
Adding 3, Accumulated = 6
Adding 4, Accumulated = 10
Adding 5, Accumulated = 15

Finalmente, se mostrará por consola el valor definitivo:

Total: 15

Esto será justamente lo que obtengamos si copiamos el código en Visual Studio y lo ejecutamos pulsando F5 o el botón correspondiente del IDE. Hasta ahí, perfecto ;)

Sin embargo, este código tiene una trampa oculta. Si desde Visual Studio cambiamos el modo de compilación a "Release" y lo lanzamos, o bien si lanzamos directamente el ejecutable que encontraremos en la carpeta bin/Release/net8.0 (o la versión de .NET que uses, da igual), veremos que la aplicación no se detiene nunca (?!)

El motivo de este extraño comportamiento lo explicamos hace ya bastantes años por aquí, en el post métodos condicionales en C#.

Estos métodos, presentes desde las primeras versiones de .NET (pre-Core), se decoran con el atributo [Conditional] para hacer que éstos y todas las referencias a los mismos sean eliminadas del ensamblado resultante de la compilación si no existe una constante de compilación determinada.

De hecho, si acudimos al código fuente de la clase estática Debug, veremos que su método WriteLine() está definido de la siguiente manera:

public static partial class Debug
{
    ...
    [Conditional("DEBUG")]
    public static void WriteLine(string? message) 
        => s_provider.WriteLine(message);
}

Cuando compilamos en modo depuración, la constante DEBUG estará definida, por lo que este método podrá ser invocado con normalidad y todo funcionará bien. Sin embargo, si compilamos en Release o cualquier otra configuración que no incluya la constante, este método desaparecerá del ensamblado, junto con las referencias que lo utilicen.

Es decir, si usamos el modo Release, el código que hemos escrito antes quedará tras la compilación como el siguiente:

int i = 1;
int accum = 0;
while (i <= 5)
{
    accum += i;
    // La siguiente llamada será eliminada en compilación:
    // Debug.WriteLine($"Adding {i++}, Accumulated = {accum}");
}
Console.WriteLine($"Total: {accum}");

Fijaos que, al eliminar la llamada, con ella desaparecerá también la expresión de autoincremento del índice del bucle i++, por lo que nunca se alcanzará la condición de salida y quedará iterando de forma indefinida.

Bonito, ¿eh? ;)

Publicado en Variable not found.

martes, 28 de mayo de 2024
C#

Me da la impresión de que el operador with de C# no es demasiado conocido, y creo que vale la pena echarle un vistazo porque puede resultarnos útil en nuestro día a día.

Introducido con C# 9, allá por 2020, las expresiones with proporcionan una forma sencilla de crear nuevas instancias de objetos "copiando" las propiedades de otro objeto, pero con la posibilidad de modificar algunas de ellas sobre la marcha. Esto, aunque puede usarse con varios tipos de objeto, es especialmente interesante cuando estamos trabajando con componentes inmutables, como son los records, porque la única posibilidad que tenemos de alterarlos es creando copias con las modificaciones que necesitemos.

martes, 21 de mayo de 2024
C#

Revisando código ajeno, me he encontrado con un ejemplo de uso del operador null coalescing assignment de C# que me ha parecido muy elegante y quería compartirlo con vosotros.

Como recordaréis, este operador, introducido en C# 8, es una fórmula muy concisa para asignar un valor a una variable si ésta previamente es null, una mezcla entre el operador de asignación y el null coalescing operator que disfrutamos desde C# 2:

// C# "clásico":
var x = GetSomething();
...
if(x == null) {
    x = GetSomethingElse();
}

// Usando null coalescing operator (C# 2)
var x = GetSomething();
...
x = x ?? GetSomethingElse();

// Usando null coalescing assignment (C# 8)
var x = GetSomething();
...
x ??= GetSomethingElse();

Pero, además, al igual que otras asignaciones, este operador retorna el valor asignado, lo que nos permite encadenar asignaciones y realizar operaciones adicionales en la misma línea.

Por ejemplo, observad el siguiente código:

Console.WriteLine(y ??= "Hello, World!");

Aquí, básicamente lo que hacemos es asignar el valor "Hello, World!" a la variable si ésta contiene un nulo, y luego imprimirlo por consola. Y si no es nulo, simplemente se imprime su valor actual.

martes, 23 de abril de 2024
C#

Cuando tenemos una colección de datos para procesar, es relativamente habitual tener que hacerlo por lotes, es decir, dividir la colección en partes más pequeñas e irlas procesando por separado.

Para ello, normalmente nos vemos obligados a trocear los datos en lotes o chunks de un tamaño determinado, y luego procesar cada uno de ellos.

Por ejemplo, imaginad que tenemos un método que recibe una lista de objetos a procesar y, por temas de rendimiento o lo que sea, sólo puede hacerlo de tres en tres. Si la lista de elementos a procesar es más larga de la cuenta, nos veremos previamente obligados a trocearla en lotes de tres para ir procesando cada uno de ellos por separado.

Tradicionalmente, es algo que hemos hecho de forma manual con un sencillo bucle for que recorre la lista completa, y va acumulando los elementos en un nuevo lote hasta que alcanza el tamaño deseado, momento en el que lo procesa y lo vacía para empezar de nuevo.

Pero otra posibilidad bastante práctica, y probablemente más legible, sería pasar la lista de elementos previamente por otro proceso que retorne una lista de chunks, o pequeñas porciones de la lista original, para que luego simplemente tengamos que ir procesándolos secuencialmente. Es decir, algo así:

private char[] array = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'];
private int chunkSize = 3;

var chunks = GetChunks(array, chunkSize); // [['a', 'b', 'c'], ['d', 'e', 'f'], ['g', 'h', 'i'], ['j']
foreach (var chunk in chunks) { ProcessChunk(chunk); // 'chunk' sería un array de 3 elementos }

Y la pregunta es, ¿cómo podríamos implementar ese método GetChunks() en C#? Vamos a ver varias opciones.

miércoles, 3 de abril de 2024
C#

Hoy va un post rápido sobre una característica de C# 12 que, aunque pequeña, nos puede resultar interesante en determinadas situaciones y puede pasar fácilmente desapercibida entre todas las demás novedades de esta versión del lenguaje.

Hasta C# 11 (.NET 7), no teníamos forma de definir una expresión lambda con parámetros opcionales y valores por defecto, como podemos hacer en los métodos o funciones normales, lo que complicaba la implementación de algunos escenarios.

martes, 6 de febrero de 2024
C#

Hace unos días, veíamos por aquí los constructores primarios de C#, una característica recientemente introducida en el lenguaje que permite omitir el constructor si en éste lo único que hacemos es asignar los valores de sus parámetros a campos de la clase.

Veamos un ejemplo a modo de recordatorio rápido:

// Clase con constructor tradicional
public class Person
{
    private readonly string _name;
    public Person(string name)
    {
        _name = name;
    }
    public override string ToString() => _name;
}

// La misma clase usando el constructor primario:
public class Person(string name)
{
    public override string ToString() => name;
}

Como comentamos en el post, los parámetros del constructor primarios eran internamente convertidos en campos privados de la clase, generados automáticamente e inaccesibles desde el código.

martes, 30 de enero de 2024
C#

Hace ya algunos años, con la llegada de C# 9, los records nos mostraron otra forma de crear objetos distinta a los clásicos constructores. Ya entonces, y únicamente en aquél ámbito, se comenzó a hablar de primary constructors, pero no ha sido hasta C# 12 cuando se ha implementado de forma completa esta característica en el lenguaje.

En este post vamos a ver de qué se trata, cómo podemos utilizarlos y peculiaridades que hay que tener en cuenta.

martes, 19 de diciembre de 2023
C#

Poco a poco vamos haciéndonos con las novedades de C# 12, y en esta ocasión nos centraremos en una nueva sintaxis que proporciona una forma concisa y rápida para declarar los elementos de una colección.

Ya os adelanto que si sois de los que siempre han envidiado otros lenguajes por la facilidad con la que se declaran los elementos de un colección o array, estáis de enhorabuena ;) Porque, sin duda, hay formas de hacerlo menos verbosas que las que hemos tenido hasta ahora en C#:

// JavaScript:
let numbers = [1, 2, 3];

// Python:
numbers = [1, 2, 3]

// PHP:
$array = [1, 2, 3];

// Rust:
let array = [1, 2, 3];

En C# 11 y anteriores, la creación de un array es normalmente más farragosa, porque de alguna forma u otra requiere que especifiquemos que se trata de un nuevo array y, si la inferencia no lo permite, el tipo de los elementos que contendrá:

// Forma verbosa y redundante:
int[] arr1 = new int[3] { 1, 2, 3 };

// Forma clásica, usando 'var' y especificando número y tipo elementos:
var arr2 = new int[3] { 1, 2, 3 };

// Dejamos que el compilador detecte el número de elementos:
var arr3 = new int[] { 1, 2, 3 };

// Dejamos que la inferencia de tipos determine el tipo de los elementos:
var arr4 = new [] { 1, 2, 3 };

// O bien, la más concisa, usando la sintaxis con llaves (sólo válida para arrays):
int[] arr5 = { 1, 2, 3 };

martes, 12 de diciembre de 2023
.NET

Los records son una interesante fórmula para definir tipos en C# de forma rápida gracias a su concisa sintaxis, además de ofrecer otras ventajas, entre las que destacan la inmutabilidad o la implementación automática de métodos como Equals(), GetHashCode() o ToString().

Por si no tenéis esto muy fresco, aquí va un ejemplo de record y la clase tradicional equivalente en C#:

// Record:
public record Person(string FirstName, string LastName);

// Clase equivalente (generada automáticamente):
public class Person
{
    public string FirstName { get; init; }
    public string LastName { get; init; }

    public Person(string firstName, string lastName)
    {
        FirstName = firstName;
        LastName = lastName;
    }

    public override bool Equals(object obj)
    {
        return obj is Person person &&
               FirstName == person.FirstName &&
               LastName == person.LastName;
    }

    public override int GetHashCode()
    {
        return HashCode.Combine(FirstName, LastName);
    }

    public Person With(string FirstName = null, string LastName = null)
    {
        return new Person(FirstName ?? this.FirstName, LastName ?? this.LastName);
    }

    public void Deconstruct(out string firstName, out string lastName)
    {
        firstName = this.FirstName;
        lastName = this.LastName;
    }
}

Como podéis comprobar, hay pocas características de C# que ofrezcan una relación código/funcionalidad tan bárbara como los records. Por ello, poco a poco van ganando popularidad y comenzamos a verlos ya de forma habitual en código propio y ajeno.

Sin embargo, su concisa sintaxis hacen que a veces no sea fácil intuir cómo resolver algunos escenarios que, usando las clases tradicionales, serían triviales.

Por ejemplo, hoy vamos a centrarnos en un escenario muy concreto pero frecuente, cuya solución seguro que veis que puede ser aplicada en otros casos: ya que en los records no definimos propiedades de forma explícita, ¿cómo podríamos aplicarles atributos?

martes, 14 de noviembre de 2023
C#

Desde su llegada con la versión 7 del lenguaje C#, allá por 2017, nuestro lenguaje favorito dispone de soporte para tuplas. Sin embargo, no he visto muchos proyectos donde estén siendo utilizadas de forma habitual; quizás sea porque pueden hacer el código menos legible, o quizás por desconocimiento, o simplemente porque lo que aportan podemos conseguirlo normalmente de otras formas y preferimos hacerlo como siempre para no sorprender al que venga detrás a tocar nuestro código.

Pero bueno, en cualquier caso, es innegable que las tuplas han venido para quedarse, así que en este post vamos a ver algunos usos posibles, y a veces curiosos, de esta característica del lenguaje C#.

martes, 7 de noviembre de 2023
C#

El otro día me descubrí escribiendo un código parecido al siguiente:

return $"Order: {Items.Length} items, {Total.ToString("#,##0.#0")}";

Mal, lo que se dice mal, no estaba; funcionaba perfectamente y cumplía los requisitos, pero me di cuenta de que no estaba aprovechando todo el potencial de las cadenas interpoladas de C# que ya habíamos comentado por aquí mucho tiempo atrás.

Y como siempre que tengo algún despiste de este tipo, pienso que quizás pueda haber alguien más al que le ocurra o no esté al tanto de esta posibilidad el lenguaje, así que vamos a ver cómo podíamos haberlo implementado de forma algo más simple.

martes, 20 de junio de 2023
C#

Desde la aparición de los nullable reference types, o tipos referencia anulables, en C# 8, nos encontramos frecuentemente con el warning de compilación CS8618, que nos recuerda que las propiedades de tipo referencia definidas como no anulables (es decir, que no pueden contener nulos) deben ser inicializadas obligatoriamente porque de lo contrario contendrán el valor null, algo que iría en contra de su propia definición.

Para verlo con un ejemplo, consideremos una aplicación de consola con el siguiente código:

var friend = new Friend() {Name = "John", Age = 32};
Console.WriteLine($"Hello, {friend.Name}");

public class Friend
{
    public string Name { get; set; }
    public int Age { get; set; }
}

La aplicación se ejecutará sin problema, aunque al compilarla obtendremos el warning CS8618:

D:\Projects\ConsoleApp78>dotnet build
MSBuild version 17.4.1+9a89d02ff for .NET
  Determining projects to restore...
  Restored D:\Projects\ConsoleApp78\ConsoleApp78.csproj (in 80 ms).
D:\Projects\ConsoleApp78\Program.cs(8,19): warning CS8618: Non-nullable property 'Name' 
 must contain a non-null value when exiting constructor. Consider declaring the 
 property as nullable.
[...]

[D:\Projects\ConsoleApp78\ConsoleApp78.csproj]
    1 Warning(s)
    0 Error(s)

Time Elapsed 00:00:02.63

D:\Projects\ConsoleApp78\ConsoleApp78>_

También en Visual Studio podremos ver el subrayado marcando la propiedad como incorrecta:

Visual Studio mostrando el warning CS8618 sobre la propiedad

Aunque muchas veces este warning viene bien porque nos ayudará a evitar errores, hay otras ocasiones en las que puede llegar a ser molesta tanta insistencia. Y en estos casos, ¿cómo podemos librarnos de este aviso?

Disclaimer: algunas de las soluciones mostradas no son especialmente recomendables, o incluso no tienen sentido en la práctica, pero seguro que son útiles para ver características de uso poco habitual en C#.

martes, 28 de marzo de 2023
C#

A raíz del artículo publicado hace algunas semanas sobre las ventajas de usar diccionarios en lugar de listas, me llegaba vía comentarios un escenario en el que se utilizaba una clase List<T> para almacenar objetos a los que luego se accedía mediante clave. Lo diferencial del caso es que dichos objetos tenían varias claves únicas a través de las cuales podían ser localizados.

Por verlo por un ejemplo, el caso era más o menos como el que sigue:

public class FriendsCollection
{
    private List<Friend> _friends = new();
    ...
    public void Add(Friend friend)
    {
        _friends.Add(friend);
    }

    public Friend? GetById(int id) 
        => _friends.FirstOrDefault(f => f.Id == id);

    public Friend? GetByToken(string token) 
        => _friends.FirstOrDefault(f => f.Token == token);
}

Obviamente en este escenario no podemos sustituir alegremente la lista por un diccionario, porque necesitamos acceder a los elementos usando dos claves distintas. Pero, por supuesto, podemos conseguir también la ansiada búsqueda O(1) si le echamos muy poquito más de tiempo.

martes, 21 de marzo de 2023
C#

El pattern matching de C# proporciona la capacidad de analizar expresiones para ver si cumplen determinados "patrones" o presentan características determinadas. Podéis ver algunos ejemplos interesantes en el post Un vistazo a los patrones relacionales y combinadores.

Aunque ya los tengo bastante interiorizados y hago uso de ellos cuando toca, todavía no se me ha dado el caso de necesitar los patrones de listas, introducidos hace unos meses en C# 11. Así que no está de más echarles un vistazo para cuando se dé la ocasión 😉

martes, 14 de marzo de 2023
C#

A veces, los problemas de rendimiento de las aplicaciones, o determinadas funcionalidades de ellas, vienen derivados del uso de estructuras incorrectas para almacenar los datos, ya sea en memoria, base de datos o en cualquier tipo de almacén.

En este post vamos a centrarnos en un caso específico que me he encontrado demasiadas veces en código real: el uso indebido del tipo List<T> cuando sólo nos interesa buscar en esta colección por una propiedad que actúa como identificador único del objeto T.

martes, 21 de febrero de 2023
C#

Aunque muchos de nosotros trabajamos a diario con C#, siempre hay algo nuevo por aprender o formas de utilizar algunas características que nunca se nos habían ocurrido. Siempre.

En un nuevo capítulo de la serie de C# bizarro, hoy os planteo un reto sobre este código:

var sum = (int a, int b) => a + b;
var sub = (int a, int b) => a - b;
var mul = (int a, int b) => a * b;
var result = sum - sub + mul;

Console.WriteLine("Resultado: " + result(3, 2));
¿Compila? Y si es así, ¿qué aparece por consola? ¡No sigáis leyendo! Echad un vistazo al código e intentad averiguarlo antes de ver la solución pulsando aquí :)
Pues sí, este código es totalmente válido y compilará sin problema. Y al ejecutarlo, por consola veremos lo siguiente.
Resultado: 6

En primer lugar el código compila correctamente porque las variables sum, sub y mult, que hemos definido usando expresiones lambda de tipo Func<int, int, int>, a la postre son simplemente delegados.

Además, los tres delegados tienen la firma idéntica (reciben dos valores int y retornan un int), podemos utilizar los operadores de combinación suma "+" y resta "-", lo que da lugar a un delegado de multidifusión (multicast delegate).

En nuestro código, creamos el nuevo delegado de multidifusión result combinando sum y mult, que son los dos delegados que se suman. Por otra parte, la resta de sub es simplemente una maniobra de distracción, pues se intentará eliminar de la combinación un delegado que no existía previamente, por lo que la operación será ignorada.

var result = sum - sub + mul;

Tras ejecutar esta línea, result será un Func<int, int, int> cuya invocación provocará que se ejecuten secuencialmente, y por orden de llegada, los delegados que han sido combinados.

Por tanto, cuando se evalúa la expresión result(3, 2), se ejecutará primero la función sum(3, 2) y luego mul(3, 2), y será el resultado de esta última la que se retorne finalmente. De ahí obtenemos el 6 que va a la consola.

¿Qué, habías acertado?


Publicado en Variable not found.
martes, 13 de diciembre de 2022
C#

Otra de las novedades del flamante C# 11 viene a romper una limitación histórica: la ausencia de tipos genéricos en la definición de atributos.

Hasta esta versión del lenguaje, cuando necesitábamos introducir referencias a tipos de datos en un atributo, debíamos pasar obligatoriamente por el uso de un System.Type y el verboso operador typeof(). Además de incómodo, no había forma de limitar los tipos suministrados, dando lugar a errores en tiempo de ejecución que bien podrían haber sido resueltos el compilación con las herramientas apropiadas.

Dicho así quizás sea difícil de entender, pero veámoslo con un ejemplo.