Archivo

Archive for 30 abril 2010

Cómo obtener la dirección MAC


Hace algunas lunas publiqué cómo obtener el número serial del disco duro usando C# y WMI. Como una especie de continuación, ahora toca explicar cómo obtener la dirección MAC (MAC Address), igual utilizando C# y WMI.

La dirección MAC: Media Access Control, es un identificador único asignado a prácticamente cualquier adaptador o tarjeta de red, por parte del fabricante.

Para obtener este número MAC, usamos WMI, y para ello primero referenciamos el ensamblado System.Management.dll. Luego nos creamos un ManagementClass que apunte a la configuración de adaptadores de red. Y finalmente, iteramos sobre los objetos hasta encontrarlo. Nota que antes debemos saber si la tarjeta de red está habilitada, por lo que revisamos la propiedad IPEnabled para asegurarnos que así sea.

using System;
using System.Collections.Generic;
using System.Management;

namespace Fermasmas.Wordpress.Com
{
  class Program
  {
    static void Main(string[] args)
    {
      ManagementClass management =
           new ManagementClass("Win32_NetworkAdapterConfiguration");
      ManagementObjectCollection adapters = management.GetInstances();

      List<string> macs = new List<string>();
      foreach (ManagementObject adapter in adapters)
      {
        bool isIpEnabled = (bool)(adapter["IPEnabled"] ?? false);
        if (isIpEnabled)
          macs.Add(adapter["MacAddress"] as string);
        adapter.Dispose();
      }

      foreach (string mac in macs)
        Console.WriteLine(mac);

      Console.ReadKey(true);
    }
  }
}

En la línea 11 creamos un objeto WMI que apunte hacia la configuración de adaptadores de red. Línea 13, obtenemos todos los adaptadores. Comenzamos a iterar sobre cada uno de estos. En la línea 18, obtenemos la propiedad IPEnabled del adaptador para saber si ese adaptador de red tiene la IP habilitada (i.e. está en uso). Si sí, obtenemos la propiedad MacAddress, que como habrás supuesto, es nuestra dirección MAC para ese adaptador. Y lixto. No olvides hacer un Dispose del objeto ManagementObject.

Y dado que actualmente estoy escribiendo el tutorial de Pininos con LINQ, pues cambiemos el código anterior por una consulta, ¿por qué no?

using System;
using System.Collections.Generic;
using System.Linq;
using System.Management;

namespace Fermasmas.Wordpress.Com
{
  class Program
  {
    static void Main(string[] args)
    {
      ManagementClass mgmt
          = new ManagementClass("Win32_NetworkAdapterConfiguration");
      ManagementObjectCollection adapters = mgmt.GetInstances();

      IEnumerable<string> macs = from ManagementObject adapter in adapters
                     where (bool)(adapter["IPEnabled"] ?? false)
                     select adapter["MacAddress"] as string;

      foreach (string mac in macs)
        Console.WriteLine(mac);

      Console.ReadKey(true);
    }
  }
}

Hasta la próxima.

Anuncios
Categorías:.NET Framework, C#, Cómo hacer Etiquetas:

Dibujar banderas en la consola


Ayer por l tarde respondí una pregunta del foro MSDN sobre cómo cambiar el color de fondo de la consola utilizando .NET y C#. La respuesta, por supuesto, es utilizar Console.BackgroundColor y Console.ForegroundColor, y lo que se escriba a continuación saldrá en los colores especificados.

Bueno, pues se me ocurrió hacer un pequeño programa que utilizando este principio pintara algunas banderas en el monitor. La idea es establecer el fondo e imprimir un caracter, un espacio en blanco. Y así dibujar las diferentes secciones de una bandera en la consola.

Alemania Francia
Inglaterra Noruega

 

No es nada útil, realmente, pero fue entretenido y ya que lo tengo, pues a postearlo. Así que bueno, un codiguín para pasar la tarde, jejeje.

using System;

namespace Fermasmas.Wordpress.Com
{
  abstract class Flag
  {
    public virtual int Width
    {
      get { return 79; }
    }

    public virtual int Height
    {
      get { return 24; }
    }

    protected virtual int WidthSectors
    {
      get { return 1; }
    }

    protected virtual int HeightSectors
    {
      get { return 1; }
    }

    protected bool IsWidthSector(int sector, int value)
    {
      int sectorWidth = Width / WidthSectors;
      return value >= sectorWidth * Math.Max(sector - 1, 0)
        && value < sectorWidth * sector;
    }

    protected bool IsHeightSector(int sector, int value)
    {
      int sectorHeight = Height / HeightSectors;
      return value >= sectorHeight * Math.Max(sector - 1, 0)
        && value < sectorHeight * sector;
    }

    protected void SetupEdgeSquare()
    {
      Console.BackgroundColor = ConsoleColor.Gray;
    }

    protected abstract void SetupSquare(int x, int y);

    public void Draw()
    {
      for (int y = 0; y < Height; y++) 
      {
        for (int x = 0; x < Width; x++)
        {
          Console.ResetColor();
          if (x == 0 || x == Width - 1 || y == 0 || y == Height - 1)
            SetupEdgeSquare();
          else
            SetupSquare(x, y);
          Console.Write(' ');
        }
        Console.WriteLine();
      }
    }
  }

  class GermanFlag : Flag
  {
    protected override int HeightSectors
    {
      get { return 3; }
    }

    protected override void SetupSquare(int x, int y)
    {
      if (IsHeightSector(1, y))
        Console.BackgroundColor = ConsoleColor.Black;
      else if (IsHeightSector(2, y))
        Console.BackgroundColor = ConsoleColor.Red;
      else if (IsHeightSector(3, y))
        Console.BackgroundColor = ConsoleColor.Yellow;
    }
  }

  class SpanishFlag : Flag
  {
    protected override int HeightSectors
    {
      get { return 3; }
    }

    protected override void SetupSquare(int x, int y)
    {
      if(IsHeightSector(2, y))
        Console.BackgroundColor = ConsoleColor.Yellow;
      else
        Console.BackgroundColor = ConsoleColor.Red;
    }
  }

  class FrenchFlag : Flag
  {
    protected override int WidthSectors
    {
      get { return 3; }
    }

    protected override void SetupSquare(int x, int y)
    {
      if (IsWidthSector(1, x))
        Console.BackgroundColor = ConsoleColor.Blue;
      else if (IsWidthSector(2, x))
        Console.BackgroundColor = ConsoleColor.White;
      else if (IsWidthSector(3, x))
        Console.BackgroundColor = ConsoleColor.Red;
    }
  }

  class ItalianFlag : Flag
  {
    protected override int WidthSectors
    {
      get { return 3; }
    }

    protected override void SetupSquare(int x, int y)
    {
      if (IsWidthSector(1, x))
        Console.BackgroundColor = ConsoleColor.Green;
      else if (IsWidthSector(2, x))
        Console.BackgroundColor = ConsoleColor.White;
      else if (IsWidthSector(3, x))
        Console.BackgroundColor = ConsoleColor.Red;
    }
  }

  class SwissFlag : Flag
  {
    public override int Width
    {
      get 
      { 
        return (int)Math.Round(Math.Min(base.Width, base.Height) * 1.7, 0); 
      }
    }

    public override int Height
    {
      get { return Math.Min(base.Width, base.Height); }
    }

    protected override int HeightSectors
    {
      get { return 5; }
    }

    protected override int WidthSectors
    {
      get { return 6; }
    }

    protected override void SetupSquare(int x, int y)
    {
      if (((IsHeightSector(2, y) || IsHeightSector(4, y)) && 
            IsWidthSector(4, x)) || (IsHeightSector(3, y) && 
           (IsWidthSector(3, x) || IsWidthSector(4, x) || 
           IsWidthSector(5, x))))
        Console.BackgroundColor = ConsoleColor.White;
      else
        Console.BackgroundColor = ConsoleColor.Red;
    }
  }

  class EnglishFlag : Flag
  {
    protected override int HeightSectors
    {
      get { return 9; }
    }

    protected override int WidthSectors
    {
      get { return 9; }
    }

    protected override void SetupSquare(int x, int y)
    {
      if (IsWidthSector(5, x) || IsWidthSector(6, x) || 
          IsHeightSector(6, y) || IsHeightSector(7, y) || 
          IsHeightSector(8, y))
        Console.BackgroundColor = ConsoleColor.Red;
      else 
        Console.BackgroundColor = ConsoleColor.White;
    }
  }

  class SweedishFlag : Flag
  {
    protected override int HeightSectors
    {
      get { return 9; }
    }

    protected override int WidthSectors
    {
      get { return 15; }
    }

    protected override void SetupSquare(int x, int y)
    {
      if (IsWidthSector(6, x) || IsWidthSector(7, x) || 
          IsHeightSector(6, y) || IsHeightSector(7, y) || 
          IsHeightSector(8, y))
        Console.BackgroundColor = ConsoleColor.Yellow;
      else
        Console.BackgroundColor = ConsoleColor.DarkBlue;
    }
  }

  class NorwegianFlag : Flag
  {
    protected override int HeightSectors
    {
      get { return 15; }
    }

    protected override int WidthSectors
    {
      get { return 25; }
    }

    protected override void SetupSquare(int x, int y)
    {
      if (IsWidthSector(10, x) || IsWidthSector(11, x) || 
          IsHeightSector(12, y) || IsHeightSector(13, y) || 
          IsHeightSector(14, y))
        Console.BackgroundColor = ConsoleColor.DarkBlue;
      else if (IsWidthSector(9, x) || IsWidthSector(12, x) || 
               IsHeightSector(11, y) || IsHeightSector(15, y))
        Console.BackgroundColor = ConsoleColor.White;
      else
        Console.BackgroundColor = ConsoleColor.Red;
    }
  }

  class IcelandicFlag : Flag
  {
    protected override int HeightSectors
    {
      get { return 15; }
    }

    protected override int WidthSectors
    {
      get { return 25; }
    }

    protected override void SetupSquare(int x, int y)
    {
      if (IsWidthSector(10, x) || IsWidthSector(11, x) || 
          IsHeightSector(12, y) || IsHeightSector(13, y) || 
          IsHeightSector(14, y))
        Console.BackgroundColor = ConsoleColor.DarkRed;
      else if (IsWidthSector(9, x) || IsWidthSector(12, x) || 
               IsHeightSector(11, y) || IsHeightSector(15, y))
        Console.BackgroundColor = ConsoleColor.White;
      else
        Console.BackgroundColor = ConsoleColor.DarkBlue;
    }
  }

  class GreekFlag : Flag
  {
    protected override int HeightSectors
    {
      get { return 10; }
    }

    protected override int WidthSectors
    {
      get { return 12; }
    }

    protected override void SetupSquare(int x, int y)
    {
      if (IsHeightSector(3, y) || IsHeightSector(5, y) || 
          IsHeightSector(7, y) || IsHeightSector(9, y))
        Console.BackgroundColor = ConsoleColor.White;
      else
        Console.BackgroundColor = ConsoleColor.DarkBlue;

      if (IsWidthSector(1, x) || IsWidthSector(2, x) || 
          IsWidthSector(3, x) || IsWidthSector(4, x) || 
          IsWidthSector(5, x))
      {
        if (IsHeightSector(1, y) || IsHeightSector(2, y) || 
            IsHeightSector(3, y) || IsHeightSector(4, y) || 
            IsHeightSector(5, y) || IsHeightSector(6, y))
        {
          if (IsWidthSector(3, x) || IsHeightSector(4, y))
            Console.BackgroundColor = ConsoleColor.White;
          else
            Console.BackgroundColor = ConsoleColor.DarkBlue;
        }
      }
    }
  }

  class Program
  {
    static void Main(string[] args)
    {
      Flag flag;
      bool quit;

      do
      {
        Console.Clear();
        Console.WriteLine("Selecciona una bandera a dibujar:");
        Console.WriteLine("1. Alemania");
        Console.WriteLine("2. España");
        Console.WriteLine("3. Francia");
        Console.WriteLine("4. Italia");
        Console.WriteLine("5. Suiza");
        Console.WriteLine("6. Inglaterra");
        Console.WriteLine("7. Suecia");
        Console.WriteLine("8. Noruega");
        Console.WriteLine("9. Islandia");
        Console.WriteLine("0. Grecia");
        Console.WriteLine("Q. Quitar");
        ConsoleKeyInfo key = Console.ReadKey(true);

        flag = null;
        quit = false;
        switch (key.Key)
        {
          case ConsoleKey.Q:
            quit = true;
            break;

          case ConsoleKey.D1:
            flag = new GermanFlag();
            break;

          case ConsoleKey.D2:
            flag = new SpanishFlag();
            break;

          case ConsoleKey.D3:
            flag = new FrenchFlag();
            break;

          case ConsoleKey.D4:
            flag = new ItalianFlag();
            break;

          case ConsoleKey.D5:
            flag = new SwissFlag();
            break;

          case ConsoleKey.D6:
            flag = new EnglishFlag();
            break;

          case ConsoleKey.D7:
            flag = new SweedishFlag();
            break;

          case ConsoleKey.D8:
            flag = new NorwegianFlag();
            break;

          case ConsoleKey.D9:
            flag = new IcelandicFlag();
            break;

          case ConsoleKey.D0:
            flag = new GreekFlag();
            break;

          default:
            Console.WriteLine("Llave inválida, intenta de nuevo...");
            Console.ReadKey(true);
            break;
        }

        if (flag != null)
        {
          Console.Clear();
          flag.Draw();
          Console.ReadKey(true);
          Console.ResetColor();
        }
      } while (!quit);
    }
  }
}

 

Bueno, eso es todo. Como dije, nada extraordinario y solo una curiosidad. Hasta la próxima.

u.u

Categorías:.NET Framework, C#, Código y ejemplos Etiquetas:

Pininos con LINQ – 2. Métodos de extensión


En la primera entrega de este tutorial: Pininos con LINQ, explicamos los conceptos básicos que hay tras esta tecnología. Para este momento ya sabemos cómo hacer consultas utilizando los operadores estándares de consulta.

En dicha entrada también mencioné que LINQ es azúcar sintáctica, y que en realidad los operadores de consulta eran interpretados por el compilador para producir llamadas a métodos de objetos que el CLR pueda entender. Pues bien, en esta entrada retomaremos lo expuesto en la primera entrega y lo explicaré a la luz de dichos objetos y métodos. Sigue leyendo.

En primera instancia, dijimos (y probamos) que LINQ actúa sobre cualquier colección de datos. Más preciso: actúa sobre cualquier enumeración. Es decir, sobre cualquier objeto que implemente System.Collections.Generic.IEnumerable<T>. Pero ¿cómo es esto posible? Gracias a una característica de C# 3.0 llamada métodos de extensión (y que casi puedo asegurar que la incorporaron para poder meter LINQ). En fin, en resumen: los operadores estándares se traducen a llamadas a métodos de extensión (ubicados dentro del espacio de nombres System.Linq) que operan sobre IEnumerable<T>. Veamos.

El primer método que llama nuestra atención es el IEnumerable<T>.Where. Éste método toma como parámetro un delegado de tipo Func<T, bool>, donde T es el tipo de dato de nuestra enumeracion. El delegado pide un método que tome un parámetro de tipo T y regrese un valor verdadero (si T debe seleccionarse) o falso (si T no debe hacerlo). Como vemos, es similar a la expresión “where”.

Para ilustrar lo anterior, recordemos un ejemplo sencillo de la primera entrega, que reproduzco a continuación.

int[] nums = new int[] { 1, 2, 3, 4, 5 };

IEnumerable<int> query =
    from num in nums
    where num % 2 == 0
    select num;
foreach (int num in nums)
    Console.Write("{0} ", num);

El ejemplo anterior devuelve solo los números pares, como lo especifica la expresión where. Por ende, lo podemos traducir con el método Where de la siguiente forma.

bool FilterFunc(int num)
{
    return num % 2 == 0;
}

void Test()
{
    int[] nums = new int[] { 1, 2, 3, 4, 5 };
    IEnumerable<int> query =
        nums.Where(new Func<int, bool>(FilterFunc));
    foreach (int num in nums)
        Console.Write("{0} ", num);
}

Mucho más largo, ¿verdad? Bueno, por eso los operadores de consulta son azúcar sintáctica… El punto es que utilizamos un delegado que sigue la firma adecuada que pide el método Where (en este caso, Func<int, bool> que se traduce en una función que tome un parámetro int y regrese un valor verdadero/falso).

Por supuesto, podemos hacer uso de las expresiones lambdas, que también son nuevas a C# 3.0. Así, lo anterior quedaría reducido a:

int[] nums = new int[] { 1, 2, 3, 4, 5 };
IEnumerable<int> query =
    nums.Where( num => num % 2 == 0);
foreach (int num in nums)
    Console.Write("{0} ", num);

Esta forma es más corta y cómoda, y es la que usaremos en el resto del apunte. Pero quise mostrar la anterior para que veas de dónde sale la expresión lambda (no he tenido tiempo de preparar algún artículo sobre expresiones lambda todavía, pero éstas son sencillas de entender; para mayor información consulta este artículo de MSDN).

Naturalmente, el siguiente paso es ver cómo podemos transformar el resultado. Sin mucha sorpresa, el método equivalente a la expresión “select” se llama… Select. Bueno, pues era obvio, ¿no? En fin, que Select toma como parámetro un delegado de tipo Func, pero ahora de la forma Func<T, TResult>, donde T es el tipo de dato de la colección original, mientras que TResult es el tipo de dato que va a resultar de la transformación. Por ejemplo, si queremos transformar los resultados a una cadena de texto, nuestro delegado sería de la forma Func<int, string>, y devolvería un IEnumerable<string>. Ejemplo:

string SelectFunc(int num)
{
    return "texto: " + num;
}

void Test()
{
    int[] nums = new int[] { 1, 2, 3, 4, 5 };
    IEnumerable<string> query =
        nums.Select(new Func<int, string>(SelectFunc));
    foreach (string num in nums)
        Console.WriteLine(num);
}

 

O bien, traducido a expresión lambda:

int[] nums = new int[] { 1, 2, 3, 4, 5 };
IEnumerable<string> query =
    nums.Select( num => "texto: " + num);
foreach (string num in nums)
    Console.Write("{0} ", num);

Ahora bien, ¿qué pasa si quisiéramos juntar el Where con el Select? Por ejemplo, para seleccionar todos los números pares y regresar su texto correspondiente, tendríamos que ejecutar el Where y sobre el resultado de éste llamar al Select –a final de cuentas Where regresa un IEnumerable.

bool FilterFunc(int num)
{
  return num % 2 == 0;
}

string SelectFunc(int num)
{
  return "texto: " + num;
}

void Test()
{
  int[] nums = new int[] { 1, 2, 3, 4, 5 };

  IEnumerable<int> q1 = nums.Where(num => num % 2 == 0);
  IEnumerable<string> q2 = q1.Select(num => "texto: " + num);
  foreach (string num in q2)
    Console.WriteLine(num);
}

O bien, para hacer la expresión más corta, simplemente los encadenamos en una sola sentencia:

IEnumerable<string> query = nums
                .Where(num => num % 2 == 0)
                .Select(num => "texto: " + num);
foreach (string num in query)
  Console.WriteLine(num);

Por supuesto, todo lo que hablamos en la entrega anterior aplica en este caso (al final, el compilador traduce a este mismo código). Por ejemplo, ¿recuerdas cuando dijimos que la consulta sobre los datos se ejecuta hasta que se itera sobre la enumeración? Pues obvio esto también funciona aquí. Este es el ejemplo de la excepción dividida entre cero, utilizando métodos de extensión de LINQ.

int zero = 0;
int[] nums = new int[] { 1, 2, 3, 4, 5 };
IEnumerable<int> query = nums
    .Select(num => num / zero);
foreach (int num in query)
    Console.WriteLine(num);

La excepción DivideByZeroException se lanzará hasta que se ejecute el primer foreach.

Adicionalmente, también aplica todo lo dicho sobre los tipos de dato anónimos. El siguiente código muestra una consulta que devuelve un tipo anónimo. ¿Recuerdas la clase Employee que utilizamos? Pues está de vuelta. De hecho vamos a traducir el mismo ejemplo que usé la vez pasada. No muestro el código de Employee por razones de espacio (y flojerita). Por cierto, nota el uso del identificador var.

List<Employee> emps = new List<Employee>()
{
  new Employee("Fernando Gómez", 27, 20000F, Gender.Male),
  new Employee("Katia De Montanaro", 29, 30000F, Gender.Female),
  new Employee("Moisés Pedraza", 30, 25000F, Gender.Male),
  new Employee("Joan Hernández", 25, 20000F, Gender.Male),
  new Employee("Claudia Estrella", 27, 15000F, Gender.Female)
};

var query = emps
  .Where(emp => emp.Gender == Gender.Female)
  .Select(emp => new { Data = emp.Name + " - " + emp.Gender });
foreach (var result in query)
  Console.WriteLine(result.Data);

Analiza detenidamente el código anterior, de nueva cuenta. ¿Ya lo notaste? El código hace lo que promete, pero resalta un hecho de utilizar tipos anónimos: éstos solo pueden ser empleados en una función lambda. En efecto, dada su naturaleza es imposible saber cuál es el nombre que el compilador le asigna. Luego, no podemos referenciarlos y por ende no podríamos crear nuestro delegado ya que no sabríamos especificar el parámetro de retorno del método invocado por éste. En otras palabras, ¿cómo escribirías el delegado: Func<T, *>, ¿qué pondrías en lugar del asterisco? No can do.  Sugerencia: utiliza siempre lambdas.

Hasta el momento hemos visto como el trasladar las expresiones select y where a sus métodos equivalentes Select y Where ha sido muy directo. Es decir, solo cambia un poquito la forma de escribir, pero al final creo que no hay mucha diferencia entre operadores y métodos de consultas. Sin embargo esto es un poco diferente con el ordenamiento.

De entrada, para ordenar de alguna forma en particular se utilizan dos métodos: OrderBy y OrderByDescending, respectivamente. Éstos reciben un delegado, el cual regresa la variable sobre la que se va a ordenar (y el tipo de dato de ésta debe implementar IComparable). Por ejemplo:

var query = emps
  .Where(emp => emp.Gender == Gender.Female)
  .OrderBy(emp => emp.Income)
  .Select(emp => new { Data = emp.Name + " - " + emp.Gender });
foreach (var result in query)
  Console.WriteLine(result.Data);

ordenaría el resultado de forma ascendente (de menor ingreso al mayor), y:

var query = emps
  .Where(emp => emp.Gender == Gender.Female)
  .OrderByDescending(emp => emp.Income)
  .Select(emp => new { Data = emp.Name + " - " + emp.Gender });
foreach (var result in query)
  Console.WriteLine(result.Data);

lo haría en forma descendente (mayor ingreso al menor). Nota que ambos métodos regresan un IOrderedEnumerable en lugar de un simple IEnumerable.

Bueno, ¿y qué pasa si queremos aplicar dos o más criterios de ordenamiento? Es decir, consideremos que queremos ordenar nuestros empleados por género ascendente y luego por nombre descendente. Usando los operadores de consulta, haríamos:

var query = from emp in emps
            orderby emp.Gender ascending, emp.Name descending
            select new { Data = emp.Name + " - " + emp.Gender };

Para hacerlo usando los métodos de extensión, usamos OrderBy u OrderByDescending con el primer criterio de ordenamiento, pero con el segundo (y subsiguientes) tenemos que emplear los métodos ThenBy y ThenByDescending, respectivamente. De esta forma:

var query = emps
        .OrderBy(emp => emp.Gender)
        .ThenByDescending(emp => emp.Name)
        .Select(emp => new { Data = emp.Name + " - " + emp.Gender });

Esto es así porque tanto OrderBy como OrderByDescending toman un IEnumerable, que en principio está desordenado. Pero para añadir un segundo criterio de ordenamiento se tiene que asumir que la enumeración ya ha sido previamente ordenada: de no hacerlo corremos el riesgo de “desordenar” nuestra enumeración. Por eso es que se necesitan los métodos ThenBy y ThenByDescending, que toman como parámetro un IOrderedEnumerable (el devuelto por OrderBy y OrderByDescending). Por supuesto, para añadir un tercer criterio de ordenamiento utilizamos ThenBy y ThenByDescending respectivamente, y así sucesivamente.

Y así llegamos al final de esta entrada. Al igual que en la anterior, dejamos en suspenso lo relativo a uniones de datos y agrupaciones (groupby y join, respectivamente), pero esto es así porque en una entrada posterior explicaremos cómo se utilizan.

Así pues, ahora sí queda claro cómo es que LINQ no es nada del otro mundo: puros métodos de extensión; y se reafirma lo que comentaba acerca de que LINQ es azúcar sintáctica. De qué forma realizar la consulta: utilizando los operadores estándares de consulta, o bien utilizando los métodos de extensión, eso es algo que depende del gusto de cada quién. Utilizar los operadores de consulta es una forma elegante que mejora la legibilidad del código. Sin embargo, los métodos de extensión son más claros con respecto a qué es lo que se está realizando, desde la perspectiva del CLR. Ahora sí que cada quién. En lo personal, me quedo con los operadores de consulta, siempre que sea posible y no tenga que realizar acciones demasiado complicadas, en cuyo caso un lambda sería menos engorroso.

En fin, lo importante es que ya sabes qué ocurre tras bambalinas. No te pierdas la siguiente entrega de este tutorial: Pininos con LINQ.

Categorías:.NET Framework, C#, Tutorial Etiquetas: ,

Crear nuestra propia etiqueta [url]


Hoy me vi en la necesidad de crear una columna personalizada para un sitio en SharePoint que estoy desarrollando. SharePoint cuenta con una columna, Hyperlink, que permite guardar enlaces a una dirección web, en un par URL-título. Pero no permite guardar en una sola columna varios de estos pares, por lo que si requiero que mi lista tenga un número variable de enlaces, tengo que crear varias columnas Hyperlink, y eso no está chidito.

Así las cosas, me di a la tarea de crear mi propia columna. Sin embargo, solo puedo guardar texto, así que decidí que lo mejor era guardar los diferentes enlaces en pares URL-título de la siguiente forma:

[url=https://fermasmas.wordpress.com]Blog de Fer++[/url]

 

Este tipo de notación no es nueva, y de hecho se puede encontrar en diversas wikis y blogs. Lo que yo necesito, pues, es poder guardar varios de estos valores, y el control que muestra la columna personalizada parsea las diferentes etiquetas y las muestra en forma de lista.

Si te encuentras en una situación similar, sigue leyendo.

Para resolver este problema, me cree una clase hace precisamente eso. Lo interesante de ésta, por supuesto, es el método Parse, que utiliza una expresión regular para obtener la URl y el título.

La clase en cuestión es la siguiente.

using System;
using System.Runtime.Serialization;
using System.Text.RegularExpressions;

namespace Fermasmas.Wordpress.Com
{
  public class Url : ISerializable
  {
    private string _url;
    private string _title;
    private string _description;
    private static Regex _regex;

    public static readonly Url Empty;

    static Url()
    {
      _regex = new Regex(
           @"\[url=(http|https)://{1}([a-zA-Z0-9/%@?:#&+._=-]*)\](.*?)\[/url\]",
           RegexOptions.Compiled);
      Empty = new Url("http://");
    }

    public Url()
      : this(string.Empty, string.Empty)
    {
    }

    public Url(string url)
      : this(url, string.Empty)
    {
    }

    public Url(string url, string title)
    {
      if (url == null)
        throw new ArgumentNullException("url");
      if (title == null)
        throw new ArgumentNullException("title");

      _url = url;
      _title = title;
      _description = string.Empty;
    }

    protected Url(SerializationInfo info, StreamingContext context)
    {
      if (info == null)
        throw new ArgumentNullException("info");

      _url = info.GetString("UrlPath") ?? string.Empty;
      _title = info.GetString("Title") ?? string.Empty;
      _description = info.GetString("Description") ?? string.Empty;
    }

    public string UrlPath
    {
      get { return _url; }
      set
      {
        if (value == null)
          throw new ArgumentNullException("value");
        _url = value;
      }
    }

    public string Title
    {
      get { return _title; }
      set
      {
        if (value == null)
          throw new ArgumentNullException("value");
        _title = value;
      }
    }

    public string Description
    {
      get { return _description; }
      set
      {
        if (value == null)
          throw new ArgumentNullException("value");
        _description = value;
      }
    }

    public string CodedUrl
    {
      get { return string.Format("[url={0}]{1}[/url]", _url, _title); }
    }

    public void Parse(string input)
    {
      _url = string.Empty;
      _title = string.Empty;

      Match match = _regex.Match(input ?? string.Empty);
      if (match.Success)
      {
        string protocol = string.Empty;
        if (match.Groups.Count >= 2)
          protocol = match.Groups[1].Value;
        string url = string.Empty;
        if (match.Groups.Count >= 3)
          url = match.Groups[2].Value;
        string text = string.Empty;
        if (match.Groups.Count >= 4)
          text = match.Groups[3].Value;
        _url = string.Format("{0}://{1}", protocol, url);
        _title = text;
      }
    }

    public Uri ToUri()
    {
      return ToUri(true);
    }

    public Uri ToUri(bool fail)
    {
      Uri uri;
      bool created = Uri.TryCreate(UrlPath, UriKind.RelativeOrAbsolute, out uri);
      if (!created && fail)
        throw new UriFormatException(
            string.Format("La url '{0}' está mal formada. ", UrlPath));
      
      return uri;
    }

    public override string ToString()
    {
      return CodedUrl;
    }

    public void GetObjectData(SerializationInfo info, StreamingContext context)
    {
      if (info == null)
        throw new ArgumentNullException("info");

      info.AddValue("UrlPath", _url);
      info.AddValue("Title", _title);
      info.AddValue("Description", _description);
    }
  }
}

Creo que la clase no es muy difícil de entender. Lo interesante ocurre en el método Parse (línea 94). Este método utiliza una expresión regular (estática, definida en el constructor estático, línea 18), la cual obtiene hasta cuatro grupos:

  • El primer grupo obtiene la etiqueta completa. Es decir: [url=https://fermasmas.wordpress.com]Blog de Fer++[/url].
  • El segundo grupo obtiene el protocolo, limitado a http y https.
  • El tercer grupo obtiene la URL sin el protocolo, en nuestro ejemplo, fermasmas.wordpress.com.
  • El cuarto grupo obtiene el título, osea Blog de Fer++.

La expresión regular fuerza a que ésta sea de la siguiente forma:

  • [url=http://url]título[/url]
  • [url=https://url]título[/url]

Donde URL es cualquier URL válida formada por caracteres alfanuméricos y cualesquiera signos “%@?:#&+._=-“. Nada fuera del otro mundo.

La clase implementa ISerializable porque necesito poder serializarla a binario o XML, y para ello implementamos el constructor protegido, que se encarga de de-serializar, y el método GetObjectData. Nada de interés para el tema en cuestión.

Fuera de eso todo lo demás es fácil de comprender.

Ahora bien, también tuve que crear una clase UrlCollection que me permita almacenar una colección de URLs. Esta clase también implementa un método Parse, pero a diferencia del de la clase Url donde el método obtiene la primera concordancia de la expresión regular, aquí se obtienen todas las concordancias, y para cada una agrega un nuevo objeto Url a la colección. He aquí el código.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Runtime.Serialization;
using System.Text;
using System.Text.RegularExpressions;

namespace ePeople.VirtualStore
{
  public class UrlCollection : Collection<Url>, ISerializable
  {
    private static Regex _regex;
    private static readonly string _serializationName;

    static UrlCollection()
    {
      _regex = new Regex(
           @"\[url=(http|https)://{1}([a-zA-Z0-9/%@?:#&+._=-]*)\](.*?)\[/url\]", 
           RegexOptions.Compiled);
      _serializationName = "Values";
    }

    public UrlCollection()
      : base()
    {
    }

    public UrlCollection(IList<Url> list)
      : base(list)
    {
    }

    protected UrlCollection(SerializationInfo info, StreamingContext context)
    {
      if (info == null)
        throw new ArgumentNullException("info");

      string values = info.GetString(_serializationName);
      if (!string.IsNullOrEmpty(values))
        Parse(values);
    }

    public void GetObjectData(SerializationInfo info, StreamingContext context)
    {
      if (info == null)
        throw new ArgumentNullException("info");
      info.AddValue(_serializationName, ToString());

    }

    public Url Add(string url, string title)
    {
      Url newUrl = new Url(url, title);
      Add(newUrl);
      return newUrl;
    }

    public Url Add(string url)
    {
      return Add(url, string.Empty);
    }

    public void Parse(string input)
    {
      Parse(input, false);
    }

    public void Parse(string input, bool clear)
    {
      if (clear)
        Clear();

      MatchCollection matches = _regex.Matches(input ?? string.Empty);
      foreach (Match match in matches)
      {
        string protocol = string.Empty;
        if (match.Groups.Count >= 2)
          protocol = match.Groups[1].Value;
        string url = string.Empty;
        if (match.Groups.Count >= 3)
          url = match.Groups[2].Value;
        string text = string.Empty;
        if (match.Groups.Count >= 4)
          text = match.Groups[3].Value;
        url = string.Format("{0}://{1}", protocol, url);
        Add(url, text);
      }
    }

    public override string ToString()
    {
      StringBuilder value = new StringBuilder();
      foreach (Url url in this)
        value.Append(url.ToString());

      return value.ToString();
    }
  }
}

 

Ahora sí, el siguiente programa muestra cómo se puede utilizar.

class Program
{
  static void Main(string[] args)
  {
    string singleInput = 
            @"[url=https://fermasmas.wordpress.com]Blog de Fer++[/url]";
    string multipleInput = 
            @"[url=https://fermasmas.wordpress.com]Blog de Fer++[/url]
                 [url=http://www.tiburones-rojos.com]Tiburones Rojos[/url]
                 [url=http://www.codeproject.com]Code Project[/url]
                 [url=http://msdn.microsoft.com]MSDN[/url]";

    Url url = new Url();
    url.Parse(singleInput);
    Console.WriteLine("URL sencilla\n=====\n ");
    Console.WriteLine("Original: {0}", url.CodedUrl);
    Console.WriteLine("URL: {0}", url.UrlPath);
    Console.WriteLine("Título: {0}", url.Title);
    Console.WriteLine();

    UrlCollection urls = new UrlCollection();
    urls.Parse(multipleInput);
    Console.WriteLine("URL múltiples\n=====\n ");
    foreach (Url thisUrl in urls)
    {
      Console.WriteLine("Original: {0}", thisUrl.CodedUrl);
      Console.WriteLine("URL: {0}", thisUrl.UrlPath);
      Console.WriteLine("Título: {0}", thisUrl.Title);
      Console.WriteLine();
    }

    Console.ReadKey(true);
  }
}

Y al ejecutarse, obtenemos la siguiente salida en la consola.

URL sencilla
=====

Original: [url=https://fermasmas.wordpress.com]Blog de Fer++[/url]
URL: https://fermasmas.wordpress.com
Título: Blog de Fer++

URL múltiples
=====

Original: [url=https://fermasmas.wordpress.com]Blog de Fer++[/url]
URL: https://fermasmas.wordpress.com
Título: Blog de Fer++

Original: [url=http://www.tiburones-rojos.com]Tiburones Rojos[/url]
URL: http://www.tiburones-rojos.com
Título: Tiburones Rojos

Original: [url=http://www.codeproject.com]Code Project[/url]
URL: http://www.codeproject.com
Título: Code Project

Original: [url=http://msdn.microsoft.com]MSDN[/url]
URL: http://msdn.microsoft.com
Título: MSDN


Y eso es todo. Sirva este código para que lo utilices directamente o bien para que lo adaptes para crear tus propias etiquetas. Nos vemos la próxima.

Post Scriptum: cuando termine la columna personalizada de SharePoint, pondré aquí el código para que veas cómo quedó.

Categorías:.NET Framework, C#, Código y ejemplos Etiquetas:

Pininos con LINQ – 1. Conceptos básicos


LINQ, acrónimo para Language Integrated Query, es el nombre que se le da a ciertas palabras reservadas, métodos y expresiones que contiene el lenguaje C# (versión 3 en adelante), y que permiten realizar consultas, muy similares a SQL, sobre diversas fuentes de datos: bases de datos, archivos XML, e incluso simples colecciones de objetos.

Por ejemplo, en SQL, uno hace una sentencia donde pide qué campos se traen de una o varias tablas, se especifican criterios de búsqueda y finalmente se determina la forma en que los resultados son ordenados. LINQ intenta emular este comportamiento, pero sin limitarse a una fuente de datos en particular.

Evidentemente, LINQ no es otra cosa que azúcar sintáctica. Esto es, al compilar traduce las expresiones LINQ en llamadas a ciertos métodos y regresan un resultado en particular (una colección IEnumerable<T>). De lo anterior se desprende que hay dos formas de usar LINQ: con las palabras reservadas de C#, o bien con llamadas a los métodos (de extensión, por cierto) que se definen en el espacio de nombres System.Linq.

Así pues comencemos con lo básico. En primer lugar, debes saber que LINQ actúa sobre cualquier objeto que implemente la interfaz IEnumerable<T>, donde T es un objeto cualquiera sobre el que queramos operar. Esto lo consigue a través de los métodos de extensión que se encuentran en el espacio de nombres System.Linq. Así, lo primero que hacemos al crear nuestra consulta es indicarle cual es la fuente de datos. Esto lo hacemos a través de la cláusula “from”. Consideremos el siguiente código.

int[] nums = new int[] { 1, 2, 3, 4, 5 };
from num in nums
...etc...

Sabemos que un array de enteros implementa IEnumerable<int>. De hecho, cualquier array de tipo T implementa IEnumerable<T>. Por lo que podemos utilizar LINQ. En la segunda línea vemos cómo decimos que se tomará el valor de la colección “nums”. El valor “num” es como una variable con la que podemos hacer referencia a cada objeto que pertenece a la colección (análogo a si hiciéramos “foreach (int num in nums)”).

En segundo lugar, para que la consulta esté completa, se necesita indicar qué valores interesa que se regresen (similar a cuando se hace un “select campo1, campo2 from tabla” en SQL). Si queremos que regrese el número tal cual, simplemente hacemos:

int[] nums = new int[] { 1, 2, 3, 4, 5 };
from num in nums
select num;

Y ahí termina nuestra cláusula, por lo que la terminamos con un punto y coma. Como en el select solo regresamos “num”, que es de tipo entero, nuestro valor de retorno de la consulta será un IEnumerable<int>. Por lo que para que la cláusula quede completa, tenemos que asignarla a alguna variable. Ahora sí:

int[] nums = new int[] { 1, 2, 3, 4, 5 };
IEnumerable<int> query = 
    from num in nums
    select num;

Por supuesto, en el select podemos hacer cualquier transformación que nos venga en gana. Por ejemplo, el siguiente código crea una consulta que regresa la representación en cadena de texto de cada número de la colección.

int[] nums = new int[] { 1, 2, 3, 4, 5 };
IEnumerable<string> query = 
    from num in nums
    select "texto: " + num.ToString();

Ahora bien, consideremos el siguiente ejemplo:

int zero = 0;
int[] nums = new int[] { 1, 2, 3, 4, 5 };
IEnumerable<int> query = 
    from num in nums
    select num / zero;

Éste código evidentemente divide cada número de la colección entre cero, lo cual –uno pensaría- nos debería lanzar un DivideByZeroException. Si ejecutamos este código así tal cual, y lo depuramos, vemos que cuando se asigna el valor a query no se lanza ninguna excepción, como cabría esperar. Esto es así debido a que cuando creamos la consulta LINQ, ésta no se ejecuta hasta que iteramos sobre los resultados, sobre query (por ejemplo, hasta que hacemos un foreach). En el siguiente código, obtenemos un DivideByZeroException en la línea 6, no antes.

int zero = 0;
int[] nums = new int[] { 1, 2, 3, 4, 5 };
IEnumerable<int> query = 
    from num in nums
    select num / zero;
foreach (int i in query)
    Console.WriteLine(i);

Hasta ahorita hemos visto como transformar datos dada una consulta. Un aspecto importante de ésta es el poder filtrar los datos que se deben obtener de la colección. Para ello, utilizamos una cláusula “where”, la cual debe ser seguida por una expresión que se evalúe a verdadero o falso. Para cada elemento de la colección, se evalúa dicha expresión, y si ésta es verdadera entonces se ejecuta el select para ese elemento. Si no, se descarta y se pasa al siguiente.

La cláusula where debe estar entre el from y el select. El siguiente ejemplo obtiene todos los números pares de la colección.

int[] nums = new int[] { 1, 2, 3, 4, 5 };
IEnumerable<int> query = 
    from num in nums
    where num % 2 == 0
    select num;

Cuando se ejecuta la consulta, recorremos la colección. Para el primer elemento, se evalúa “1 % 2 == 0”, lo cual es falso, por lo que “1” no se incluye en la colección de regreso. Algo similar ocurre para 3 y 5. Pero 2 y 4 sí cumplen, por lo que “query” contendrá solo dos números: 2 y 4. En otras palabras, al ser compilada, la anterior consulta generaría un pseudo-código similar a este:

int[] nums = new in[] { 1, 2, 3, 4, 5 };
List<int> query = new List<int>();
foreach (int num in nums)
{
  if (num % 2 == 0)
    query.Add(num);
}

Más o menos esa es una aproximación al código que generaría el compilador.

También podemos incluir criterios de ordenamiento utilizando la cláusula orderby … ascending y orderby … descending para ordenar de forma ascendente o descendente, respectivamente. Para poder ordenar de esta forma, el tipo de dato del objeto seleccionado en el orderby debe implementar la interfaz IComparable (o IComparer, en su defecto). Si no se implementa IComparable, se utilizará el comparador por defecto que tenga asociado el tipo de dato en cuestión. Por supuesto, int sí implementa esta interfaz, por lo que podemos ahora ordenar nuestros números pares de forma descendente, por ejemplo.

int[] nums = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
IEnumerable<int> query = 
    from num in nums
    where num % 2 == 0
    orderby num descending
    select num;

Las consultas pueden ser más complejas incluso, realizando joins y groups, pero eso es más avanzado y será otro tema de la serie “Pininos en LINQ”.

Por supuesto, podemos hacer consultas sobre tipos de dato que nosotros hayamos definido, como clases y estructuras. El siguiente ejemplo es un programa que utiliza una clase, Employee, y genera una consulta que obtiene todos los objetos que representen a mujeres.

using System;
using System.Collections.Generic;
using System.Linq;

namespace Fermasmas.Wordpress.Com
{
  enum Gender { Male, Female, Unknown }

  class Employee : IComparable
  {
    public string Name;
    public int Age;
    public float Income;
    public Gender Gender;

    public Employee()
      : this(string.Empty, 0, 0F, Gender.Unknown)
    {
    }

    public Employee(string name, int age, float income, Gender gender)
    {
      Name = name;
      Age = age;
      Income = income;
      Gender = gender;
    }

    public int CompareTo(object obj)
    {
      if (obj == null)
        throw new ArgumentNullException("obj");
      if (!(obj is Employee))
        throw new ArgumentException("obj debe ser de tipo Employee", "obj");

      Employee emp = obj as Employee;
      return Name.CompareTo(emp.Name);
    }

    public override string ToString()
    {
      return Name;
    }
  }

  class Program
  {
    static void Main(string[] args)
    {
        List<Employee> emps = new List<Employee>()
        {
          new Employee("Fernando Gómez", 27, 20000F, Gender.Male),
          new Employee("Katia De Montanaro", 29, 30000F, Gender.Female),
          new Employee("Moisés Pedraza", 30, 25000F, Gender.Male),
          new Employee("Joan Hernández", 25, 20000F, Gender.Male),
          new Employee("Claudia Estrella", 27, 15000F, Gender.Female)
        };

        IEnumerable<Employee> query =
        from emp in emps 
        where emp.Gender == Gender.Female
        orderby emp.Age ascending
        select emp;

        Print(query);

        Console.ReadKey(true);
    }

    static void Print<T>(IEnumerable<T> query)
    {
        foreach (T t in query)
            Console.WriteLine(t);
    }
  }
}

Al correr este ejemplo, obtenemos:

Claudia Estrella
Katia De Montanaro

Como podemos ver, LINQ es una forma muy buena y fácil no solo para realizar consultas sobre colecciones, sino que también para ordenar nuestros datos de forma rápida y elegante.

Pero aún hay más. Hasta el momento hemos utilizado la cláusula “select” para regresar los objetos completos (como un número entero o la clase Employee del ejemplo anterior). Sin embargo, ¿qué pasa si solo requiero un subconjunto del objeto? Por ejemplo, ¿qué pasa si solo me interesa obtener la propiedad Name del objeto Employee? Bueno, pues podemos combinar con LINQ un aspecto de C# conocido como tipos anónimos. Éstos no son muy útiles por sí solos, pero combinados con LINQ tenemos una forma elegante y potente no solo de realizar consultas, sino que además podemos hacerlas sobre propiedades específicas de un objeto.

Para llevar esto a cabo, basta con crear un tipo anónimo en la cláusula “select” e inicializarlo con el objeto parámetro (aquel que anteriormente simplemente devolvíamos). Sin embargo, dado que un tipo anónimo no puede ser referenciado por su nombre, no podemos crear una variable del tipo IEnumerable<T> que reciba la consulta, porque no sabemos el nombre de “T”. Pero podemos utilizar un tipo de dato variable para que el compilador se encargue de determinar el tipo de dato en base al tipo devuelto por la consulta, lo cual es perfectamente legal. Así pues, podemos modificar el ejemplo anterior para que nos devuelva solamente el nombre y el género. Esto lo haríamos de la siguiente forma.

var query =
  from emp in emps 
  where emp.Gender == Gender.Female
  orderby emp.Age ascending
  select new { Data = emp.Name + " - " + emp.Gender };
foreach (var result in query)
  Console.WriteLine(result.Data);

Como puedes observar, la línea 1 ha cambiado y ahora utilizamos “var” para que no haya problema con el tipo de dato que devuelve la consulta. Luego, en la línea 5 tenemos la cláusula “select” que regresa un tipo de dato anónimo con una sola propiedad: Data, que se forma de concatenar el nombre del empleado con su género. Y finalmente, en la línea 6 tenemos que usar “var” de nueva cuenta para iterar sobre la consulta, por lo mismo que en la línea 1.

Lo que hemos mostrado al momento es solo la superficie de LINQ, pero creo que es suficiente para mostrar su poder y sobre todo, la facilidad con la que ahora podemos buscar, filtrar y ordenar datos de una colección. Seguramente, si no conocías LINQ, te has dado cuenta del mundo de posibilidades que ésto abre: dado que LINQ opera sobre cualquier colección (i.e. cualquier objeto enumerable, es decir, que implemente IEnumerable<T>), es posible crear clases de lógica de negocio que obtengan datos de cualquier fuente (como bases de datos o un archivo XML) y que sean enumerables. En efecto, aplicaciones particulares de LINQ son LINQ to SQL, que permite utiliar LINQ para realizar consultas a bases de datos Microsoft SQL Server, LINQ to DataSet para realizar consultas sobre objetos System.Data.DataSet de ADO.NET, o LINQ to XML para realiar consultas sobre archivos XML. En apuntes subsiguientes iremos explicando esto y más, como parte de este tutorial.

Y ya para finalizar. En este apunte vimos los conceptos básicos de LINQ. Mencionaba yo al inicio que LINQ es “azúcar sintáctica”, y que el compilador traducía consultas a llamadas a métodos de extensión. En el siguiente apunte explicaré qué hay tras bambalinas, y qué métodos son los que llama LINQ. Más aún, veremos cómo utilizar las clases propias de LINQ (dentro de System.Linq) para realizar las mismas consultas, pero con llamadas a métodos de extensión en lugar de cláusulas from…select. No te lo pierdas.

Categorías:.NET Framework, C#, Tutorial Etiquetas: ,

Tipos anónimos


Un tipo anónimo es un objeto que es instanciado, pero sin darle un nombre en particular, y que es inicializado al momento de declararlo. Por supuesto, tras bambalinas, el compilador le genera un nombre, pero éste no está disponible en el resto de tu código fuente. Y por lo mismo, tienes que asignarlo a una variable declarada con “var”, ya que no puedes especificar el tipo de dato.

Para crear una instancia de un tipo anónimo basta utilizar el operador “new” seguido de las propiedades y su inicialización. Por ejemplo:

var tipo = new { Numero = 100, Cadena = "Avada kedavra", Real = 1.2F };

En este ejemplo, “tipo” contiene tres propiedades: Numero, de tipo int; Cadena, de tipo string; y Real, de tipo float. El compilador determina el tipo de dato de las propiedades en base al valor con el que se inicializa. Evidentemente la propiedad no puede quedar sin inicializar.

Un tipo anónimo no puede tener métodos, ni heredar de una clase (que no sea System.Object) ni implementar interfaces ni nada. Incluso las propiedades son de solo lectura.

Cuando uno manda llamar Equals del tipo anónimo (que hereda de object), obtendrá un valor verdadero solo si todas las propiedades entre ambas son iguales. Algo similar aplica para GetHashCode.

Los tipos anónimos parecen ser una limitante, pero son muy útiles cuando trabajamos con LINQ (en poco tiempo publicaré un tutorial al respecto).

Para mayor información, consulta la documentación de MSDN sobre tipos anónimos.

Categorías:.NET Framework, Apunte, C# Etiquetas:

Código de navegar estructura de directorios, en F#


Hace unos días escribí un apunte sobre cómo navegar la estructura de directorios. El tema tratado en ese artículo no viene al caso en estos momentos. Sin embargo, dicho apunte terminó en un pequeño codiguillo para mostrar la información de un directorio. Y eso es lo que me gustaría retomar.

Por otro lado, hace tiempo que le he echado un vistazo a F#, la nueva iniciativa de Microsoft para programación funcional. Pero la verdad es que nunca me había metido tanto como ahora. Y dado el inminente lanzamiento de Visual Studio 2010 para la próxima semana, con la oficial liberación de F#, he pensado que es buena idea ir posteando cosas en este lenguaje.

Así, tuve la brillante idea de migrar el código del apunte mencionado a F#. Por supuesto, no soy ningún experto y estoy aprendiendo, así que si por causalidad algún lector experto encuentra algún error, espero sea indulgente conmigo.

He aquí el código.

open System
open System.IO

let queryPath =
    Console.WriteLine("Ingrese un directorio:")
    Console.ReadLine()
    
let getPath (args : string[]) =
    match args.Length with
        | 0 -> args.[0]
        | _ -> queryPath
    
let openDir path =
    let dir = new DirectoryInfo(path)
    match dir.Exists with
    | true -> dir
    | _ ->
        Console.WriteLine("El directorio {0} no existe.", dir.Name)
        Console.WriteLine("¿Desea crearlo? (Y/N)")
        let keyInfo = Console.ReadKey(true)
        if keyInfo.Key = ConsoleKey.Y then
            dir.Create()
            dir.Refresh()
        dir
        
let showDirProperties (dir : DirectoryInfo) =
    match dir.Exists with
    | true ->
        Console.WriteLine("Nombre: {0}", dir.Name);
        Console.WriteLine("Nombre completo: {0}", dir.FullName);
        Console.WriteLine("Directorio padre: {0}", dir.Parent.Name);
        Console.WriteLine("Directorio raíz: {0}", dir.Root.Name);
        Console.WriteLine("Atributos: {0}", dir.Attributes);
        Console.WriteLine("Creado: {0}", dir.CreationTime);
        Console.WriteLine("Leído: {0}", dir.LastAccessTime);
        Console.WriteLine("Modificado: {0}", dir.LastWriteTime);
        ()
    | false -> ()
    dir
    
let showDirContent (dir : DirectoryInfo) =
    match dir.Exists with
    | true ->
        for subdir in dir.GetDirectories() do
            Console.WriteLine("[DIR] {0}", subdir.Name)
        for file in dir.GetFiles() do
            Console.WriteLine("- {0}", file.Name)
        ()
    | false -> ()
    
[<EntryPoint>]
let main (args: string[]) =
            
    getPath args
    |> openDir
    |> showDirProperties
    |> showDirContent
    
    ignore(Console.ReadKey(true))
    0

Funciona el código, ¿OK? Bueno, todavía no sé cómo emular las cláusulas try-catch, pero bueno, no debería dar mucho problema.

Intentaré explicar el código paso a paso.

Nuestra aplicación comienza en la línea 52. La función main lo que hace es una composición de funciones. Primero, ejecuta “getPath”, y el valor que ésta regresa se pasa a la función openDir, quien le pasa su valor de retorno a showDirProperties, y ésta hace lo propio con showDirContent. Digamos que su equivalencia en C# sería:

string getPath(string[] args) { ... }
DirectoryInfo openDir(string path) { ...}
DirectoryInfo showDirProperties(DirectoryInfo dir) { ... }
void showDirContent(DirectoryInfo dir) { ... }

static void Main(string[] args)
{
    showDirContent(
        showDirProperties(
            openDir(
                getPath(args)
            )
        )
    );

    Console.ReadKey(true);
}

Comencemos por revisar getPath. Esta función revisa si el parámetro (un array de cadenas de texto) tiene elementos. De hacerlo, regresa el primer elemento, y si no, obtiene el valor que regresa queryPath, el cual se encarga de obtener un directorio por parte del usuario.

La función openDir toma como parámetro una cadena de texto que representa la ruta del directorio, y devuelve un objeto DirectoryInfo. Si el directorio no existe, se asegura de preguntarle al usuario si desea crearlo, y así lo hace si el usuario lo decide.

Por otra parte, showDirProperties toma como parámetro un objeto DirectoryInfo y muestra las propiedades del directorio, si este existe. Y showDirContent hace un for-in-do (equivalente a un foreach) para imprimir los subdirectorios y los archivos contenidos.

El programa no es muy excitante, lo admito, pero hey, así se empieza: haciendo pequeños programillas.

Categorías:.NET Framework, Cómo hacer, F# Etiquetas: ,