|
Predicados Anónimos en C# |
Predicados anónimos
Todo programador desarrolla, con el tiempo, cierto
sentido de la estética, que a la larga le resulta
muy útil para escoger lenguajes, herramientas o
simplemente técnicas de programación. Este placer
estético es el que nos permite hablar completamente
en serio de El Arte de Programar. Este humilde
escritor, en particular, está infantilmente
entusiasmado con la nueva versión de C#. Tras haber
tratado durante años con lenguajes como CLU o
Eiffel, que ofrecían características muy
interesantes, y que sin embargo, no lograban un
reconocimiento general, es alentador encontrarse
con C# v2.0, un lenguaje mainstream, y su
implementación de tipos paramétricos, iteradores,
métodos anónimos y otras delicatessen.
Hoy le traigo una pequeña sorpresa, un recurso
que muestra lo que podemos lograr si mezclamos dos
de las novedades introducidas en C# v2.0: los tipos
genéricos y los métodos anónimos. Comenzaremos con
un breve recordatorio, para ponernos de acuerdo
sobre la nomenclatura. Un tipo genérico, nombre
oficial en .NET, es lo mismo que un tipo
paramétrico, y significa que la declaración del
tipo contiene parámetros que deben asociarse más
tarde a otros tipos, para poder usar el primero.
Por ejemplo, en .NET 1.1 teníamos una clase
ArrayList, que nos permitía almacenar objetos de
cualquier procedencia. Su principal defecto era que
guardábamos un perro y sacábamos un objeto,
guardábamos una suegra y salía también un objeto…
por lo que puede imaginarse la cantidad de errores
que podía provocar. Ahora en .NET 2.0 tenemos una
clase List que sirve como sustituto, y que en la
documentación aparece declarada así:
public class List<T>…
La novedad consiste en la presencia del parámetro
T. Para usar esta clase, tenemos que sustituir T
por un tipo de datos concreto:
List<Suegra>
listaSospechosas = new List<Suegra>();
Ahora sólo podremos añadir suegras (o instancias de
clases derivadas) a la lista, y cuando
seleccionamos un elemento de la lista tenemos total
seguridad de que se trata de una suegra (o algo
parecido). Lo mejor de todo: no hay ningún tipo de
verificación dinámica de tipos tras estas
operaciones, como por desgracia sí ocurre en la
propuesta oficial de genericidad para Java. En
.NET, los compiladores son capaces de garantizar la
corrección de los tipos de datos en tiempo de
compilación.
¿Otro ejemplo? Aquí le muestro el nuevo tipo
Dictionary: una tabla de hash genérica, que admite
dos tipos paramétricos:
Dictionary<string,
Cliente> clientes =
new Dictionary<string, Cliente>();
Cliente cln = new Cliente("Ian", "Marteens");
clientes[cln.Name] = cln;
System.Console.WriteLine(cln["Ian"]);
Vamos a lo nuestro. ¿Cómo buscaría usted un
registro de clientes dentro de una lista de
clientes? Si utiliza la nueva lista genérica, su
código se parecerá mucho a lo siguiente:
public Cliente
Buscar(List<Cliente> lista, string nombre)
{
foreach (Cliente c in lista)
if (c.Nombre == nombre)
return c;
return null;
}
No hay nada malo en esta función. Analice, no
obstante, esta otra versión:
public Cliente
Buscar(List<Cliente> lista, string nombre)
{
return lista.Find(delegate (Cliente c)
{ return c.Nombre == nombre; });
}
¿A que parece extraño? Hay nada menos que todo un
bloque de código dentro de unos paréntesis que
pertenecen a una llamada a método. Para entender
qué está pasando es aconsejable aislar ese presunto
parámetro para analizarlo por separado:
delegate (Cliente c) {
return c.Nombre == nombre; }
Se trata de un método anónimo. Este fragmento se
evalúa como si fuese una “simple” expresión… de
tipo delegado, como sugiere la cabecera sintáctica.
Recuerde, si lo ha olvidado, que un tipo delegado
es el equivalente elegante de un puntero a una
función, porque en el fondo, el compilador de C#
convierte el fragmento anterior en un método
oculto, y lo que se pasa al método Find es un
puntero (perdón, ¡un delegado!) a dicho método.
Ahora echaremos un vistazo al método que recibe el
parámetro especial:
public Cliente
Buscar(List<Cliente> lista, string nombre)
{
return lista.Find(fragmento);
}
Lo que queda, luego de identificar la naturaleza
del fragmento, es una simple llamada a un método
llamado Find definido por la clase genérica List.
Según la documentación, la declaración de Find es
la siguiente:
public T Find(Predicate<T>
condición);
El tipo T, por supuesto, es el parámetro de tipo
declarado antes por la clase List. En cuanto al
tipo del parámetro, el misterioso y también
genérico Predicate, ésta es su declaración:
public delegate bool
Predicate<T>(T item);
Reunamos todo lo que sabemos: tenemos una clase
genérica, List, que nos permite trabajar sobre
listas dinámicas con elementos pertenecientes al
tipo que se nos antoje. Entre las virtudes de esta
clase, está la presencia de un método Find, que
busca un elemento dada una condición. Find ha sido
diseñado para recibir la dirección de un método que
evaluará si un elemento dado cumple o no con la
condición deseada. Normalmente, este tipo de
métodos se definiría por separado:
private string
nombreBuscado;
private bool EsNombreBuscado(Cliente c)
{
return c.Nombre == nombreBuscado;
}
public Cliente Buscar(List<Cliente> lista, string
nombre)
{
this.nombreBuscado = nombre;
return lista.Find(EsNombreBuscado);
}
Note que hemos recurrido a un truco bastante sucio
para poder pasar el nombre que se busca al
predicado: copiamos el parámetro de Buscar en un
campo de la clase común a este método y al
predicado. Como alternativa, C# 2.0 nos permite
declarar el método en línea, dentro de la propia
función… ¡y lo mejor de todo es que así tenemos
acceso directo al parámetro de la búsqueda, sin
campos adicionales!
Hay otro detalle a destacar. en el último listado
de código. Si ya ha trabajado con tipos delegados,
en la versión 1.1, puede que esta instrucción le
parezca incorrecta:
return
lista.Find(EsNombreBuscado);
En versiones anteriores de C#, no se podía pasar
directamente un nombre de método donde se
necesitaba un tipo delegado. En aquellas versiones,
lo correcto sería:
return lista.Find(new
Predicate<Cliente>(EsNombreBuscado));
Pero C# 2.0 simplifica el trabajo con tipos
delegados al añadir la capacidad de inferencia de
tipos al compilador. Internamente, seguimos creando
la instancia del tipo delegado, pero nos basta con
mencionar el nombre del método para que el
compilador sepa con cuál tipo delegado debe
trabajar. De hecho, al usar el método anónimo vimos
también un caso particular de inferencia de tipos.
Recordemos:
delegate (Cliente c) {
return c.Nombre == nombre; }
Hemos tenido que declarar los parámetros de entrada
del método anónimo, por razones obvias. En cambio,
¿se ha dado cuenta de que no hemos declarado el
tipo de retorno? El compilador es suficientemente
inteligente como para deducirlo a partir del
contexto.
Para terminar, valoremos la técnica desde un punto
de vista práctico. ¿Qué es preferible? ¿Seguir con
las búsquedas implementadas mediante bucles ad hoc,
o echar mano de estas nuevas técnicas?
Aparentemente, hay poca ganancia en pasarse a los
métodos anónimos. Sospecho que el mantenimiento del
código que usa el método anónimo es un poco más
sencillo, pero es cierto que con un ejemplo tan
simple es difícil apreciar alguna ventaja.
Por el contrario, suponga que debe ordenar una
lista de clientes. Primero se compararán los
nombres, y luego los apellidos, si los nombres
resultan ser iguales. ¿Cómo implementaríamos la
operación? Aquí sí merece la pena usar métodos
anónimos:
clientes.Sort(delegate
(Cliente c1, Cliente c2)
{
int rslt = c1.Nombre.Compare(c2.Nombre);
return rslt != 0 ? rslt :
c1.Apellidos.Compare(c2.Apellidos);
});
En primer lugar, sería impensable implementar a
mano el algoritmo quicksort: una cosa es realizar
una sencilla búsqueda lineal y otra muy diferente
es programar un quicksort de memoria y sin errores.
Nos decidimos entonces por usar el método
predefinido Sort, de la clase List, pero éste exige
un tipo delegado. ¿Declaramos e implementamos el
método de comparación de forma separada? No: así
tendríamos más código para mantener, con el riesgo
añadido de que pierda la sincronía con el resto del
código al menor descuido. Lo más sencillo: declarar
un método anónimo en línea, como muestra el ejemplo
anterior.

Nombre
Ian Marteens
Ubicación
Estados Unidos
Nota del Webmaster :
Ian
es un prestigioso escritor de varios libros de lenguajes
como C# y Delphi. Visite el siguiente sitio web para obtener
más Información
www.marteens.com
Si quieres acceder a nuestros foros haz clic
aquí
|
|