¿Debemos aprender una nueva forma de escribir bucles?
English version
Hasta el momento, cada vez que alguien menciona los peligros potenciales que comenzarán a “rondar” al programador con la introducción de las nuevas características de C# 3.0 (la inferencia de tipos de variables locales –léase var- y los métodos extensores, principalmente), mi reacción es siempre la misma: considero que son recursos necesarios para ofrecer el soporte a las consultas integradas, no son en principio características carentes de atractivo ni mucho menos “impresentables”, y a través de la educación podremos alertar a los desarrolladores para que sean capaces de evitar los peligros que el uso de estas características podría esconder.
Hace unos días he visto un post de Luke Hoban que ha reforzado mi convicción en el papel que tendremos los formadores en la educación de los programadores .NET; en este caso, enseñándoles a no utilizar las consultas integradas para lo que no tiene sentido. El post en cuestión muestra cómo resolver la distribución de pesas en una balanza utilizando un enfoque de fuerza bruta: una serie de “bucles” anidados que se modelan utilizando el operador de consulta estándar Range(min, max), que produce secuencialmente los valores enteros entre min y max, ambos inclusive. Me pareció una situación en la que la consulta integrada no aporta nada con relación a bucles tradicionales, y que sí podría comprometer el rendimiento.
El problema en cuestión me recordó ciertos ejercicios que poníamos hace 20 años en la Universidad a los estudiantes de la asignatura “Introducción a la Programación” (bajo la inestimable dirección del Dr. Miguel Katrib). Así que decidí programar uno de estos ejercicios por las dos vías: mediante bucles “tradicionales” y “a la Hoban”, para comparar ambas implementaciones.
Enunciado: Escriba un programa que determine todos los números de cuatro cifras que son iguales a la suma de las cuartas potencias de cada una de las cifras que lo componen. Por ejemplo, uno de esos números es 1634:
1634 = 14 + 64 + 34 + 44
Solución (naïve): Iteramos en un cuádruple bucle anidado sobre todos los posibles valores de cada una de las cuatro cifras del número.
Al final de este post se presenta el código del programa que prueba ambas variantes; en la variante basada en bucles, hemos introducido una lista genérica para hacerla lo más parecida posible a la variante LINQ.
En mi portátil, la relación entre los tiempos consumidos fue de 80:95. Honestamente, esperaba un desequilibrio bastante mayor. Pero lo importante es: ¿aporta algo la utilización de una consulta integrada aquí? Las opiniones son bienvenidas.
//*********************************************************************
class Program
{
static void Main(string[] args)
{
Stopwatch sw = new Stopwatch();
sw.Start();
// versión "clásica" List<int> cumplen = new List<int>();
for (int i1 = 1; i1 <= 9; i1++) for (int i2 = 0; i2 <= 9; i2++) for (int i3 = 0; i3 <= 9; i3++)
for (int i4 = 0; i4 <= 9; i4++) if (Condicion(i1, i2, i3, i4))
cumplen.Add(Numero(i1, i2, i3, i4));
foreach (int n in cumplen) Console.WriteLine(n);
sw.Stop();
Console.WriteLine("Transcurrido: " + sw.ElapsedMilliseconds.ToString());
sw.Start();
// versión LINQ IEnumerable<int> cumplen2 =
from j1 in Enumerable.Range(1, 9) from j2 in Enumerable.Range(0, 9)
from j3 in Enumerable.Range(0, 9) from j4 in Enumerable.Range(0, 9)
where Condicion(j1, j2, j3, j4) select Numero(j1, j2, j3, j4);
foreach (int n in cumplen2) Console.WriteLine(n);
sw.Stop();
Console.WriteLine("Transcurrido: " + sw.ElapsedMilliseconds.ToString()); Console.ReadLine();
}
static bool Condicion(int a, int b, int c, int d)
{
return Numero(a, b, c, d) == Cuarta(a) + Cuarta(b) + Cuarta(c) + Cuarta(d);
}
static int Numero(int a, int b, int c, int d) { return 1000 * a + 100 * b + 10 * c + d; } static int Cuadrado(int a) { return a * a; } static int Cuarta(int a) { return Cuadrado(a) * Cuadrado(a); }
}
//*********************************************************************