viernes, 13 de febrero de 2009

XNA Y METODOS DE EXTENSION - PARTE 3

El artículo original fue escrito el 17-Nov-08.

Con este tercer artículo, estoy culminando mi serie introductoria al mundo de los métodos de extensión y XNA.

Al principio, este artículo se centraría en cómo extender la clase "Random" para obtener nuevos colores, pero "Ziggy", en su reciente artículo titulado "Particles Using LINQ" -el cual vale la pena leer, por cierto- ha mostrado con claridad cómo lograrlo, así que tomaré ese gran artículo como punto de partidapara profundizar en la discusión del tema.

Si ves el método de Ziggy llamado "RandColor" -al cual me referiré en lo que sigue como "NextColor" para estar en línea con la clase Random, el da muestra de una de las nuevas características del Framework 3 de XNA: cuando usas "floats" no hay necesidad de crear un "Vector3" para luego crear un "Color", no más.

En previas versiones de XNA, para usar floats a fin de cear un color, debías declarar:

public static Color NextColor(this Random rand)
{
    return new Color(new Vector3(rand.NextFloat(), rand.NextFloat(), rand.NextFloat()));
}

Pero desde el XNA Framework 3 en adelante, lo siguiente también funciona:

public static Color NextColor(this Random rand)
{
    return new Color(rand.NextFloat(), rand.NextFloat(), rand.NextFloat());
}

Ahora bien, si ves la clase "Particle2D" en el artículo de referencia, entonces notarás el campo llamado "Color", el cual es declarado como una variable que comienza en mayúscula; un nuevo método de extensión podría ser creado para dicho campo, como sigue:

public static void NextColor(this Random rand, out Color color)
{
    color = new Color(rand.NextFloat(), rand.NextFloat(), rand.NextFloat());
}

Por ende, podemos usarlo dentro del método de actualización "Update" en el referido ejemplo, reemplazando la siguiente línea:

p.Color = rand.NextColor();

Por esta nueva línea:

rand.NextColor(out p.Color);

Dado que el tipo "Color" es un estructurado (struct), la ventaja de usarlo sobre la primer opción podría considerarse en general como marginal, pero eventualmente te encontrarás con la situación donde incluso una mejoría mínima en tu código es deseada, especialmente cuando te manejas con el compact framework.

Nueva pregunta: que tal si sigues la guía de diseño de C#, declarando el campo "color" en minúscula como un campo privado y así mismo, incluyes un "getter" y un "setter" en la clase, en la forma de una propiedad pública llamada "Color"?

En ese caso, la nueva sobrecarga no puede usarse dado que Propiedades e Indexadores no pueden pasarse como parámetros "out" y/o "ref". Por qué? Bueno, simplemente recuerda que cuando usas propiedades e indexadores no estás tratando con variables sino métodos. En este caso particular, la primera implementación -esto es, la cual se incluye en el artículo de Ziggy- sería la mejor solución.

Reconozcámoslo! Podrías estar tentado a pasar la instancia existente de "Particle2D" como "ref" para salvar la restricción antedicha, tal como sigue:

public static void NextColor(this Random rand, ref Particle2D particle)
{
    particle.Color = new Color(rand.NextFloat(), rand.NextFloat(), rand.NextFloat());
}

Debería compilar bien PERO SOLO si NO PASAS a ese método la variable temporal creada por un bucle "foreach" -en contraposición al ejemplo base. Esta operación simplemente NO ESTA PERMITIDA para las variables de iteración de bucles "foreach". Mi consejo aquí es simple: evita esta tentadora implementación y como dije antes sólo utiliza lo presentado por Ziggy. En general, la solución más simple es la correcta.

Una nota al márgen: en caso que seas nuevo con C#, es importante notar que en este caso estamos utilizando "ref" en vez de "out", puesto que necesitamos una instancia existente (esto es, no nula) de la clase a fin de asignar el nuevo color. De lo contrario, al usar "out" tendríamos que crear una instancia del tipo "Particle2D" desde dentro del método de extensión mismo ANTES de asignar el nuevo color al mismo.

Ahora bien, pasemos a una situación interesante. Que tal si:

  1. No estamos ni estaremos dentro de un bucle "foreach",
  2. No queremos exponer la totalidad del tipo "Particle2D" a un método de extensión, y al mismo tiempo
  3. Precisamos usar este método para aquellas clases donde estén declaradas propiedades de asignación de colores ("setters")?

Si ese es el caso, entonces deberías echarle un vistazo a la siguiente implementación:

public interface IColor
{
    Color Color { get; set; }
}
 
public static class HelperMethods
{
 
    ...
 
    public static void NextColor<T>(this Random rand, ref T instance)
        where T : class, IColor
    {
        instance.Color = new Color(rand.NextFloat(), rand.NextFloat(), rand.NextFloat());
    }
}

Como puedes ver, estamos aqui combinando el poder de "Generics" con métodos de extensión.

Pero, por qué la restricción "IColor"? Lo que estamos diciendo aquí es simple: únicamente tipos que implementen la propiedad "Color" pueden llamar a este método de extensión. Y aquellas instancias no nulas pasadas al método como parámetro (recuerda que estamos utilizando la palabra "ref" aquí) serán manejados bajo el tipo "IColor".

Ok, pero por qué la restricción "class" adicional? Pregunta engañosa. Como corolario de la anterior restrcción, sin no especificamos que sólo se aceptan instancias de clases como parámetros, entonces podrían además pasarse estructurados al llamar al método, y para ello también deberán implementar la interface "IColor" ... Lo cual siginifica? ... Tic tac tic tac ... Pseee, "boxing". Ummm! Esa palabra desagradable. Manejar estructurados mediante interfaces causa "boxing". Da miedo, no? Ahora ya lo sabes, en caso que no lo supieras.

Para resumir, como puedes ver los métodos de extensión son muy prácticos, pero tienes que usarlos con cuidado si quieres evitar ingresar en una trampa de diseño. Como es habitual, no todas las situaciones pueden resolverse de la misma forma, incluso si al principio te suena lógico. Puedes encontrarte diciendo "Quéeee? Pero por quéeee? ...". Así que espero que estos ejemplos te muestren qué usar y cuándo, y qué evitar y porqué.

Algunos pensamientos finales sobre métodos de extensión:

  1. Son fenomenales para tareas habituales: créelo o no, dos tipos comúnes donde siempre me encuentro repitiendo tareas usuales son las clases "TYPE" y "RANDOM", pero esta bondad puede ser utilizada para tantos tipos como precises,
  2. Son de utilización intuitiva: antes de que esta funcionalidad fuera introducida en el .NET Framework, uno usualmente escribía clases estáticas llenas de ayudantes ("helpers"); por favor no me malentiendas, aun tienes que hacerlo, pero ahora estos ayudantes se presentan en una forma más intuitiva y amigable al usuario. Con los métodos de extensión, no tienes que buscar métodos "navegando" la clase estática donde los declaraste e implementaste; en cambio, únicamente miras las instancias de los tipos que estés utilizando. Y por último pero no menos importante,
  3. Cumplen con los principios de diseño: como dije en mi primer artículo de la serie, no estamos rompiendo ningún principio de diseño, debido a que no contamos con acceso directo a los miembros privados de los tipos "extendidos".

En resúmen, si utilizas métodos de extensión con cuidado y sabiduría, pueden convertirse en un amigo verdadero a la hora de programar tu juego u aplicación basada en XNA: simplemente te dan una mando para entender cualquier tipo con nuevos métodos COMO SI éstos originalmente fueran parte de dichos tipos, lo cual a su vez puede volverse útil, por ejemplo:

  1. Cuando no quieres crear una especificación de una clase, o por otro lado,
  2. Cuando quieres agragar mas funcionalidad dentro de una clase sellada ("sealed") de alguna manera -hasta cierto punto, claro está.

Así que vamos, prueba los "Extension Methods" y comparte lo que encuentres con la Comunidad XNA. Sería grandioso y divertido saber para que los estas utilizando!

Bueno, espero que hayas encontrado esta serie útil. Comentarios y sugerencias constructivas son siempre bienvenidas.

Otra ronda, salud!
~Pete

XNA Y METODOS DE EXTENSION - PARTE 2

El artículo original fue escrito el 03-Nov-08.

Continuando con el tema de cómo usar la muy útil funcionalidad provista por los "Extension Methods" en nuestras creaciones basadas en XNA, para este artículo necesitaremos el juego de ejemplo "FuelCell" (recuerda que encontrarás este tutorial -junto con el "TopDownShooter"- en los archivos de ayuda en la sección "What's New In This Release").

Ok. En el capítulo llamado "Finishing Touches" del ejemplo "FuelCell", verás el siguiente método al final del archivo "FuelCellGame.cs":

private Rectangle GetTitleSafeArea(Viewport viewport)
{
Rectangle retval = new Rectangle(viewport.X, viewport.Y,
         viewport.Width, viewport.Height);

#if XBOX
retval = viewport.TitleSafeArea;
#endif

 return retval;
}


Si recuerdas mi último artículo sobre este tema, debes estar anticipando a esta altura que el estructurado "Viewport" puede ser "extendido" tal que se obtenga el área "insegura" en una sola llamada a un método.

Si esto es así, adivinaste! Podemos usar los métodos de extensión para declarar una operación llamada, digamos "GetFullArea", y brindarle la implementación adecuada.

No sólo pasaremos otr vez un parámetro por referencia (usando la palabra reservada "out") -como hicimos en la Parte 1 de esta serie- sino también para este caso particular, puesto que estamos manejándonos con un estructurado, podemos darnos el lujo de implementar una método sobrecargado que localmente declare, cree y retorne una instancia de un "Rectangle" (sé lo que estás pensando, no hay propiedades de extensión en C#, así que necesitaremos obtener el rectángulo llamando a un método).

Usando métodos de extensión, nuestra solución debería lucir como sigue:

using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

namespace MyNamespace.Math
{
public static class MathExtensions
{
public static void GetFullArea(this Viewport viewport,
              out Rectangle area)
{
area = new Rectangle(viewport.X, viewport.Y,
                   viewport.Width, viewport.Height);
}

public static Rectangle GetFullArea(this Viewport
              viewport)
{
return new Rectangle(viewport.X, viewport.Y,
                   viewport.Width, viewport.Height);
}
}
}

Ahora bie, el método "GetTitleSafeArea(...)" podría ser reimplementado de esta forma:

private Rectangle GetTitleSafeArea(Viewport viewport)
{
Rectangle retval;

 #if XBOX
retval = viewport.TitleSafeArea;
#else
viewport.GetFullArea(out retval);
#endif

 return retval;
}

O también así:

private Rectangle GetTitleSafeArea(Viewport viewport)
{
#if XBOX
return viewport.TitleSafeArea;
#else
return viewport.GetFullArea();
#endif
}

Recuerda dos cosas importantes:

  • Tendrás que agregar la sentencia "Using" correspondiente en el archivo que usará el método de extensión (que en este ejemplo es "MyNamespace.Math"), y
  • Tendrás que agregar manualmente una referencia al ensamblado "System.Core" en tu proyecto.

Esta técnica puede usarse para obtener, digamos -entre otras cosas, un "Vector2" que contenga el ancho y la altura de un "Viewport" (idem para las áreas de visualización y cliente). Dejo ésto como un ejercicio para que lo intentes (verás que es muy fácil de implementar).

Espero que encuentres este artículo útil.

Hasta la próxima,
~Pete