domingo, 15 de mayo de 2011

Extension Methods

Imaginemos que tenemos una clase que contiene datos de una Persona.

class Person
{
public string Name { get;set; }
public string Surname { get;set; }
}

Si dada una persona queremos obtener el nombre completo utilizando un método, hay varias opciones. Agregar un método a la clase es lo más simple:

public string GetFullName()
{
return Name + " " + Surname;
}

También podemos agregar un método en una clase de utilidades que reciba un objeto de tipo Person y realice el trabajo:

namespace Helpers
{
static class PersonHelper
{
public static string GetFullName(Person person)
{
return person.Name + " " + person.Surname;
}
}
}

Ambas opciones son válidas pero se utilizan en forma diferente:

using Helpers;
class Program
{
public static void Main()
{
var person = new Person { Name = "Juan", Surname = "Pérez" };
Console.WriteLine(person.GetFullName()); // En el caso del método miembro.
Console.WriteLine(PersonHelper.GetFullName(person)); // En el caso del método estático.
}
}

La ventaja de escribir el método miembro es que permite utilizarlo en forma sencilla usando el operador punto, pero sólo es una alternativa viable cuando tenemos acceso al código fuente de nuestros objetos, ya que lo que estamos haciendo es cambiando la definición de nuestra clase. El hecho de definir un método, además, trae aparejada la desventaja de que el código que estamos agregando está fuertemente acoplado con la definición del tipo, ya que podemos ver los campos privados y protegidos del mismo, lo cual no siempre es deseable.

Por otra parte, la definición del método estático, tiene acceso únicamente a las propiedades y métodos expuestos públicamente, lo que fuerza un nivel menor de acoplamiento, además puede definirse para tipos que están fuera de nuestro control, cómo pueden ser los tipos del Framework. La principal desventaja de este método, es que es mucho menos conciso (más verbose), y en caso de querer utilizar más de un método en forma encadenada tendremos cosas así:

StringHelpers.Capitalize(PersonHelper.GetFullName(person)); // Feo de leer.

Con el advenimiento de la tercer versión de C#, se incorporaron al standard los llamados Extension Methods, los cuales permiten usar una sintaxis más amigable para el uso de los métodos estáticos que hemos discutido.

Básicamente, si marcamos el primer parámetro de un método estático con el modificador this, dicho método podrá ser invocado de la misma manera que lo son los miembros de un tipo.

Redefinamos la clase PersonHelper:

namespace Helpers
{
public static class PersonHelper
{
public static string GetFullName(this Person person)
{
return person.Name + " " + person.Surname;
}
}
}

Como podemos ver, la única diferencia con la definición anterior es el hecho de que incorporamos el modificador this. El haber hecho esto, nos permite escribir el siguiente código:

view source
print?

using Helpers;
class Person
{
public string Name { get;set; }
public string Surname { get;set; }
}
class Program
{
public static void Main()
{
var p = new Person { Name = "José", Surname = "Gómez" };
Console.WriteLine(p.GetFullName()); // Se invoca cómo un método de instancia.
Console.WriteLine(PersonHelper.GetFullName(p)); // Se puede seguir invocando como método estático.
}
}

El código generado por el compilador es idéntico al de una invocación de un método estático normal, la única diferencia es el agregado (automático) del atributo ExtensionAttribute en la definición del método.

Otro ejemplo:

var fn1 = StringHelpers.Capitalize(PersonHelper.GetFullName(person));
var fn2 = person.GetFullName().Capitalize();
Console.WriteLine(fn1 == fn2); // Retorna true.

Para poder invocar un extension method, primero es necesario que su definición entre en el scope de resolución del archivo actual, para lo que se utiliza el keyword using junto con el namespace en el que está definido el extension method. Aún así, de existir un miembro con la misma firma en el tipo “extendido” dicho método tiene preferencia.

Si bien en esencia es un cambio menor, los extension methods mejoran mucho la legibilidad del código y habilitan la existencia de algo de gran importancia, LINQ, que será el tema de mi próximo post.