Conceptos OOP: jerarquía de clases


Cuando era pequeño y estaba aburrido -casi siempre- me inventaba un juego, el cual consistía en que andaba por la casa buscando objetos hasta que escogía una característica de algo que me gustaba: el color de algo, su textura, su olor… y con base en esa característica me ponía a comparar los objetos que tenía a mi alrededor, y los diferenciaba entre sí. Veía que característica era la que ganaba al final.

En una ocasión, para desmayo de mis padres, la característica fue la resistencia de los objetos a un impacto tras una trayectoria de varios metros. Descubrí que las jarras, floreros y la colección de discos de vinil no eran resistentes…

Durante mi infancia, siempre pensé que era un juego ingenioso, hasta que me di cuenta que es un proceso natural de cualquier ser humano, y que posiblemente es el que nos distingue del resto de la fauna de este planeta: la clasificación de objetos.

Clasificando al mundo

En efecto, la clasificación de un objeto requiere un nivel de abstracción. Clasificar significa tomar un atributo común de un conjunto de objetos, y compararlos con otros objetos. Así, por ejemplo, tenemos una mesa de madera, una jarra con agua y una llanta de auto. Yo podría decir que tanto la mesa como la botella son artículos domésticos que se encuentran en cocinas, y diferenciarlo de la llanta, la cual no es un artículo doméstico. En este caso, estoy clasificando los objetos por su utilidad en el hogar. Claramente la mesa y la jarra son útiles en el hogar, y se espera que estén ahí por lo mismo. Sin embargo, si entro a la cocina y veo una llanta sobre la barra, no me hará mucho sentido: una llanta no tiene sentido en un hogar.

La clasificación está extendidísima en el quehacer humano. Los biólogos clasifican a los seres vivos en reinos. Los geólogos clasifican las eras de la Tierra, los historiadores clasifican los sucesos por períodos o etapas, los literatos clasifican las novelas por movimientos literarios, e incluso los jóvenes de hoy (snif) clasifican su música por género.

Esa capacidad de abstracción que tenemos nos da muchísimas ventajas, puesto que las clasificaciones nos permiten tratar objetos con atributos similares (y por tanto, dentro de una clasificación determinada) de forma idéntica. Así, sabemos que los mamíferos tienen ciertos comportamientos comunes, como la sociabilidad y la protección mutua. Por tanto, respecto a esto, podemos establecer lineamientos generales sin importar si tratamos con perros, leones o humanos.

Por otra parte, también podemos abstraer comportamientos. Por ejemplo, todos los mamíferos tienen el comportamiento de comer, aunque la forma de hacerlo varíe: el león probablemente cazará a su presa y la comerá cruda, mientras que el perro esperará a que su dueño le alimente con croquetas y huesos; el humano probablemente irá a algún restaurante. Pero el comportamiento es el mismo: el comportamiento aplica al sentido o intención de la acción, no tanto a cómo ésta se lleva a cabo. Otro ejemplo: todos los animales tienen el comportamiento de respirar. Por tanto, dado que un pez, un cangrejo y una gaviota son animales, podemos asegurar que respiran. Sin embargo, el pez respira separando el oxígeno del agua que le rodea, mientras que la gaviota toma el oxígeno disuelto del aire. El cangrejo hace ambos procesos, dependiendo de si se encuentra en tierra o mar.

Clasificando problemas

La abstracción y clasificación parecen ser una herramienta poderosa. De hecho lo es tanto que la usamos también para la resolución de problemas de todo tipo en nuestra vida cotidiana.

Pensemos en algún ejemplo. Un encargado de una fábrica de refrescos tiene que realizar diversas tareas. El área de planeación estratégica puede solicitarle que produzca más refrescos. El área de ventas puede solicitarle que le envíe refrescos a tal o cual supermercado. El área de calidad puede solicitarle que algún lote de refrescos sea destruido, pues no pasó la prueba de control. Y por último, el área de almacén puede solicitarle que traslade un lote de refrescos de una ubicación a otra. ¿Qué pueden tener estas tareas en común?

1.- Todas las tareas implican hacer algo con refrescos. Los refrescos están agrupados por sabores y presentaciones, y por lotes (i.e. fecha de creación).

2.- Todas las tareas implican un movimiento contable de almacén. entrada, salida a tienda, salida a desperdicio y movimiento interno.

3.- Todas las tareas conllevan un movimiento financiero: crear refrescos implica que se gastará materia prima, por lo que el movimiento será un costo. Lo mismo el destruir un lote porque no pasó las pruebas de calidad. Por otra parte, la venta al supermercado implica un movimiento positivo, pues entrará dinero a la fábrica por concepto de ventas. Por último, el movimiento entre almacén genera un movimiento neutral, pues no se gasta ni recibe nada.

4.- Todas las tareas tienen una persona quien la solicita y se hace responsable, y otra que la ejecuta y se hace responsable de llevarla a cabo.

5.- Todas las tareas deben realizarse en un período contable válido y en horarios laborales válidos.

Supongo que hay más, pero creo que con esas podemos ilustrar el tema. Si nosotros fuéramos el encargado de la fábrica, ¿qué podríamos hacer? Si no clasificamos, tendríamos que generar un proceso independiente para cada tarea. Pero no es el caso, podemos reutilizar si clasificamos bien.

De entrada, consideremos cada una de las tareas como una orden. La orden debe tener tres propiedades: el nombre de quien la solicita y el nombre de quien es responsable de ejecutarla (por el punto 4). De igual forma, debe tener una fecha de inicio y una fecha de fin (por 5). Por otra parte, deberá tener un objeto Inventario. El objeto Inventario es en realidad una lista que lleva el nombre de un refresco, su presentación y lote, y su cantidad. Todas las órdenes tienen esta lista, por tanto todas tienen un inventario. Finalmente, todas las órdenes deberán tener un comportamiento similar, llamado "hacer movimiento contable" y "hacer movimiento financiero".

clip_image001

Ya con esto, podemos decir que tenemos cuatro tipos de órdenes: de trabajo, órdenes de venta, orden de salida a desperdicio y orden de traslado. Comparten ciertas propiedades comunes, como el inventario o los responsables. Pero es evidente que hacer un movimiento contable y hacer un movimiento financiero tienen significados diferentes. Es decir, para los cuatro significa que van a afectar el inventario del almacén, y que van a afectar las finanzas de la fábrica. Pero para una orden de trabajo, significa que tendrán que gastar dinero en la producción , pero que ingresará nuevo producto al almacén; para una orden de venta es lo contrario: sale producto del almacén pero ingresa dinero a las arcas de la fábrica; la orden de salida a desperdicio es doble pérdida: sale material del almacén y no entra dinero en la billetera de la fábrica; por último, la orden de traslado mueve material, pero no sale ni entra, y no tiene movimiento financiero.

El haber hecho esta clasificación ayudará en mucho al encargado de la fábrica. Por ejemplo, seguramente tendrá que crear un formato para llevar el registro. Si no hubiera clasificado habría hecho cuatro formatos: uno por cada tarea. Pero ahora sabe que puede usar el mismo formato con los responsables, las fechas y el inventario. Ahora, quizás tenga que crear una hojita para llevar el registro especial de cada orden, el equivalente a "hacer movimiento contable" y "hacer movimiento financiero", pues seguramente tendrá que recolectar diferentes firmas, etc. Respecto a políticas y procedimientos, podrá crear las generales, y sólo las particulares para cada una de ellas, en lugar de crear una política y procedimiento para cada orden. Y así sucesivamente.

Clasificando bits

Hasta ahora hemos visto puros ejemplos de la vida real, aplicados a objetos físicos y a escenarios de uso en un negocio hipotético. Pero ¿y eso qué tiene que ver con la programación?

Para contestar esa pregunta, tenemos que hacer un poquito de historia de la computación. Recordemos que el propósito de una computadora es computar. Es decir, realizar operaciones que cuantifiquen. Entre estas operaciones, podemos computar valores lógicos y relacionarlos (i.e. álgebra booleana). Y gracias a esto, podemos realizar interpretaciones de hardware, lo cual se traduce en una instrucción a algún dispositivo. Un conjunto de instrucciones, una tras otra, forman un programa.

Lo que desprende de lo anterior es que un programa es un conjunto secuencial de instrucciones. Podemos tener unas diez instrucciones secuenciales, y la computadora lee la primera, la ejecuta y devuelve un resultado; lee la segunda, la ejecuta y devuelve un resultado; lee la tercera… y así sucesivamente. Es perfectamente natural para una computadora: lo único que necesita es tener las sentencias, el hecho de que sean secuenciales es parte inherente a su naturaleza: las computadoras no pueden pensar de otra forma. Se comen una sentencia, escupen el resultado y van por la siguiente.

Así las cosas, pues es perfectamente natural y entendible que durante años, cuando surgieron los lenguajes de programación, éstos se amoldaran a la forma de pensar de una computadora. Lenguajes como Cobol, Fortran y C escriben instrucción tras instrucción, una tras otra. Esto hizo imposible que el mismo código pudiera reutilizarse en más de un lugar. Esta necesidad provocó que salieran lenguajes estructurados, que permiten escribir subrutinas parametrizadas. Pero a pesar de esto, el hecho sigue siendo el mismo: instrucción tras instrucción, todo secuencial: llamar a una subrutina tras otra, etc.

El problema de pensar en instrucción tras instrucción es que tras miles de líneas de código, comienza a hacerse difícil de administrar, y por tanto, el código se vuelve propenso a errores. Pero lo peor es que esta forma de pensar no es natural en un humano. ¡Pues claro: si nosotros pensamos clasificando! Para nosotros sería mucho más natural enfocar los problemas de software mediante clasificaciones, en lugar de ejecutar tareas una tras otra. Estas necesidades son las que han provocado que surgieran los lenguajes orientados a objetos.

En otras palabras, los lenguajes orientados a objetos deben permitir realizar clasificaciones. Por supuesto, los lenguajes modernos como Java o C# permiten mucho más que crear clasificaciones, pero es el motivador principal. En C# se clasifica creando -tatatáaaan- clases. ¡Así es! Las clases y estructuras, y en general los tipos de datos, son las herramientas mediante las cuales creamos nuestras clasificaciones. Sin embargo, crear una clase no es suficiente, pues éstas, por sí mismas, no nos permiten crear relaciones de identidad.

Relaciones de identidad

Cuando hablábamos de los objetos y sus atributos y comportamientos, y cómo clasificarlos, pasamos por algo un concepto muy importante: las relaciones. Aunque las mencionamos brevemente no ahondamos mucho en ellas. Pues bien, es el momento de hacerlo.

Para poder clasificar, tenemos que comparar. Y para comparar, tenemos que relacionar propiedades y atributos, si no, no podríamos llevar a cabo la clasificación. Así, es importante que nos detengamos un momento para estudiar este concepto.

El primer tipo de relación, quizás el más sencillo, es el de asociación. La relación de asociación quiere decir que un objeto conoce a otro. Por ejemplo, un vaso debe conocer a una mesa, pues es donde se posa. Una mesa, sin embargo, no necesariamente conoce un vaso.

El segundo tipo de relación, más interesante, es el de la agregación. Es una relación de "x contiene un y". Por ejemplo, una computadora contiene un monitor, un teclado y un ratón. Otro tipo de relación, similar a la agregación, es la composición. Ésta es una relación de "x está formado por a, b y c". Por ejemplo, un automóvil está formado por un motor, unas llantas, un chasis, etc.

El último tipo de relación importante es el de la identidad. Esta relaciona las propiedades de un objeto con otro y crea generalizaciones. Es una relación de tipo "x es un tipo de y". Por ejemplo, un auto es un vehículo, un autobús es un vehículo, y una motocicleta es también un vehículo. Por otra parte, un vehículo es una máquina, una computadora es una máquina. Por lo tanto, un automóvil es una máquina. Y así sucesivamente.

Las relaciones de identidad quedan como anillo al dedo para todo lo que hemos visto. En efecto, podemos ver claramente que la clasificación que podemos hacer sobre ciertos objetos queda complementada cuando podemos hacer relaciones "x es un tipo de y". Más aún, según podemos ver en el ejemplo anterior, conforme establecemos relaciones de identidad, éstas se trasladan hacia arriba y hacia abajo, y entonces decimos que hemos creado una jerarquía. Ejemplo:

clip_image002

El ejemplo anterior es un poco somero, pero ilustra relaciones de identidad y una jerarquía de clases. Los animales, plantas y hongos son seres vivos, así que siempre que hablemos de un ser vivo, inferimos que es alguno de estos tres (por lo menos). Comportamiento común de los seres vivos: vivir, reproducir, comer, morir. Luego, un insecto, un mamífero, un ave, molusco o pez son un tipo de animal. Los homínidos, canes y felinos son un tipo de mamífero. Un humano es un tipo de homínido, y un león, tigre y un gato son tipos de felinos.

Pues bien, ya con clasificación y con relaciones de identidad entendidas, podemos regresar a C#. En .NET, las relaciones de identidad se hacen mediante la herencia.

Herencia en C# y .NET

El concepto de herencia, que no es otra cosa que establecer una relación de identidad entre dos clases, es medular en la plataforma .NET y en general, en cualquier plataforma / lenguaje de programación orientada a objetos. Junto con la encapsulación y el polimorfismo, conforman los pilares de la misma.

En .NET cada lenguaje decide cómo implementar la herencia. En el caso de C#, se hace poniendo dos puntos tras el nombre de la clase, seguido del nombre de la clase con la cual queremos establecer la relación.

class ClaseDerivada : ClaseBase
{
    …
}

Decimos que la clase con la que relacionamos es una clase base de la actual, a la cual se le conoce como clase derivada. También se les conoce como clase generalizada y clase especializada, respectivamente.

En C# y .NET, una clase tiene una y sólo una relación de identidad (si una clase no especifica su clase base, ésta en automático será la clase System.Object). En C++, por ejemplo, una clase puede tener cero, una o múltiples relaciones de identidad. A esta característica se le conoce como herencia múltiple. Las razones por las que C# no implementa herencia múltiple es que ésta causa problemas cuando no se implementa bien (lo cual lo convierte en un problema de un programador no muy hábil, más que una falla del lenguaje… pero bueno). Asimismo, la herencia siempre es pública (es decir, todos los métodos públicos se heredan como públicos), en contraste con C++ donde la herencia puede ser pública, protegida o privada.

Por supuesto, al momento de heredar, heredamos métodos y propiedades y atributos protegidos y públicos.

class ClaseBase
{
    public void Foo() { }
    public string Goo { get; set; } 
}

class ClaseDerivada : ClaseBase
{
    public void Hoo() { }
}

ClaseDerivada c = new ClaseDerivada();
c.Foo();
c.Goo = "Goo";

c.Hoo();

En este tenor, podemos hacer uso de la relación de la siguiente forma:

ClaseBase b = new ClaseDerivada();
b.Foo();
b.Goo = "Goo";

b.Hoo(); // no compila

Dado que ClaseDerivada es un tipo de ClaseBase, podemos asignar a una variable de tipo ClaseBase una instancia de ClaseDerivada. Podemos invocar al método Foo y a la propiedad Goo, puesto que ClaseBase las define. No podemos, sin embargo, invocar al método Hoo porque esa es propia de ClaseDerivada.

La herencia de clase puede seguir y seguir y seguir:

class SegundaClaseDerivada : ClaseDerivada { … }

class TerceraClaseDerivada : SegundaClaseDerivada { … }

class CuartaClaseDerivada : TerceraClaseDerivada { … }

Conforme vamos creando especializaciones, decimos que vamos creando una jerarquía de clases. Por supuesto, no importa qué tan profunda sea la jerarquía, la relación siempre se mantiene:

ClaseBase b = new CuartaClaseDerivada();
b.Foo(); // ok

b.Goo = "Goo"; // ok

A veces querremos que una clase ya no pueda seguir heredándose. En estos casos, decimos que la clase es final, o que está sellada. Para sellar una clase, usamos la palabra reservada "sealed" en C#. Cualquier intento por heredar de una clase sellada, generará un error de compilación.

sealed class QuintaClaseDerivada : CuartaClaseDerivada { … }

class SextaClaseDerivada : QuintaClaseDerivada { … } // error de compilación

Hay ocasiones en las que queremos crear clases que sólo sirvan como base. Es decir, que puedan ser heredadas, pero no instanciadas. A estas clases las llamamos abstractas, y se marcan con la palabra reservada "abstract".

abstract class ClaseBase { … }
class ClaseDerivada : ClaseBase { … }

ClaseDerivada d1 = new ClaseDerivada(); // ok
ClaseBase b1 = new ClaseDerivada(); // ok

ClaseBase b2 = new ClaseBase(); // error de compilación

 

Comportamientos y contratos

Como decíamos arriba, una clase abstrae comportamientos y atributos. Esto, traducido a C#, no es otra cosa que métodos y propiedades. Un conjunto de métodos define un comportamiento: esto es, de qué forma se modifica el estado de un objeto. Ahora bien, ¿qué pasa cuando queremos abstraer varios comportamientos? Los comportamientos en general definen cómo se interactúa y maneja el estado de un objeto. Pongamos un ejemplo para entender la pregunta anterior.

class Point {
    public int X { get; set; }
    public int Y { get; set; }

    public static readonly Zero;

    public Point(int x, int y) {
        X = x;
        Y = y;
    }

    static Point() {
        Zero = new Point(0, 0);
    }
}

class Line {
    public Point Start { get; set; }
    public Point End { get; set; }
    
    public Line(Point start, Point end) {
        Start = start;
        End = end;
    }
}

class Circle {
    public Point Center { get; set; }
    public int Radius { get; set; }

    public Circle(Point center, int radius) {
        Center = center;
        Radios = radios;
    }
}

Bien, tenemos unas tres clases que representan objetos en un plano euclídeo: un punto, una línea y un círculo. Dado que son figuras, podríamos pensar que existe una clase base, llamada Shape, y que ésta puede agrupar comportamientos comunes. Pongamos por ejemplo que queremos tener un mecanismo para comparar dos objetos y saber si son iguales. Podemos crear un método, llamado IsEqualTo, que tome como parámetro un Shape y regrese true cuando son iguales y false en caso contrario.

class Shape {
    public abstract bool IsEqualTo(Shape other);
}

class Point : Shape {
    …
    public override bool IsEqualTo(Shape other) {
        bool equals = false;
        Point pt = other as Point;
        if (pt != null) {
            equals = X == pt.X && Y == pt.Y;
        }
        return equals;
    }
}

class Line {
    …
    public override bool IsEqualTo(Shape other) {
        bool equals = false;
        Line ln = other as Line;
        if (ln != null) {
            equals = Start.IsEqualTo(ln.Start)
                  && End.IsEqualTo(ln.End);
        }
        return equals;
    }

}

class Circle {
    …
    public override bool IsEqualTo(Shape other) {
        bool equals = false;
        Circle cc = other as Circle;
        if (cc != null) {
            equals = Center.IsEqualTo(cc.Center)
                  && Radius == cc.Radius;
        }
        return equals;
    }
}

De esta forma, a través e la jerarquía de clases, hemos asegurado un comportamiento: todas las figuras que hereden de Shape pueden ser comparables entre sí. Gracias a esto, podemos implementar algoritmos genéricos para la clase Shape, como por ejemplo, un método que busque en los elementos de un Array o List de objetos Shape, y que pueda utilizarse para cualquiera de las tres clases.

static class Search {
    public static bool Exists(List<Shape> shapes, Shape shapeToFind) {
        foreach (Shape shape in shapes) {
            if (shape.IsEqualTo(shapeToFind))
                return true;
        }
        return false;
    }
}
…

List<Shape> shapes = new List<Shape>();
shapes.Add(new Point(10, 15));
shapes.Add(new Line(new Point(42, 42), new Point(25, -14)));
shapes.Add(new Circle(new Point(17, 15), 12));
shapes.Add(new Point(42, 42));

bool exists = Search.Exists(new Point(42, 42));

Console.WriteLine("Punto 42, 42 encontrado: {0}", exists);

Sin embargo, esto no es suficiente. Resulta que yo tengo, aparte, una clase Color, que permite establecer el color de una figura (Shape).

class Color
{
    public R { get; set; }
    public G { get; set; }
    public B { get; set; }

}

Evidentemente un Color no es una figura, por lo que no podemos establecer una relación de identidad (y por tanto no podemos heredar de Shape: hacerlo sería un error de diseño). Pero a todas luces, un color también puede ser comparable entre sí:

class Color 
{
    public R { get; set; }
    public G { get; set; }
    public B { get; set; }

    public bool IsEqualTo(Color c)
    {
        bool equals = false;
        if (c != null) 
            equals = R == c.R && G == c.G && B == c.B;

        return equals;
    }

}

Con esto vemos algo esencial: a pesar de las diferencias existentes entre Shape (y sus derivadas) y Color, ambas definen un mismo comportamiento: ambas pueden compararse.

Regresando a mi método de búsqueda… como éste se hizo para Shape, ya nos amolamos. Podríamos hacer que Color herede de Shape, pero no tiene sentido, porque no hay realmente una relación de identidad. Podríamos cambiar al método Exists para que tome un object, y hacer la conversión correspondiente entre Shape, y si falla, luego a Color… el problema de este enfoque es que si tenemos una tercera clase base: Pencil, que también sea comparable, pues ahora tendremos que triplicar el código de Exists. Y de todas formas no sería nada extensible.

Otro enfoque para solucionar el problema sería crear una clase base, Comparable, de las cuales hereden tanto Color como Shape.

class Comparable {
    public abstract bool IsEqualTo(Comparable c);
}

class Shape : Comparable { … }

class Color : Comparable { … }

Esto podría resolver el problema momentáneo. Pero… supongamos que aparte del comportamiento de comparabilidad, queremos añadir cualquier otro comportamiento, que no lo compartan Shape y Color, pero sí Color y Pen? Nuestra jerarquía de clases se convertiría en una sumamente compleja, difícil de gestionar, y en resumen terminaríamos duplicando código y con un mal diseño entre manos. Y encima, sólo podemos tener una clase base, pues la herencia múltiple no está soportada en .NET.

¿Cómo podemos hacerle entonces para definir comportamientos que trasciendan las relaciones de identidad? La respuesta es: mediante contratos.

Un contrato básicamente es la promesa de un comportamiento. Cuando uno hace un contrato comercial: quedamos en venderle a la empresa fulana una cantidad equis de refrescos. Pues bien, hacemos un contrato: ahí se estipula que nosotros entregaremos 200 cajas de refresco, cada caja con 10 refrescos cada una, entregables el próximo lunes. Asimismo, establecemos que el cliente nos pagará $10 pesos por refresco, totalizando $20,000, pagaderos contra entrega. Al firmar ambas partes el contrato, garantizamos que vamos a hacer lo ahí estipulado. ¿Cómo lo hagamos? No importa: yo puedo llevar mis refrescos cargando, puedo contratar a algún camión repartidor, etc., mientras que el cliente puede pagarme en efectivo, con cheque, con tarjeta de crédito, etc. El chiste es que se cumpla lo establecido en el contrato.

Pues bien, cuando hablamos de contratos relacionados con clasificaciones, estamos queriendo decir lo mismo: garantizamos que el contrato va a hacer lo que quedamos.

De alguna manera, los comportamientos de una clase constituyen un contrato en sí mismo. En efecto, garantizan un comportamiento gracias a la relación de identidad existente. Pero estos contratos no trascienden dicha relación. Los contratos, en general, siempre trascenderán las relaciones de identidad, y permiten que clases que se adhieran a un contrato específico garantizan el comportamiento, sin importar cómo lo implemente de forma interna.

En C# y .NET los contratos se definen mediante las interfaces.

Interfaces en C# y .NET

Una interfaz es un contrato, el cual define un conjunto de métodos, propiedades y eventos. Una interfaz no define atributos y los métodos y propiedades son solamente declarativos, no definen un cuerpo en particular; asimismo, tampoco pueden tener modificadores de acceso. Simplemente tipo de dato de retorno y parámetros. Las interfaces se declaran mediante la palabra reservada "interface" en C#. La siguiente interfaz muestra cómo declarar una interfaz con un método, una propiedad y un evento. Nota que el nombre de la interfaz comienza con una letra I. Esto es una convención de codificación impuesto por el .NET Framework, pero está tan extendido que nosotros seguiremos esa convención.

interface IAlgunaInterfaz
{
    void Foo();
    string Goo { get; set; }
    event EventHandler Hoo;
}

Cuando una clase se adhiere al contrato de una interfaz, se dice que la implementa. La implementación de una interfaz se hace similar al de la herencia de una clase, con la salvedad que las interfaces siempre van después de la clase heredada (si existiese; separadas por comas). Además, puede implementarse más de una interfaz en una clase determinada (en cuyo caso se separan por una coma). Por otro lado, al implementar una interfaz, la clase en cuestión tiene que definir TODOS los elementos declarados por la interfaz, o si no recibirá un error de compilación.

interface IComparable {
    bool IsEqualTo(object obj);
}

class Shape : IComparable {
    public abstract bool IsEqualTo(object other);
}

class Color : IComparable {
    public bool IsEqualTo(object other) {
        … 
    }
}

Y entonces ahora sí, nuestro algoritmo de búsqueda podría quedar así:

static class Search {
    public static bool Exists(List<IComparable> objects, object objToFind) {
        foreach (IComparable obj in objects) {
            if (obj.IsEqualTo(objToFind))
                return true;
        }
        return false;
    }
}
…

List<IComparable> objs = new List<IComparable>();
objs.Add(new Point(10, 15));
objs.Add(new Line(new Point(42, 42), new Point(25, -14)));
objs.Add(new Color(255, 0, 255));
objs.Add(new Circle(new Point(17, 15), 12));
objs.Add(new Point(42, 42));
objs.Add(new Color(128, 99, 128));

bool exists = Search.Exists(new Point(42, 42));
Console.WriteLine("Punto 42, 42 encontrado: {0}", exists);
exists = Search.Exists(new Color(128, 99, 128));

Console.WriteLine("Color [128, 99, 128] encontrado: {0}", exists);

Por supuesto, siempre podremos referenciar una instancia de una clase por la interfaz que implementa:

IComparable c = new Point(0, 0);

Pero nunca podremos instanciar una interfaz directamente:

IComparable c = new IComparable(); // error de compilación

Ahora bien, hasta este momento hemos visto cómo una interfaz se implementa en una clase con métodos particulares. Pero ¿qué pasa cuando una clase implementa dos interfaces que tienen un mismo nombre? Digo, se supone que los nombres deben ser representativos, pero pues puede ser que dos nombres sean semánticamente diferentes y queramos hacer la diferencia. Este es el caso.

interface IShape {
    void Draw();
}

interface IControl {
    void Draw();
}

class Circle : IShape {
    public void Draw() { … }
}

class Button : IControl {
    public void Draw() { … }
}

class CircledButton : IControl, IShape {
    Circle circle;
    Button button;

    public void Draw() {
        ???
    }
}

En el ejemplo anterior hemos creado una interfaz para definir el funcionamiento de una figura, y el de un control. Definimos un círculo que implementa IShape, y un Button que implementa IControl. Por supuesto, ahora queremos crear un botón circular, por tanto implementamos IControl e IShape. Sin embargo, al implementar Draw: ¿qué versión ha de llamar, la de Circle o la de Button? Debería llamar una u otra, dependiendo de si quien manda llamar está pensando en un IControl o un IShape… Esto es posible mediante la implementación explícita de una interfaz.

Para hacer la implementación explícita, en contraposición de la implícita que ya hemos visto, lo que hacemos es en el método, propiedad o evento, colocar el nombre de la interfaz, seguida de un punto, seguida de… bueno, mejor un ejemplo:

class CircledButton : IControl, IShape {
    Circle circle;
    Button button;

    void IControl.Draw() {
        button.Draw();
    }

    void IShape.Draw() {
        circle.Draw();
    }
}

Como puedes ver, no hay modificadores. Esto es porque un método/propiedad/evento implementado explícitamente NO PUEDE ser invocado desde la clase, sino que tiene que ser invocado desde la interfaz.

CircledButton cb = new CircledButton();
cb.Draw(); // no compila

IControl b = cb;
b.Draw(); // ok, se invoca a IControl.Draw

IShape c = cb;
c.Draw(); // ok, se invoca a IShape.Draw

Por supuesto, si no nos importa y no queremos diferenciarla, podemos dejar la implementación implícita.

class CircledButton : IControl, IShape {
    Circle circle;
    Button button;

    void Draw() {
        circle.Draw();
        button.Draw();
    }
}

Interfaces genéricas

También cabe la pena recordad que las interfaces pueden ser genéricas, aplicando las mismas reglas que para las clases.

interface IComparable<T> {
    bool IsEqualTo(T other);
}

class Shape : IComparable<Shape> {
    public bool IsEqualTo(Shape other) { … }
    ...
}  

class Color : IComparable<Color> {
    public bool IsEqualTo(Color other) { … }
    ...

}  

Ahora bien, las interfaces genéricas son sujetas a un concepto importante de herencia que en .NET se implementó hasta la versión 4.0: la covarianza y contravarianza. Ciertamente esta entrada no entrará a fondo en ambos temas, dado que es uno extenso. Sin embargo, echémosle un vistazo.

Cuando en algún elemento genérico definimos un parámetro genérico T, este puede aceptar tres tipos de parámetros cuando se instancia:

1.- El tipo de dato T tal cual. Esto es lo que hacemos desde .NET 1.0.

2.- Se convierte entre T y tipos más especializados (i.e. clases derivadas).

3.- Se convierte entre T y tipos más generales (i.e. clases base).

Pues bien, al punto 2 se le llama covarianza, y al punto 3, contravarianza. Para declarar un parámetro genérico como covariante, se le añade la palabra reservada out. Para declararlo como contravariante, se le añade la palabra reservada in.

class Clase<T> { … } // ni uno ni otro
class ClaseCV<in T> { … } // contravariante
class ClaseCO<out T> { … } // covariante

// ejemplo de covarianza
List<ClaseCO<object>> co = new List<ClaseCO<object>>();
co.Add(new ClaseCO<object>()); // ok
co.Add(new ClaseCO<string>()); // ok, porque ClaseCO es covariante y string 
                                // deriva de object. 

List<ClaseCO<string>> co = new List<ClaseCO<string>>();
co.Add(new ClaseCO<string>()); // ok
co.Add(new ClaseCO<object>()); // error, porque ClaseCO es covariante y object 
                               // no derivda de string

// ejemplo de contravarianza
List<ClaseCV<object>> cv = new List<ClaseCV<object>>();
cv.Add(new ClaseCV<object>()); // ok
cv.Add(new ClaseCV<string>()); // error, porque ClaseCV es contravariante y 
                               // string es más especializada que object

List<ClaseCV<string>> cv = new List<ClaseCV<string>>();
cv.Add(new ClaseCV<string>()); // ok
cv.Add(new ClaseCV<object>()); // ok, porque ClaseCV es contravariante y object 

                               // es más genérica que string

Más sobre covarianza y contravarianza en este enlace.

Bueno, pues las interfaces pueden declararse como covariantes o contravariantes de la misma forma. De hecho esto puede ayudarnos con nuestro algoritmo de búsqueda. Lo que necesitamos aquí es declarar al parámetro de nuestra interfaz como covariante.

interface IComparable<out T> {
    bool IsEqualTo(T other);
}

class Shape : IComparable<Shape> {
    public bool IsEqualTo(Shape other) { … }
    ...
}  

class Color : IComparable<Color> {
    public bool IsEqualTo(Color other) { … }
    ...
} 

static class Search {
    public static bool Exists(List<IComparable<object>> objects, 
                              object objToFind) 
    {
        foreach (IComparable<object> obj in objects) {
            if (obj.IsEqualTo(objToFind))
                return true;
        }
        return false;
    }
}
…

List<IComparable<object>> objs = new List<IComparable<object>>();
objs.Add(new Point(10, 15)); // a pesar de que implementa IComparable<Shape>
objs.Add(new Line(new Point(42, 42), new Point(25, -14)));
objs.Add(new Color(255, 0, 255)); // a pesar de que implementa IComparable<Color>
objs.Add(new Circle(new Point(17, 15), 12));
objs.Add(new Point(42, 42));
objs.Add(new Color(128, 99, 128));

bool exists = Search.Exists(new Point(42, 42));
Console.WriteLine("Punto 42, 42 encontrado: {0}", exists);
exists = Search.Exists(new Color(128, 99, 128));
Console.WriteLine("Color [128, 99, 128] encontrado: {0}", exists);

Las interfaces de .NET

Ya que hemos platicado de interfaces, como último tema, me gustaría exponer algunas de las interfaces que ya tiene .NET. ¡En efecto! Hay muchas que son muy utilizadas a lo largo de la plataforma, por lo que es importante conocerlas.

La primera sobre la que quiero hablar es la interfaz IDisposable. Esta interfaz representa un objeto que tiene recursos que han de ser liberados en forma determinista, y que no puede esperar a que el colector de basura los libere. Esta interfaz tiene un solo método, llamado Dispose. En este método debe liberarse los recursos asociados.

Supongamos que tenemos una clase, llamada DataLayer, que contiene un elemento SqlConnection, que representa una conexión a una base de datos de SQL Server. Naturalmente queremos que cuando ya no se ocupe DataLayer, se libere la conexión a la DB. Entonces implementamos IDisposable.

class DataLayer : IDisposable
{
    SqlConnection _cnn;
    …

    void Dispose()
    {
        if (_cnn != null) {
            _cnn.Dispose();
            _cnn = null;
        }
    }
}

DataLayer d1 = new DataLayer();
… // usamos el objeto
d1.Dispose(); // liberamos recursos

DataLayer d2 = null;
try {
    d2 = new DataLayer();
    … // usamos el objeto
} catch {
    …
} finally {
    if (d2 != null)
        d2.Dispose();
}

using (DataLayer d3 = new DataLayer())
{
    … // usamos el objeto
} // no necesitamos llamar a Dispose

DataLayer d4 = new DataLayer();
using (d4)
{
    … // usamos el objeto
} // no necesitamos llamar a Dispose

Hemos puesto cuatro ejemplos. El primero muestra cómo invocar Dispose de forma directa, y el segundo, dentro de un bloque try-catch-finally. Los siguientes dos son especiales, pues utilizan la palabra reservada using. Esta palabra acepta (dentro de los paréntesis) un objeto que debe implementar IDisposable. Si no implementa IDisposable, la sentencia causa un error de compilación. La sentencia se encarga de llamar a Dispose cuando se alcanza la llave de cierre del bloque. Si surgiese una excepción, el bloque using se encarga de llamar a Dispose antes de lanzar la excepción, similar al bloque try-catch-finally. Por tanto, no es necesario llamar a Dispose manualmente.

Por cierto, la llamada a Dispose nunca debería lanzar una excepción.

Vamos con la siguiente interfaz. En los ejemplos que mostramos hace rato, creábamos una interfaz llamada IComparable. Pues bien, esta interfaz ya existe y tiene el mismo nombre: IComparable. Esta interfaz tiene un método: CompareTo. Este método debe regresar -1 cuando el objeto actual sea semánticamente menor que el objeto a comparar, 0 si son iguales, y 1 si el otro es mayor. En el caso de variables numéricas, es claro. Pero por ejemplo, una cadena de texto implementa el mayor y menor respecto al orden alfabético de sus letras.

class Point : IComparable {
    public int X { get; set; }
    public int Y { get; set; }

    public int CompareTo(object other)
    {
        Point pt = other as Point;
        if (pt == null)
            throw new ArgumentException("other no es un Point");

        int val;
        if (X < pt.X)
            val = -1;
        else if (X > pt.X)
            val = 1;
        else if (Y < pt.Y)
            val = -1;
        else if (Y > pt.Y)
            val = 1;
        else
            val = 0;

        return val;
    }
}

La interfaz IComparable se utiliza mucho en algoritmos para ordenar colecciones de datos. Por tanto, es importante implementarla en nuestros tipos básicos.

Otra interfaz importante es muy similar a nuestra buena IComparable. Se llama: IEquatable<T>. Esta interfaz tiene un método, Equals(T t), que indica si la instancia actual es igual a alguna otra insatncia. En otras palabras, que Equals regrese true equivaldría a que CompareTo regresara 0.

class Point : IEquatable<Point> 
{
    public int X { get; set; }
    public int Y { get; set; }

    public bool Equals(Point other)
    {
        bool equals = false;
        if (other != null)
        {
            equals = X == other.X && Y == other.Y;
        }

        return equals;
    }
}

Una interfaz clásica también lo es ICloneable. Esta interfaz define un método, Clone, cuya finalidad consiste en crear una copia idéntica del objeto actual.

class Point : ICloneable
{
    public int X { get; set; }
    public int Y { get; set; }

    public Point Clone() {
        return new Point { X = this.X, Y = this.Y };
    }

    object ICloneable.Clone() {
        return new Point { X = this.X, Y = this.Y };
    }
}

Nota que aquí implementamos dos Clones, uno de ellos explícitamente. Esto, porque el explícito regresa un object. El otro, regresa un Point fuertemente tipado. Así cumplimos con la interfaz, pero la clase recibe un valor tipado (Point).

La última interfaz que es súper utilizada es IEnumerable (y su variante genérica IEnumerable<T>). Esta interfaz expone un enumerador. Afortunadamente, ya he hablado de ella en otra entrada, así que hasta aquí la dejamos.

También hay otras interfaces interesantes, que poco a poco iremos explorando. Pero por el momento, creo que son las más importantes. ¡Sigue explorando!

Conclusiones

Esta entrada fue larga. Comenzamos hablando de las clasificaciones del mundo, y poco a poco fuimos introduciendo el tema desde el punto de vista del diseño de software. Pusimos varios ejemplos sobre cómo clasificar, y vimos el concepto de relación de identidad. Una vez dejada clara la teoría, vimos cómo se implementa la clasificación y relación de identidad en C# y .NET, y cómo podemos crear una jerarquía de clases.

Posteriormente, vimos el concepto de comportamiento y contratos, y cómo se implementan en C# mediante el concepto de interfaces. Exploramos varios conceptos relacionados, como interfaces explícitas y covarianza/contrvarianza, y finalmente vimos algunas interfaces comunes en .NET.

Ha sido larguito, espero que haya valido la pena. ¡No dudes en dejar tus preguntas!

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

Reflexión: diagnóstico y creación de tipos en tiempo de ejecución


Recuerdo que cuando programaba con C++, en una ocasión tuve que crear un mecanismo para admitir plug-ins y add-ons a un programa. Mientras lo hacía, lo primero que se me ocurrió fue que me gustaría poder obtener todas las clases existentes en una DLL y poder instanciarla a partir de una cadena de texto. De esta forma, mis motor de plug-ins se limitaría a abrir una DLL y leer un archivo de configuración y ya. Pero por supuesto, eso no puede hacerse con C++.

Sin embargo, es una característica que sí está presente en .NET. De hecho es parte vital de la misma, no nada más crear tipos en tiempo de ejecución, sino el poder analizar un objeto determinado: conocer sus métodos, sus propiedades, sus eventos, e incluso invocarlos. A esta capacidad de introspección, en .NET, se le conoce con el nombre de Reflexión (Reflection).

Arquitectura de un ensamblado

Antes que nada debemos conocer cómo se compone un ensamblado. El ensamblado es la unidad más alta que existe para agrupar componentes y clases. Un ensamblado puede ser un archivo ejecutable o una DLL. Los ensamblados contienen módulos (que en C# existe uno sólo y está oculto, pues el lenguaje no soporta esa característica; sin embargo VB.NET sí que la soporta), y espacios de nombres, los cuales contienen clases. Las clases tienen métodos, propiedades, constructores, atributos, eventos, atributos y otros tipos de datos anidados (como otras clases o delegados). Finalmente, los métodos y constructores tienen parámetros.

Pues bien, para cada uno de estos casos existen clases que las describen y se encuentran en el espacio de nombres System.Reflection (salvo Type, que se encuentra en el espacio de nombres System).

image

 

1.- Assembly. Representa un ensamblado. Contiene módulos, espacios de nombres y los tipos de datos.

2.- Module. Representa un módulo. En el caso de C#, sólo existe un módulo que contiene todas las clases.

3.- ConstructorInfo. Representa información de un constructor, con sus parámetros y los modificadores de acceso.

4.- MethodInfo. Representa un método, y tiene información sobre su firma, modificadores de acceso, tipo de retorno, parámetros y modificadores de herencia como si es abstracto (abstract), si es virtual (virtual) o si es oculto (new).

5.- FieldInfo. Representa un atributo en una clase, es decir, una variable declarada en la clase. Informa los modificadores de acceso y si el atributo es estático o no.

6.- EventInfo. Detecta información sobre eventos, como su nombre, tipo de delegado, atributos, etc.

7.- PropertyInfo. Representa una propiedad, y tiene información sobre su tipo, sus modificadores de acceso, sus modificadores de herencia (abstract, virtual, new, etc.) y sobre todo, información sobre sus accesores (getter y setter).

8.- ParameterInfo. Detecta información sobr atributos: su tipo, si el parámetro es de entrada o de salida, si se pasa por valor o referencia, y la posición ordinal dentro de la firma de un método.

Todo comienza con un tipo

Después del vistazo que echamos anteriormente, necesitamos saber cómo obtener objetos de cada una de esas clases. Hay dos formas de hacerlo: mediante la información del ensamblado (clase Assembly), o bien mediante un tipo de dato (clase Type). La forma más fácil es esta última.

Este método que veremos a continuación nos sirve para obtener la información de reflexión de un tipo determinado que conozcamos. Lo primero que necesitamos es una clase:

using System;
using System.ComponentModel;
… 

namespace Calendars
{
  public class Meeting : INotifyPropertyChanged
  {
    private string _title;
    private string _description;
    private DateTime _start;
	private DateTime _end;
	
	public Meeting() { … }
	public Meeting(string title) { … }

    public DateTime Start { … }
    public DateTime End { … }
    public string Title { … }
    public string Description { … }

    public event PropertyChangedEventHandler PropertyChanged;

    protected OnPropertyChanged(PropertyChangedEventArgs args) { … }

    public TimeSpan CalcDuration() { … }
    public TimeSpan CalcRemainder() { … }
  }
}

Creo que esta clase servirá para nuestros ejemplos. Ahora sí, lo que queremos es obtener la descripción reflejada de esta clase. Lo primero que necesitamos es obtener el objeto Type para nuestra clase Meeting. ¿Cómo le hacemos?

Recordemos que en .NET, todas las clases tienen un antecesor común: System.Object. Es decir, todas implementan Object como clase base. Object cuenta con varios métodos: dos métodos estáticos, Equals y ReferenceEquals; dos métodos protegidos, Finalize y MemberwiseClone; y cuatro métodos públicos, Equals, GetType, GetHashCode y ToString. Y como seguramente ya adivinaste, el método que nos interesa es GetType: en efecto, este método nos devuelve un objeto de tipo Type para una instancia en particular. Así, si tenemos una insancia cualquiera, basta invocar GetType, pues todos los objetos lo implementan.

Type t;

int i = 5;
t = i.GetType();
Console.WriteLine(t); // imprime 'System.Int32'

string s = "hola mundo";
t = s.GetType();
Console.WriteLine(t); // imprime 'System.String'

Meeting m = new Meeting();
t = m.GetType();
Console.WriteLine(t); // imprime 'Calendars.Meeting'

Ahora bien, GetType sirve para cuando tenemos una instancia. ¿Y si no tenemos una instancia? Podemos utilizar el operador typeof de C#. Este operador recibe el nombre de un tipo de dato y regresa el objeto Type correspondiente.

Type t;

t = typeof(int);
Console.WriteLine(t); // imprime 'System.Int32'

t = typeof(String);
Console.WriteLine(t); // imprime 'System.String'

t = typeof(Meeting);
Console.WriteLine(t); // imprime 'Calendars.Meeting'

Ya que tenemos el objeto Type la vida es sencilla. Pero no es la única forma de obtener un Type…

Explorando los ensamblados

Los dos métodos anteriores funcionan cuando ya sabemos el tipo de dato. Pero ¿qué pasa si no sabemos de antemano el tipo? Digamos que quiero ver todas las clases existentes en una DLL determinada. Digamos, en la propia DLL. ¿Qué hacer?

En este caso necesitamos obtener un objeto Assembly. ¿Por qué? Porque la clase Assembly tiene, entre otras maravillas, un método llamado GetTypes, el cuál nos regresa un arreglo de objetos Type con todos los tipos existentes, declarados dentro de ese ensamblado. Y eso es lo que queremos, ¿no? Al final nuestra meta es llegar a obtener un objeto Type.

Bien, pues comencemos a explorar los métodos. En primer lugar, Assembly define un método estático llamado GetAssembly, el cual devuelve un objeto Assembly que referencia el ensamblado actual. Pero también hay otras opciones: GetExecutingAssembly devuelve la referencia al ensamblado del ejecutable actual (si es invocado desde una clase que pertenece a un exe, entonces GetAssembly y GetExecutingAssembly devolverán la misma referencia; si no, GetExecutingAssembly devuelve la referencia al archivo ejecutable que a su ves referencia al ensamblado actual); GetEntryAssembly devuelve la referencia al ensamblado donde comenzó la ejecución del proceso actual (salvo en contadas ocasiones, el ensamblado donde se encuentra el método estático Main); y finalmente GetCallingAssembly, que devuelve la referencia al ensamblado del método que invocó el método actual. Bueno, un relajito.

using System;
using System.Reflection;
…

Assembly assembly = Assembly.GetAssembly();
Type[] types = assembly.GetTypes();
foreach (Type type in types)
    Console.WriteLine(t);

El código anterior obtiene el ensamblado actual e itera los tipos que existen en éste.

Información de tipos de datos

Bueno, ya vimos cómo obtener nuestro objeto Type, ahora veamos qué podemos hacer con él. De entrada, el objeto Type nos permite describir un tipo de dato mediante varias propiedades, destacando las siguientes.

  • Assembly. Obtiene el objeto Assembly al que pertenece el tipo de dato.
  • AssemblyQualifiedName. Obtiene el nombre calificado del ensamblado. Por ejemplo, "MiEnsamblado, Version=1.0.0.0, Culture=neutral, PublicKeyToken=xxxxxxxxxxxx".
  • BaseType. Representa un objeto Type de la clase padre, o null si la clase padre es Object.
    Type type = typeof(ArgumentNullException);
    while (type != null)
    {
        Console.WriteLine("-> {0}", type);
        type = type.BaseType;
    }
    Console.WriteLine(" + {0}", typeof(object));
    	
    /* El código anterior imprime: 
    
    -> System.ArgumentNullException
    -> System.ArgumentException 
    -> System.SystemException
    -> System.Exception
     + System.Object
    */
  • FullName. Representa el nombre calificado del tipo (con su espacio de nombres). Por ejemplo: System.Reflection.MemberInfo.
  • GenericTypeArguments. Regresa un arreglo de objetos Type que representa el tipo de los parámetros genéricos.
  • IsAbstract. Pues eso, si el tipo es abstracto o no.
  • IsArray. Verdadero si el tipo es en realidad un arreglo.
  • IsClass. Verdadero si el objeto es una clase, falso si es otro elemento como enumeraciones, estructuras, interfaces, etc.
  • IsEnum. Verdadero si el objeto es una enumeración.
  • IsInterface. Verdadero si el objeto es una interfaz.

Después de obtener información sobre un tipo de dato, nos va a interesar obtener información sobre sus miembros. El método GetMembers regresa un arreglo (de tipo MemberInfo) con todos los miembros de la clase. Estos pueden ser propiedades, constructores, métodos, eventos y todos los objetos que describimos en la primera sección. GetMembers tiene una sobrecarga que permite indicar el tipo de elementos a enumerar: instancias, estáticos, públicos, no-públicos, etc.

Type type = typeof(Meeting);
MemberInfo[] mems = type.GetMembers(BindingFlags.Public | BindingFlags.Instance);
foreach (MemberInfo mem in mems)
{
    Console.WriteLine("{0} - {1}", 
        mem.MemberType,
        mem.Name);
}

/* imprime: 
   Constructor - ctor
   Constructor - ctor'1
   Property - Start
   Property - End
   Property - Title
   Property - Description
   Event - PropertyChanged
   Method - OnPropertyChanged
   Method - CalcDuration
   Method - CalcRemainder
   Method - Equals
   Method - GetHashCode
   Method - GetType
   Method - ToString
*/

En el ejemplo anterior no sólo vimos cómo enumerarlos, sino que también vimos que MemberInfo tiene una propiedad, llamada MemberType, de tipo MemberTypes, que permite saber si el miembro es un constructor, propiedad, evento, atributo, etc. Con base en el valor de esta propiedad, es posible hacer una conversión de MemberInfo al tipo propio de cada miembro: ConstructorInfo, MethodInfo, PropertyInfo, FieldInfo, EventInfo, etc. Todas estas clases heredan directamente de MemberInfo, por lo que la conversión es aceptable.

Ahora, en lugar de invocar a Type.GetMembers, puedes también invocar métodos directos:

  • GetConstructor te permite obtener un objeto ConstructorInfo: el constructor cuya firma coincida con el arreglo que se le pasa como parámetro; Get Constructors te permite obtener todos los constructores (i.e. un arreglo de ConstructorInfo).
  • GetEvent permite obtener un objeto EventInfo pasándole como parámetro el nombre del mismo; GetEvents regresa un arreglo de EventInfo con todos los eventos del tipo.
  • GetField regresa el FieldInfo, esto es, la información de un atributo, dependiendo del nombre que se pase como parámetro; o bien GetFields regresa el arreglo con todos los atributos del tipo.
  • GetMethod regresa el descriptor de un método, MethodInfo, a partir del nombre del método y del tipo de parámetros que se pasen; o bien GetMethods regresa todos los métodos del tipo.
  • GetProperty regresa un PropertyInfo con la información de la propiedad cuyo nombre se pasa como parámetro, o bien GetProperties regresa todas las propiedades.

Y así sucesivamente. Existen algunos otros miembros de Type que nos ayudan a conocer más sobre la clase. Por ejemplo, GetInterfaces te da una lista con información sobre las interfaces que el tipo implementa, o GetGenericArguments, que devuelve el tipo de datos de los parámetros para tipos genéricos.

Instanciando e invocando

Ahora viene la parte interesante. A partir de objetos Type, de objetos MemberInfo y derivados, etc., podemos crear tipos e invocar métodos, propiedades y otros objetos. Veamos cómo se hace.

Type type = typeof(Meeting);
ConstructorInfo ctor = type.GetConstructor(new Type[] { });
Object meeting = ctor.Invoke(new Object[] { });

Como puedes ver, instanciamos un tipo Meeting al invocar a su constructor. Para ello obtenemos el constructor que no tiene parámetros (el array Type está vacío), y luego usamos el método Invoke para invocarlo, también sin ningún parámetro (el array Object está vacío). Veamos cómo invocar el constructor que tiene un parámetro.

Type type = typeof(Meeting);
Type[] paramTypes = new Type[] { typeof(string) };
ConstructorInfo ctor = type.GetConstructor(paramTypes);
Object paramObjs = new Object[] { "Junta de revisión" };

Object meeting = ctor.Invoke(paramObjs);

En ambos casos, la variable meeting cuenta ya con una instancia válida de la clase Meeting. Ahora lo que vamos a hacer es invocar alguna de sus propiedades, para establecer los valores de Start y End. Asumamos que nuestra variable obj ya tiene una instancia válida de Meeting, según vimos en el ejemplo anterior.

Type type = typeof(Meeting);
Type[] paramTypes = new Type[] { typeof(string) };
ConstructorInfo ctor = type.GetConstructor(paramTypes);
Object paramObjs = new Object[] { "Junta de revisión" };
Object meeting = ctor.Invoke(paramObjs);

PropertyInfo propStart = type.GetProperty("Start", BindingFlags.Instance);
MethodInfo propStartSet = propStart.GetSetMethod();
propStartSet.Invoke(meeting, new DateTime(2012, 11, 20));

PropertyInfo propEnd = type.GetProperty("End", BindingFlags.Instance);
propEnd.SetValue(meeting, new DateTime(2012, 11, 21);

¡Jolines! Lo interesante comienza en el segundo párrafo, pues el anterior ya lo conocemos.

En efecto, vemos que en éste, obtenemos el PropertyInfo relacionado con la propiedad Start. Sabemos, sin embargo, que cada propiedad tiene dos métodos: un getter y un setter. De ahí que mandemos llamar GetSetMethod, para obtener el MethodInfo del setter (para obtener el MethodInfo del getter, usamos GetGetMethod). Por último, llamamos al método Invoke del MethodInfo para invocar el setter. El primer parámetro es el objeto sobre el cuál ejecutaremos el método (en este caso, meeting), y el segundo objeto es un array con los parámetros. Los setters sólo tienen un parámetro, que es lo que pasamos en el array.

Luego pasamos a modificar la propiedad End. Hacemos algo parecido, pero en lugar de obtener el MethodInfo del setter, llamamos al método SetValue, que hace todo por nosotros. Quise mostrar ambas formas porque el SetValue está disponible sólo para .NET 4.5 en adelante. Si estás con .NET 4.0 o anterior, tendrás que hacerlo de la primera forma.

Tras lo que hemos visto, invocar a los métodos es easy peasy: obtenemos el MethodInfo y luego llamamos al método Invoke.

MethodInfo method = type.GetMethod("CalcDuration", BindingFlags.Instance);
DateTime duration = (DateTime)method.Invoke(meeting, new Object[] { });

Type[] signature = new Type[] { typeof(DateTime) };
method = type.GetMethod("CalcRemainder", signature);
DateTime remainder = (DateTime)method.Invoke(meeting, 
                         new Object[] { DateTime.Now });

El primer ejemplo es muy directo: obtenemos el MethodInfo y luego llamamos a Invoke. El segundo párrafo tiene una particularidad. Cuando queremos obtener un método que tiene parámetros, tenemos que pasar el tipo de parámetros como un array de tipos. Por ejemplo, si queremos obtener el MethodInfo del siguiente método:

void foo(int a, string b, Meeting c, DateTime d);

Tendríamos que invocar al GetMethod de estas forma:

Type[] signature = new Type[] {
    typeof(int),
    typeof(string),
    typeof(Meeting),
    typeof(DateTime)
};

method = type.GetMethod("foo", signature);

El array de Type debe tener los tipos de cada uno de los parámetros. De esta forma puede identificarse al método correcto cuando éste se encuentra sobrecargado.

Otro ejemplo interesante consiste en obtener los atributos (campos) mediante GetField, que nos regresa un FieldInfo. Nota que puedes acceder a atributos privados y protegidos, siempre que tengas permiso. Si no lo tienes, te verás con un SecurityException entre manos.

FieldInfo field = type.GetField("GenericTitle", 
     BindingFlags.Public | BindingFlags.Static);
String value = (String)field.GetValue(meeting); 
     // meeting es ignorado porque es un atributo estático
if (!field.IsInitOnly)
    field.SetValue(meeting, "Hola mundo!"); 
     // meeting es ignorado porque es un atributo estático

Un poquito más interesante. GetValue y SetValue obtienen y establecen el valor del atributo. La propiedad IsInitOnly nos indica si el atributo es de sólo lectura o si puede ser cambiado posteriormente.

Sobre los eventos: podemos subscribir manejadores o eliminarlos mediante EventInfo. Lo que no podemos hacer es invocar al evento, eso sólo lo hace el propio objeto.

EventInfo evnt = type.GetEvent("PropertyChanged");
Delegate del = (s, a) => { Console.WriteLine("Propiedad cambiada!"); };
evnt.AddEventHandler(meeting, del);

Aquí el meollo es obtener el EventInfo y luego llamar a AddEventHandler.

 

Seguridad

Hasta ahora hemos evitado hablar sobre la seguridad asociada a Reflection. Sin duda debe haber algo, pues ¿cómo cualquiera va a poder ejecutar el MethodInfo.Invoke de algún método público o privado? ¡Eso rompería el encapsulamiento!

En primera instancia, cabe decir que el uso de Reflection para obtener información sobre tipos y miembros NO ESTÁ RESTRINGIDA. Todo código puede usar Reflection para enumerar tipos y sus miembros, y enumerar ensamblados y módulos.

Sin embargo, acceder a los miembros: establecer valores, leerlos, invocar métodos, subscribir eventos, etc., es otra historia. En general, sólo código que esté marcado como confiable puede usar Reflection para acceder a miembros con seguridad crítica.

Por último, para dar permisos especiales de un ensamblado a otros que quieran inspeccionarlo vía Reflection, deberán darle un ReflectionPermission con la bandera ReflectionPermissionFlagMemberAccess

¿Qué sigue?

En esta entrada hemos visto cómo utilizar la reflexión para inspeccionar ensamblados y buscar clases y tipos de datos. Hemos visto cómo obtener descriptores de métodos, propiedades, eventos, constructores, etc. Hemos visto cómo instanciar un tipo y cómo invocar métodos y establecer valores en atributos y propiedades.

No hemos visto, sin embargo, un concepto importante, que es el de los atributos de meta-información. Ahm… en .NET, a los atributos se les conoce también como "campos" (fields), pues existe aparte el concepto de atributos, que derivan de la clase System.Attribute. Estos atributos aportan información adicional sobre clases, métodos, propiedades, etc., y pueden accederse vía Reflection. Este tema es tan abundante que será motivo de una entrada propia.

Aparte de lo anterior, otro paso que sigue es el de generación de código. En efecto, ¡podemos generar código ejecutable a partir de Reflection! También podemos apoyarnos en CodeDom para crear nuestros propios lenguajes y compiladores. CodeDom también será sujeto de su propia entrada.

C.U.L8ER.

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

Conceptos OOP: encapsulación


Uno de los conceptos importantes con los que contamos en la programación orientada a objetos es el de encapsulación. En esta entrada analizaremos un poco a fondo la teoría detrás de la encapsulación, y cómo podemos implementarla en nuestros lenguajes favoritos.

Comencemos por hacer un análisis que nos lleve a motivar la necesidad de la encapsulación.

Estado de objetos


Recordemos un poco algunas definiciones importantes. Un objeto es una instancia de un tipo de dato (i.e. clase) que contiene atributos (es decir, variables internas). Estos atributos determinan el estado de un objeto. ¿Qué quiere decir un estado? Es la combinación de posibles valores de sus atributos internos.

Por ejemplo, pensemos en el interruptor que nos permite encender y apagar una bombilla incandescente (a.k.a. foco). Si yo hago clic en el interruptor, el foco, que está apagado, se enciende. Si vuelvo a hacer clic, el foco se apaga. En este caso, no podemos hacer nada más que un clic, que enciende y apaga. Ahora bien, el foco sólo puede tener dos estados posibles: o está encendido o está apagado (digo, también puede estar fundido, pero consideremos ese un caso excepcional). Entonces decimos que el conjunto de posibles estados de un sistema foco-interruptor sólo puede tener dos estados posibles, y sólo puede haber dos transiciones posibles: de encendido a apagado, y de apagado a encendido. Además, bajo una regla: si está encendido, se apaga, y si está apagado, se enciende.

Pensemos ahora que queremos representar ese sistema con una clase. Tenemos entonces que crear un atributo (i.e. variable interna) que almacene el estado. Dado que sólo puede haber dos estados posibles, podemos usar una variable de tipo booleano. Este tipo de dato permite justamente dos valores: true o false.

¿Por qué usamos un bool y no un entero, por ejemplo? Podríamos hacerlo, y podríamos decir que un 0 representa "apagado", y un "1" o cualquier otro valor, sea el "encendido". Nos hace falta ahora representar las transiciones. En realidad sólo tenemos una transición: Click, que dependiendo del estado cambia entre encendido o apagado.


El diagrama anterior ilustra los posibles estados del objeto. Vemos que las dos transiciones son en realidad la misma, y que ésta varía dependiendo del estado actual.

Ahora bien, imagínate que un buen día decides cambiar tu sistema por uno de esos focos que permiten tener varios grados de luminosidad. Digamos, el foco puede estar apagado, pero cuando está encendido puede estar en tres modalidades: penumbra, normal y brillante. Entonces tenemos cuatro estados posibles. Pero ¿y cómo se hace la transición? Si el interruptor es una media perilla que recorres de izquierda a derecha, con el izquierdo el estado apagado y el derecho el estado brillante, entonces sólo podemos hacer estas transiciones: apagado a penumbra y viceversa, penumbra a normal y viceversa, normal y brillante y viceversa.

image

 

La cosa se comienza a complicar. Tenemos cuatro estados con seis transiciones. De éstas últimas, se agrupan en tres que aumentan el estado, y tres que lo disminuyen. Al implementarse, probablemente las transiciones sean dos métodos, y los estados una enumeración con las cuatro opciones.

Este interruptor podemos volverlo tan complejo como queramos. Por ejemplo, si el interruptor tiene sensores para detectar comandos de voz, tendríamos seis nuevas transiciones: una para mover la perilla y otra para el comando de voz. Y si pensamos en una perilla entera, en donde el número de grados representa un porcentaje entre la ausencia de luz y el 100% de luminosidad de la bombilla, ya entonces tenemos un estado no dictado por una enumeración, sino por un atributo de tipo entero corto (16 bits en .NET), con un rango que va de 0 a 100 (cada unidad representando el porcentaje deseado). Así, nuestra máquina de estados se vuelve mucho más compleja, pues tiene 101 estados válidos (contando al cero), peeeero ¡65435 estados inválidos! ¿Por qué? Pues porque un entero corto en .NET tiene 16 bits, esto es, puede almacenar 65536 valores (de -32767 a 32768). ¿Notas el problema?

Imaginemos que modelamos los interruptores anteriores como clases o estructuras de C#. Sin contar las transiciones, sino los puros estados, tendríamos algo así.

struct SimpleSwitch {
    bool on;
}

enum DimSwitchState {
    Off, Gloom, Normal, Bright
}


struct DimSwitch {
    DimSwitchState state;
}

struct FullSwitch {
    short brightness;
}

 

Más o menos. Si queremos usar SimpleSwitch, sólo podemos asignarle dos posibles valores, y ambos valores son totalmente válidos. Esto quiere decir que SimpleSwitch nunca puede estar en un estado inválido. Por tanto, podemos estar seguros que esta estructura no nos generará problemas o bugs.

En el caso de DimSwitch, la cosa varía. La enumeración hace un muy buen trabajo al limitar el posible número de estados para la estructura. En este sentido, estableciendo la estructura a Off, Gloom, Normal o Bright tiene el mismo efecto que establecer los valores true o false: el objeto siempre estará en un estado válido. Sin embargo, a diferencia de un bool, las enumeraciones pueden convertirse a partir de valores numéricos. ¡Zaz!

// nada puede malir sal
SimpleSwitch s1;
s1.on = true;
s1.on = false;
s1.on = 42; // ¡no compila!

// hum…
DimSwitch s2;
s2.state = DimSwitchState.Off; // ok
s2.state = DimSwitchState.Gloom; // ok
s2.state = DimSwitchState.Bright; // ok
s2.state = DimSwitchState.Normal; // ok

// so far so good…
s2.state = (DimSwitchState)42; // ¡madres!

 

 

Y valió queso. El idiota programador convirtió un valor numérico cualquiera en la enumeración. Es decir, introduje un valor inválido. Y ahora mi objeto s2 se ha corrompido, y seguramente eso causará errores y bugs más adelante.

El caso anterior es extremo, por supuesto. Ningún programador en su sano juicio haría algo así. Pero sí pueden ocurrir situaciones similares. Por ejemplo, si ese valor se guardara en alguna base de datos, tendría que guardarse en formato numérico. En teoría, nunca se guardaría un valor inválido, pero algún DBA no muy brillante podría actualizar la tabla y meter un 42 por ahí. Entonces ya nos veríamos con este mismo escenario. ¡No muy recomendable!

Hay formas de paliarlo, sin embargo. Por ejemplo, limitando acceso a DBAs que no saben lo que hacen. O bien, podemos hacer uso de el método Enum.IsDefined, al cual le pasamos un valor numérico que nos dice si el valor está definido o no. Algo así.

var value = 42;

if (Enum.IsDefined(value))
    s2.state = (DimSwitchState)value;
else
    throw InvalidOperationException("Enumeración con valor inválido.");

 

 

Y el mundo vuelve a la normalidad. Sin embargo, ya vimos que es necesario siempre realizar un cierto tipo de validación. ¡Esto es dramáticamente cierto para el siguiente ejemplo!

 

FullSwitch s3;
s3.brightness = 50; // ok
s3.brightness += 25; // ok, estamos en 75
s3.brightness += 30; // madres, ya s3 es inválido
s3.brightness = -10; // otra vez, es inválido
s3.brightness = 55;
s3.brightness *= 2; // otra vez inválido



 

Y así podríamos seguirnos. La cantidad de valores inválidos es enorme. Pero más aún, hay muchas operaciones que pueden provocar valores inválidos. Si no tenemos cuidado, esta estructura luce como fuente inagotable de bugs. Por supuesto, hay una forma muy sencilla de evitar caer en estado inválido.

 

var value = -10;
if (value >= 0 && value <= 100)
    s3.brightness = value;
else
    throw new InvalidOperationException("El valor no está en un rango válido.");

 

Y nuevamente hemos paliado la situación, aunque hemos de decir que el riesgo latente siempre estará presente.

Una motivación



 

Estos escenarios que hemos analizado son realmente sencillos. En efecto, una clase con un solo atributo no es nada. Las clases que escribimos en la vida real suelen tener decenas de atributos. Las combinaciones son catastróficas. Una clase con doubles, decimals, strings y longs tienen millones de posibles estados. Y el asunto es combinatorio: si tienes una clase con dos bools, entonces tienes cuatro posibles estados. Y así sucesivamente.

Por supuesto, el asunto no es tan catastrófico. No todos los atributos tienen rangos restringidos. Pero la causa de muchos bugs es que aquellos datos que sí lo están, no se le pone la suficiente atención para prevenir los estados inválidos. Clásico: el nombre de un empleado está limitado a 255 caracteres en una base de datos, y tu pones un string, que puede almacenar mucho más que 255 caracteres. Si no validas, ¡pum! Un SqlException seguro al querer guardar en la DB. O tienes datos que no hacen sentido. En alguna ocasión, en un sistema, vi que podía meter mi edad con un valor negativo. O puedes poner en riesgo la seguridad del sistema. En efecto, cuando actualizamos a base de datos, es de todos sabido que hay ciertos caracteres que no deben utilizase, como la comilla simple, pues puede provocar SQL Injection. Ese es un ejemplo de un estado restringido que no validamos.

El problema de los estados inválidos de objetos es muy grande. Tanto, que han surgido paradigmas y lenguajes que ¡no permiten que los objetos cambien de estado! Es decir, una vez que un objeto se crea, y por tanto, tiene un único estado, ya no puede cambiarse. Puede crearse un nuevo objeto a partir del anterior, pero no cambiarse. Un ejemplo de esto son los lenguajes funcionales, como Ocaml, Haskell o el mismo F# de .NET. Vaya, C# y .NET tienen algunas clases inmutables. La clase String es quizás un ejemplo claro. Una vez que creas el objeto, no hay método que lo modifique. Métodos como Append o Trim devuelven un objeto nuevo. ¿Qué tal?

Pero entonces, ante somera preocupación, surge la duda: ¿quién es el encargado de hacerse responsable del estado del objeto?

No es pregunta sencilla de responder. Cuando todos programábamos en C, allá en los 70s, se decía que el encargado era el método que usaba un objeto. Sin embargo, eso creaba ciertas complicaciones: si José creaba la función F1 y modificaba la estructura S1, y luego llegaba Juan y creaba la función F2, que invocaba a F1 y también cambia el estado de S1, y encima llega Pedro y modifica la F1 creada por José e introduce un estado inválido, ya le dio en la mother tanto a F1 como a F2. Estos escenarios intentaron paliarse con documentación, sin mucho éxito que digamos.

Ante esto, alguien sugirió que el dueño del estado debía ser quien crease el objeto. Pero una vez que éste comienza a ser distribuido por funciones, es imposible que el dueño haga un rastreo a cualquier oportunidad. Entonces, ¿qué puede hacerse?

Estas situaciones propiciaron una respuesta diferente: el objeto mismo debería tener un mecanismo para validar su propio estado, al limitar los rangos de posibles valores que recibe. Cuando existía C, no había forma, pero luego se creó C++, que daba la posibilidad de crear métodos sobre clases. Un método es una función que tiene acceso al estado del objeto al que pertenece. Cuando una función cambia algún valor de alguna variable interna, está cambiando el estado del objeto y se dice que es una transición. De esta forma, comenzó a ser posible llegar a códigos como el siguiente.

 

struct FullSwitch
{
    short brightness;

    void SetBrightness(short value)
    {

        if (value < 0 || value > 100)
            throw new InvalidOperationException("Rango inválido. ");

        brightness = value;
    }
}
...

FullSwitch s;
s.SetBrightness(42); // ok
s.SetBrightness(-10); // oops, ¡excepción!

 

De esta forma nos aseguramos que al invocar SetBrightness no tengamos estados inválidos. Tendremos excepciones, que indicarán bugs durante el desarrollo, pero aseguramos que al menos mientras se invoque dicha función, s será un objeto con estado válido.

Sin embargo, a pesar de lo anterior, nada impide que algún sonso haga esto:

FullSwitch s;
s.SetBrightness(42); // ok
s.brightness = -10; // sigh :(

 

And we’re back to square one. Vamos, que no creo que alguien haga eso voluntariamente. Puede hacerse por desconocimiento, por ejemplo. Clases o estructuras con decenas de atributos, decenas de métodos hacen la vida más difícil. Y luego, si tiene decenas de clases que aprenderte… bueno, digamos que puede ser mucho.

Es por eso que comenzó a surgir el siguiente concepto: ocultar los atributos de una clase. Es decir, que estén disponibles para las transiciones de la clase, pero que noooo estén disponibles para actores externos, a menos que sea necesario. El ocultar, o permitir acceso restringido, a los atributos de una clase comenzó a hacerse más y más necesario, y muy pronto lenguajes como C++ comenzaron a implementarlo. A esto se le llamó encapsulación.

Encapsulación en C#



El concepto de encapsulación, como vemos, es viejo. Hoy día todos los lenguajes orientados a objetos lo implementan de una u otra forma: C++, Java, C#, ¡hasta Visual Basic! Sin embargo, los lenguajes dan la posibilidad de NO implementar la encapsulación. Creo que ya hemos dado argumentado suficiente a favor de este concepto. Definitivamente implementar la encapsulación hará que asegures que una clase tenga puros estados válidos, y esto hará que disminuyan los posibles bugs cuando estés creando modelos de datos, capas de negocio, etc.

En particular, C# implementa varios conceptos de encapsulación, e incluso añade algunos conceptos novedosos no presentes en otros lenguajes. Incluso, la encapsulación aplica a miembros de una clase que no sean atributos, pero que de alguna forma puedan modificar el estado de un objeto. Esto incluye métodos, delegados, eventos, etc.

Veamos.

Modificadores de acceso



Comenzamos por los modificadores de acceso. En la sección anterior, veíamos este ejemplo.

FullSwitch s;
s.SetBrightness(42); // ok
s.brightness = -10; // sigh :(



Aquí, vemos que a pesar de tener nuestro método SetBrightness, podemos cambiar el atributo asignándole un valor directamente. Entonces, lo que quisiéramos es que el atributo estuviera disponible para los métodos de la clase, pero no para los externos. En C#, esto es posible con los modificadores de acceso, que tienen los siguientes niveles de acceso.

1.- Acceso privado. Sólo los métodos que estén dentro de la propia clase pueden acceder a estos atributos. Ni siquiera las clases anidadas o clases derivadas pueden hacerlo. Se identifica mediante la palabra reservada "private".

struct FullSwitch {
    private int brightness;

    int Get() { return brightness; } // ok

    void Set(int value) {

        if (value < 0 && value > 100) 
            throw new ArgumentException("Rango inválido.");

        brightness = value; // ok
    }
}

...

FullSwitch s;
s.Set(42); // ok
int i = s.Get(); // ok, i == 42
s.brightness = -10; // ¡no compila!
s.Set(-10); // ok, pero lanza excepción: ¡s nunca queda en estado inválido!

 

2.- Acceso público. Cualquier elemento fuera de la clase en cualquier ubicación podrá ver el atributo, clase, método, delegado, evento, etc. Es el nivel más alto de acceso, y por tanto sólo debe exponerse para miembros que no vayan a dejar en estado inválido nuestra clase. Se identifica mediante la palabra reservada "public".

Por supuesto, nuca querremos declarar públicos nuestros atributos. Esto violaría el principio de encapsulación, y nos daría en la torre por todo lo ya expuesto.

Hay algunos casos, sin embargo, donde se considera válido que un atributo sea público. Esto es para aquellos atributos que no cambian: estáticos de sólo lectura o constantes. Se considera admisible porque al ser constantes o de solo lectura, su estado es siempre inmutable, y por tanto no afectan el estado general del objeto. Algunos ejemplos que implementa el propio .NET Framework.

 

  • String.Empty. Representa una cadena de texto vacía. Es equivalente a poner un par de comillas: "".
  • Int32.MinValue e Int32.MaxValue. Representan los valores mínimos y máximos de un número entero.
  • Uri.UriSchemeHttp. Representa el esquema de un enlace de HTTP. Es decir, la cadena "http://".
  • DBNull.Value. Representa el valor nulo, no asignado, que puede darse a un campo en base de datos.

Como puedes ver, todas esas son inofensivas, desde el punto de vista del estado de sus objetos, y por tanto son aceptables aunque rompan el principio de encapsulación.

3.- Acceso protegido. Los elementos así declarados, mediante la palabra reservada "protected", no están accesibles fuera de la clase, igual que con "private". Pero sí están accesibles dentro de la clase, y además, están accesibles para elementos derivados. Un ejemplo:

class FullSwitch {    
    protected int brightness;

    public int Get() { ... }
    public void Set(int value) { … }
}


class PrintFullSwitch : FullSwitch
{
    public void Print() {
        Console.WriteLine(brightness); // ok!
    }
}

PrintFullSwitch s;
s.Print(); // ok!
s.brightness = 42; // no compila

 

En este ejemplo hemos declarado a brightness como protegida, así que la clase derivada PrintFullSwitch puede acceder a ella, como se vio en el ejemplo.

Hay que tener cuidado, sin embargo, con todo lo protegido. Esto, porque abre la posibilidad de que clases derivadas cambien el estado del objeto. Y en general abrimos la puerta a que alguien derive y comience a causar estragos, si no sabe lo que hace. Por eso, en general, vamos a preferir declarar nuestros atributos como privados, y métodos y demás objetos que acceden a ellos, como protegidos o públicos.

class Switch {
    private int brightness;

    public int Get() { ... }

    protected void Set(int value) { 
        if (value < 0 || value > 100)
            throw new ArgumentException("Rango inválido.");
    }
}


...
   
class FullSwitch {
    public void ChangeValue(int value) {
        Set(value); // ok, si value es inválido, Set se 
        // encarga de lanzar la excepción. 
        ...
    }

    public void ScrewState() {
        Set(-10); // aunque queramos echar a perder el estado,
                      // Set lo impide siempre. 
    }
}

 

En general, deberás tener muy en cuenta hasta qué punto quieres que clases derivadas modifiquen el estado de tu objeto.

4.- Acceso interno. Quiere decir que un miembro es público para el ensamblado que lo contiene, pero es privado fuera de ese ensamblado. Se identifica por la palabra reservada "internal".

Piensa este escenario. Tienes un equipo de trabajo, y entre todos están construyendo un ensamblado para un proyecto. Ese ensamblado contienen clases que pueden ser usadas algún día por algún tercero de otra empresa. En esta situación, te das cuenta que tienes ciertos métodos o atributos que son peligrosos para la integridad del estado de una clase determinada, pero sabes que tus colegas no van a cometer errores porque están entrenados, conocen el proyecto, etc. Sin embargo, te da miedo que el tercero llegue y cometa errores. ¿Qué hacer?

Justo para esto se inventó el "internal". Si declaras como internal un miembro cualquiera, éste estará disponible como si fuera público para cualquier otro objeto dentro del mismo ensamblado (i.e. programa ejecutable o DLL), y sin embargo será privado para cualquier otro ensamblado. Ahora bien, quien puede crear objetos desde el mismo ensamblado es quien tiene el código fuente: tú o tu equipo de trabajo. ¡Entonces un internal resuelve el problema!

 

// switch.cs dentro de Switch.dll
class Switch {
    public void Click() { … }

    internal int brightness;
    ...
}


// foo.cs dentro de Switch.dll
Switch s = new Switch(); {
    s.Click(); // ok
    s.brightness = 42; // ok

// goo.cs dentro de ThirdParty.dll referenciando a Switch.dll
Switch s = new Switch();
s.Click(); // ok porque es público

s.brightness = 42; // ¡error! para este ensamblado es privado.

 

5.- Acceso interno protegido. Un caso muy similar al anterior. Quiere decir que un miembro es protegido para el ensamblado que lo contiene, pero es privado fuera de ese ensamblado. Se identifica con las palabras "protected internal".

Si no quieres hacer "internal" un atributo, por ejemplo, pero sí te gustaría que estuviera protegido para clases derivadas, pero privado para clases que deriven en otros ensamblados, entonces usa protected intenal.

 

// switch.cs dentro de Switch.dll
class Switch {
    protected internal int brightness;
    ...
}


// foo.cs dentro de Switch.dll
class FullSwitch : Switch {
    void Click() { 
    brightness = 42; // ok
}


// goo.cs dentro de ThirdParty.dll referenciando a Switch.dll
class OtherSwith : Switch {
void OtherClick() { 
brightness = 42; // ¡no compila! para este ensamblado es privado
}


}


 

Bueno, con eso cubrimos los cinco modificadores de acceso. No dejes de utilizarlos, puesto que representan la mitad del encapsulamiento existente en C#. La otra mitad corresponde a los accesos.

Accediendo los atributos



Bueno, ya hemos visto que con los modificadores ocultamos los atributos y miembros y sólo exponemos aquellos que sabemos no pueden ser utilizados de mala forma por agentes externos. Ahora queda la pregunta: ¿cómo exponemos los atributos, parcial o totalmente, a los externos? Lo primero que nos viene a la mente son los métodos. Ya hemos visto algunos de ellos, pero pongamos uno que sirva de base para posteriores ejemplos.

class Switch
{
    private bool _turnOn;

    public bool GetTurnOn() { return _turnOn; }

    public void SetTurnOn(bool value) { _turnOn = value; }
}

 

Este es un ejemplo básico. Tenemos un par de funciones que obtienen y establecen el valor del atributo. Estamos respetando las reglas de encapsulamiento a la perfección. De hecho esta forma es muy popular en lenguajes como C++ y Java, tanto que este tipo de métodos tienen sus nombres: métodos "getter" y métodos "setter", precisamente porque siempre se llaman igual que el atributo (TurnOn) pero con el "Get" y "Set" como prefijos.

Utilizar métodos getters y setters no tiene nada de malo, de hecho. Sin embargo, tienen un inconveniente que es más semántico que otra cosa. Un método, por su naturaleza, tiene en potencia la capacidad de alterar el estado del objeto. Así, para un agente externo, invocar un Get o un Set no garantiza que se respete el estado del objeto. En C++ existe una construcción que permiten declarar métodos constantes. Un método constante no puede cambiar atibutos (a menos que el atributo se le declare como variable "mutable"). Ejemplo:

 

class switch() {
    private:
        bool _turnOn;

    public:
        bool get() const { 
            return _turnOn;
        }

        void set(bool value) {
            _turnOn = value;
        }

        void click() const { 
            _turnOn = !_turnOn; // oh-oh!
        }
};



 

 

En la clase anterior de C++, nota que los métodos get y click tienen la palabra "const" después de los paréntesis. Esto quiere decir que esos métodos no pueden cambiar sus atributos. En el caso de get, no pasa nada porque nada más se hace el return. Sin embargo, el método click provoca un error de compilación, puesto que el método es constante y se intenta cambiar un miembro: el atributo _turnOn. En general, se conoce como "const-correctness" esta técnica, y es muy útil para asegurar métodos que no cambien el estado de un objeto.

En C#, este concepto no existe. Sin embargo, se creó algo para ayudar un poco a acercarnos a esta construcción: las propiedades.

Una propiedad en C# se "parece" mucho a un atributo, con la diferencia que las propiedades no generan un espacio en memoria. En contraposición, las propiedades cuentan con un cuerpo, el cual puede tener dos "accesores": un get y un set (o puede tener sólo uno). Cada uno de éstos tiene su propio cuerpo, y en éste podemos poner tareas normales que pondrías en cualquier método. El único requisito es que para los get, debe haber un return, y para los set, uno puede usar la palabra reservada "value" para referir al valor que se está estableciendo.

 

class Switch {
    private bool _turnOn;


    public bool TurnOn {
        get {
            return _turnOn;
        }
        set {
            _turnOn = value;
        }
    }
}


Switch s = new Switch();
s.TurnOn = true;
var val = s.TurnOn;

 

Como puedes ver, es muy similar a tener un atributo público llamado "TurnOn". Sin embargo, ¡siempre puedes realizar validaciones en el set, o puedes computar valores en el get!

 

class Switch {
    private int _brightness;

    public int Brightness{
        get {
            return _brightness;
        }
        set {
            if (value < 0 || value > 100)
                throw new ArgumentException("Rango inválido. ");

            _brightness = value;
        }
    }
    
    public bool IsOn {
        get {
            return _brightness != 0
        }
    }

Switch s = new Switch();
s.Brightness = 42;
var val = s.IsOn; // val es true        
val = s.IsOn; // val es false


 

Ahora, como puedes ver no hay mucha diferencia entre usar propiedades y usar métodos. Digo, la sintaxis sí es diferente, pero ¿qué ventajas tenemos de usar propiedades? La verdad es que la ventaja es por convención. Si un getter puede cambiar el estado de un objeto, o bien tiene que realizar computaciones muy grandes que pueden consumir significativamente memoria o procesamiento, usamos un método de la forma "GetPropiedad". En caso contrario, usamos una propiedad "Propiedad { get; }". Por otro lado, todos los setters cambian el estado del objeto (esa es su función). La regla es: si un setter cambia el estado del objeto en más de un atributo, o bien se realiza una tarea de procesamiento intensivo, utilizamos un método de la forma "SetPropiedad". En caso contrario, usamos una propiedad "Propiedad { set; }".

 

class Employee {
        private string _name;
        
        private Dictionary<DateTime, double> _salaryHistory;
        public event EventHandler NameChanged;

    public string Name{
        get {    return _name; }

    set {
        if (value == null)
            throw new ArgumentNullException("value");

        if (_name != value) {
            _name = value;    
    }

    if (NameChanged != null)
        NameChanged(this, EventArgs.Empty);
    }
}


}


public IDictionary<DateTime, double> SalaryHistory {
    get {
        if (_salaryHistory == null)
            _salaryHistory = new Dictionary<DateTime, double>();

        return _salaryHistory;
    }
}


public double GetAverage() {
    double avg = 0.0;

    foreach (var item in _salaryHistory)
        avg += item.Value;

    return avg;
}

public void SetAverage(double avg) {
    foreach (var item in _salaryHistory)
        item.Value = avg;
}

    public bool IsOn {

    get {
        return _brightness != 0;
        }
    }
}

 

El código anterior muestra varios aspectos interesantes. La clase muestra una propiedad Name, cuyo getter simplemente regresa una variable local de la clase. El setter, sin embargo, hace cosas más interesantes. Primero, verifica que el valor asignado no sea nulo (si lo es, lanza un ArgumentNullException). Luego, revisa si el valor del atributo es diferente al valor asignado, en cuyo caso aparte de actualizar el atributo con el nuevo valor, dispara un evento, llamado NameChanged. Este uso de una propiedad para validar entrada de valores y para lanzar notificaciones es sumamente común.

Luego, tenemos una propiedad llamada SalaryHistory. Esta propiedad tiene como tipo de dato una interfaz IDictionary. Sin embargo, vemos que internamente todo se guarda en un objeto de tipo Dictionary, el cual por supuesto implementa IDictionary. Este es un uso común de las propiedades: no queremos que nuestro cliente sepa qué objeto interno utilizamos: basta con que se sepa que es un IDictionary. Otro ejemplo sería regresar un IEnumerable, sin importar si internamente se implementa como un Array, un List<T> o on Collection<T>.

Adicional a esto, tenemos que el getter revisa si el atributo es nulo. En caso de serlo, inicializa el objeto antes de regresarlo. Esta técnica, llamada "lazy initialization" o "inicialización retardada", es útil cuando tenemos objetos miembro que no necesariamente ocupamos desde que se instancia el objeto padre. Por último, nota que la propiedad no tiene setter, puesto que no es necesario cambiar el objeto diccionario, sino su contenido.

Posteriormente, tenemos un par de métodos GetAverage y SetAverage. Estos son métodos porque hacen cómputo (relativamente) intenso. Podríamos poner una propiedad, pero de esta forma le indicamos al cliente de la clase que se espera no esté invocando los métodos a cada rato. Este sería un mal uso:

 

Employee e = new Employee();
...
double[] vals { … };
foreach (double val in vals)
{
    if (val > e.Average)
        // hacer algo
}

Para cada iteración, hacemos un cálculo para ver si el valor es mayor al promedio provisto por la hipotética e.Average. Durante esta iteración, e.Average será siempre la misma. Sin embargo, ejecutamos el cálculo una y otra vez, lo cual no es muy eficiente. Sin embargo, como Average es una propiedad, no me da indicios de que se estén produciendo cálculos. Ahora considera esto:

Employee e = new Employee();
...
double[] vals { … };
foreach (double val in vals)
{
    if (val > e.GetAverage())
        // hacer algo
}

Así al menos estamos conscientes de que se ejecuta un método, el cual hace cálculos. Al revisar esto, veríamos que estamos haciendo algo no tan eficiente, y probablemente decidamos cambiar nuestro código a:

Employee e = new Employee();
...
double avg = e.GetAverage();

double[] vals { … };
foreach (double val in vals)
{
    if (val > avg)
        // hacer algo
}

Mucho mejor, ¿no?

Bueno, ya regresando al ejemplo anterior, tenemos la propiedad IsOn, la cual regresa un resultado basado en el cómputo de otro tipo de atributos. Este es otro ejemplo de uso común de las propiedades.

Y ya por último, tenemos una declaración rara de una propiedad, llamada Address. Esta declaración de propiedad define un getter y un setter, pero no define el cuerpo. Cuando declaramos una propiedad así, estamos declarando una propiedad abreviada, y el compilador lo que hace es generar un atributo privado y enlazarlo al cuerpo get y set de la propiedad. Es decir, al escribir:

public string Address { get; set; }

el compilador lo transforma en:

private _Address;
public string Address { 
    get { return _Address; }
    set { _Address = value; }
}

Solo que el compilador lo hace por nosotros. Es una forma de azúcar sintáctico para que escribamos menos .

Ahora bien, las propiedades tienen un modificador de acceso, por lo que también pueden ser públicos, privados, protegidos, internos e internos protegidos. Declararlos como tales provoca que tanto getter como setter tengan el mismo nivel de acceso. Sin embargo, ¿qué pasa si queremos que uno de los elementos tenga un nivel de acceso diferente? Por ejemplo, si tenemos una propiedad pública, pero queremos que el setter sea protegida, ¿cómo lo hacemos?

Lo que se hace en este caso es calificar el getter o setter en cuestión con el modificador deseado, siempre y cuando el nivel de acceso de éste sea menor que el de la propiedad:

public string Address { 
    get { return _Address; }
    protected set { _Address = value; }
}
…
Employee e = new Employee();
var a = e.Address; // ok
e.Address = "dirección"; // ¡error de compilación! sólo clases que hereden
                              // de Employee pueden modificar esa propiedad.

El tema importante aquí es el nivel de acceso. El nivel público tiene mayor nivel de acceso que protected, y éste tiene mayor nivel de acceso que privado, y así sucesivamente. Así, el siguiente código no compila:

protected string Address { 
    get { return _Address; }
    public set { _Address = value; }
}

Creo que ha quedado claro el uso de las propiedades como medida para garantizar un buen encapsulamiento. Ciertamente es mejor que exponer los atributos directamente. En .NET, aún cuando un atributo no requiera más que existir (i.e. no necesite procesamiento en el get o en el set adicional), es buena práctica el tener las propiedades.

Implementación explícita de interfaces

En C# y .NET, las interfaces son un mecanismo de herencia muy difundido en toda la plataforma. Las interfaces, sin embargo, también nos ayudan a mejorar el encapsulamiento, principalmente a través de la implementación explícita de interfaces.

Este tipo de implementación se hace prefijando el nombre de la propia interfaz seguida de un punto, seguida del nombre del método o la propiedad. Tras lo discutido en la sección anterior, nos interesa en particular las propiedades, que nos ayudan a encapsular los atributos. Esta implementación explícita siempre será pública, y no puede ser declarada como abstracta, virtual, sobrescrita u oculta (abstract, virtual, override, new). Y lo que es importante: no puede ser accedida directamente. Vamos por partes.

interface IContactable
{
    string Address { get; set; }
    string Phone { get; set; }
}

public Employe : IContactable
{
    public string Address { get; set; } // implementación implícita
    string IContactable.Phone { get; set; } // implementación explícita
}

Employee e = new Employee();
e.Address = "Dirección"; // ok, no problemo
e.Phone = "55156789"; // ¡no compila! no puede ser accedida directamente

IContactable c = e as IContactable; // convertimos
c.Phone = "55156789"; // ok, ¡ahora sí funciona!

Es muy importante hacer notar que Phone no puede ser accedido mediante la clase Employee, pero si convertimos a la interfaz, IContactable, entonces sí podemos acceder a dicha propiedad. Esto es importante, pues nos hace notar cómo la implementación explícita de interfaces puede ayudarnos a mantener el encapsulamiento, al permitirnos ocultar propiedades que no queramos que estén disponibles directamente.

…y otra cosa

Bueno, creo que hemos hablado mucho sobre el tema del encapsulamiento. Resumiendo, hemos comenzado por definir el estado de un objeto y sus implicaciones dentro de la programación orientada a objetos. Luego, intentamos motivar, a partir de ello, la necesidad del encapsulamiento de los atributos de nuestros objetos. Por último, vimos cómo C# y .NET implementan dicho encapsulamiento, mediante los modificadores de acceso, los métodos y las propiedades, y la implementación explícita de interfaces.

No queda más, pues, que hacer la recomendación a título personal: por favor, siempre asegúrate que tus atributos estén encapsulados. Nunca sabes cuándo otro programador va a meterle mano a tu código. Encapsular el estado de tu objeto puede ser vital para que el otro no la riegue. De igual forma, cuando tú metas mano en código ajeno y veas un atributo público, desconfía de él: busca dónde se utiliza y bajo que circunstancias, no sea que le des en la torre al estado del objeto.

Pues eso es todo. ¡Hasta la próxima!

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

Leer y escribir flujos de datos en la red


Hace tiempo escribí un artículo titulado "Todo sobre flujos". En esa entrada hablé y hablé y hablé (o mejor dicho, escribí y escribí y escribí) sobre flujos y flujos y flujos. A pesar de tantos flujos que vimos, no mencioné más que de pasada la lectura de flujos que se conectan por red. Es hora de contar un poco sobre ello.

¿Te acuerdas?

Bien, recordemos que un búfer es un bloque de bytes asignados y reservados en memoria para almacenar un valor. Si los bytes son contiguos, decimos que es un búfer secuencial. Hoy en día, por cierto, prácticamente todos los búferes son secuenciales, y en .NET se representan, en su forma más cruda, como un array de bytes.

Un flujo es un objeto que tiene un búfer secuencial y del cuál mantiene una posición, a partir de la cual puede realizar operaciones de lectura y escritura. Así, mientras en un búfer de 100 bytes uno podría leer los bytes 3, 84 y 42, de forma salteada, en un flujo no pasa lo mismo: comienzo a leer el primer byte, luego el segundo, el tercero, el cuarto, etc., hasta llegar al 42. Como podemos ver, los flujos que se utilizan para obtener bytes en orden aleatorio no son muy eficientes. Pero sí lo son si queremos leer (o escribir) byte a byte. Y ahí es donde mejoran notablemente su rendimiento.

En .NET, un flujo está representado por la clase base abstracta Stream, ubicada en el espacio de nombres System. Dado que ya hablé y hablé y hablé (o mejor dicho, escribí y escribí y escribí) sobre flujos y flujos y flujos, no me parece apropiado repetir y repetir y repetir lo mismo. Por favor, revisa el otro artículo, pero sí ten en cuenta los conceptos importantes. Cualquier Stream podemos utilizarlo junto con StreamReader y StreamWriter, para leer y escribir datos en éste. Esto es independiente del tipo de flujo que estemos manejando: MemoryStream, FileStream, GZipStream, etc.

Como puedes ver, el concepto de flujo tiene un poder de abstracción muy grande. Cualquier lectura/escritura de bytes puede abstraerse de esta forma. Es natural que esta abstracción se aproveche en muchos lados de .NET.

Transmisión de datos en redes

Cuando hablamos de una transmisión de datos en red (es decir, mediante protocolo TCP/IP y afines), tenemos básicamente estos componentes.

1.- Transmisor. Representa el software que envía los datos. El transmisor envía una petición a una dirección IP y a un puerto determinado, donde espera una respuesta. Dependiendo de dicha respuesta generará la siguiente transmisión.

2.- Receptor. Representa el software que recibe los datos. El receptor usualmente abre una conexión a su propia IP y a un puerto determinado, y revisa si existe algún dato. Si no existe dato, termina la conexión, se duerme durante un momento, y vuelve a intentarlo de nuevo. A esto se le llama "escuchar" el puerto. Cuando sí existen datos, los lee y genera una respuesta apropiada.

3.- Protocolo. Durante el proceso de escucha-transmisión, se envían y reciben bytes. Cuando tenemos una regla para el envío/recepción de bytes, se ordena y se estandariza, decimos que tenemos un protocolo de comunicación.

4.- Paquetes. Los datos que se envían por red se hacen en bloques de bytes de tamaño definido, usualmente por el propio protocolo. A estos bloques se les conoce como paquetes.

5.- Enchufe. El concepto de un software que se conecta a una dirección IP y abre un puerto, para transmitir o escuchar, se conoce como un enchufe o sócket. Estos suelen ser componentes genéricos independientes, bien provistos por el sistema operativo, o por el hardware y sus manejadores.

Así pues, podemos decir que en general, el proceso de transmisión de datos se lleva de la siguiente forma.

1.- El receptor comienza el proceso de escucha mediante la apertura de un sócket local en un puerto determinado.

2.- El transmisor abre un sócket y envía una señal para indicar que va a comenzar la transmisión.

3.- Si el transmisor no recibe respuesta, aborta la operación.

4.- Si el transmisor recibe respuesta (su naturaleza depende del protocolo utilizado, por ejemplo, SMTP, SOAP, HTTP, etc.), entonces procede con el envío de información, enviando paquete por paquete.

5.- El receptor recibe paquete por paquete, y cada uno lo va almacenando, ya sea en un búfer de memoria, ya sea en disco duro directamente.

6.- Cada que se envía un paquete, el transmisor se pone en modo "escucha" para recibir la respuesta del receptor, indicándole que todo salió bien (o al menos esto es lo común; algunos protocolos como UDP no esperan confirmación).

7.- Cada que se recibe un paquete, el receptor envía una señal al receptor para indicarle que puede enviar el siguiente paquete.

8.- Cuando ya no hay nada más por enviar, el transmisor envía alguna señal para hacérselo saber al receptor. Y aquí termina el proceso: el transmisor cierra su sócket, y el receptor hará lo propio (y usualmente volverá a quedar a la escucha de nuevas peticiones).

En estos ocho pasos… ¿notas algo común con lo que hemos hablado? ¿Notas algún tremor en la fuerza? ¡Pues el paso cinco! ¿Te suena? "Recibe paquete por paquete y lo va almacenando", dice. Pues claro, ¡esa es nuestra definición de flujo! En otras palabras, podemos decir que el envío secuencial de paquetes en realidad es un flujo de datos. Por supuesto, hay diferencias puesto que entre cada paquete suele haber envío y recepción de comandos entre emisor y receptor, por lo que a pesar de ser secuencial puede ser que no sea inmediato. Sin embargo no importa, sigue entrando dentro de nuestra abstracción de flujo. Por lo tanto, es factible pensar que la transmisión de datos en red se haga mediante un flujo, y por tanto, que en .NET se implemente a través de alguna clase derivada de Stream.

El objeto NetworkStream

En el espacio de nombres System.Net existe un espacio de nombres adicional donde se declaran todas las clases necesarias para trabajar con sóckets: System.Net.Sockets. La clase que (de momento) nos interesa de ese espacio de nombres es NetworkStream. Esta clase implementa los mecanismos necesarios para la lectura/escritura secuencial de bytes (que nosotros conocemos como paquetes). Analicemos un poco la clase.

Tiene un constructor que toma como parámetro un objeto de tipo Socket. Como te imaginarás, esta clase define los sóckets de los que ya hemos hablado. En particular, implementan sóckets de acuerdo a como fueron definidos por la Universidad de Berkeley, allá en los lejanos 80s, para su sistema operativo Unix BSD. En un momento más trataremos con los sóckets. De momento sólo nos interesa saber que necesitamos un sócket para crear un NetworkStream. Lo cual hace sentido, porque el sócket está a la mitad de la comunicación entre sóckets.

Bueno, sigamos. Si analizamos las propiedades que heredamos de Stream, podemos ver que CanRead y CanWrite se implementan como siempre. CanTimeout, sin embargo, siempre regresará true. En efecto, un timeout se da si se nos cae la conexión de red a la mitad de una operación. Por eso, siempre será true. Pero la que más llama la atención es CanSeek: siempre de los siempres, esta propiedad regresa false. ¿Por qué, puedes adivinar?

CanSeek regresa false porque no puede hacerse una llamada al método Seek, por supuesto. Pero ¿Por qué? ¡Pues porque se transmiten paquetes por paquetes! Imagina este escenario. Abres el flujo, y por tanto estás posicionado en el primer byte. Lees los primeros, digamos, 1024 bytes. Si por ejemplo, el flujo se transmite en paquetes de 256 bytes, cuando tú lees los primeros 1024 bytes el flujo, por detrás, hará cuatro peticiones al transmisor, pidiéndole 4 paquetes. Y no pedirá más hasta que tú vuelvas a leer más bytes. Y pasa algo similar en un escenario al revés: supongamos que el flujo transmite de 1024 en 1024 bytes, y tú lees los primeros 256. En la primera lectura, se descargan los 1024 bytes y el flujo te da los 256 bytes. En la segunda lectura, el flujo *no* descarga más bytes, sino que te regresa los segundos 256 bytes de lo que ya había descargado. Y así sucesivamente hasta la quinta llamada, donde volverá a descargar los próximos 1024 bytes.

Ahora bien, supongamos que en medio de estos escenarios, se me ocurre llamar a Seek con un tamaño mayor a lo que ha descargado hasta el momento. ¡Sopas! Pues el flujo no podrá mover el cursor, porque puede ser que la información ni siquiera se haya descargado. Imagínate que pido Seek(10240): tendría que hacer 10 descargas de paquetes. Pero más importante aún: ¡el receptor nunca sabe cuál es el tamaño del búfer! En efecto, lo sabrá sólo cuando el transmisor le envíe la señal de fin. Entonces, en ese caso, una llamada a Seek puede hacer que nos pasemos más allá del final del búfer, sin siquiera saberlo, lo cual sería un desastre. Por eso es que la búsqueda de bytes no está soportada, y Seek lanzará un gran NotSupportedException si la invocas, y CanSeek siempre regresa false.

Por cierto, como decía, no podemos saber de antemano el tamaño del flujo. Un corolario de esto es que por lo mismo, no podremos utilizar la propiedad Length. Si la invocas, tendrás que vértelas con una brillante y reluciente NotSupportedException. Y por lo mismo, también recibirás la misma excepción si invocas a la propiedad Position, la cual debería regresarte tu posición de búsqueda dentro del flujo.

Por lo demás, las propiedades y métodos tradicionales de Stream siguen funcionando: Read, Close, Write, sus variantes Async, Flush, CopyTo, etc.

NetworkStream también cuenta con sus propiedades particulares. De entrada, la propiedad Socket nos regresa -tadáaaa- el sócket que el flujo ocupa. Esta propiedad no pretende ser utilizada por externos, por lo que está declarada como protegida.

Más interesante es la propiedad DataAvailable. Regresando al escenario de arriba, en el que el flujo descarga un paquete de 1024 bytes, pero tú sólo le pides 256. La primera vez que invocas, DataAvailable regresa false, y se descargan 1024 bytes. La segunda vez, ya no se descarga nada porque todavía hay datos en el búfer, y por tanto DataAvailable te regresa true. Y así sucesivamente.

NetworkStream stream = …;
byte[] buffer = new byte[256]

// stream.DataAvailable == false
stream.Read(buffer, 0, buffer.Length); // se descargan 1024 bytes y nos 
                                                            // devuelve 256 bytes. 
// stream.DataAvailable == true
stream.Read(buffer, 0, buffer.Length); // no se descarga nada, nos devuelve
                                                            // bytes 256-512
// stream.DataAvailable == true
stream.Read(buffer, 0, buffer.Length); // no se descarga nada, nos devuelve 
                                                            // bytes 512-768
// stream.DataAvailable == true
stream.Read(buffer, 0, buffer.Length); // no se descarga nada, nos devuelve
                                                            // bytes 768-1024
// stream.DataAvailable == false
stream.Read(buffer, 0, buffer.Length); // se descargan los próximos 1024 bytes 
                                                            // y nos devuelve 256 bytes

Stream define el método Close, con varias sobrecaras, para cerrar un flujo. Pues bien, NetworkStream define un Close con un parámetro numérico, el cual determina a partir de cuántos segundos debe cerrarse el flujo. Es como un cierre a posteriori.

Y bueno, esas son todas las peculiaridades de NetworkStream. Ahora veamos cómo podemos obtener uno de estos objetos.

Trabajando con sóckets

Bueno, la forma de obtener un NetworkStream es a través de sóckets, como ya hemos dicho. La verdad es que la programación con sóckets es un poco complicada, sobre todo, porque necesitas definir tu propio protocolo de comunicación, lo cual no suele ser tarea trivial. Es mejor basarse en algún protocolo estándar, como HTTP, SMTP, FTP, SOAP o algo así. En fin, que no es propósito de esta entrada ver cómo mantener comunicaciones par-a-par (peer-to-peer). Pero veamos un ejemplo. Supongamos que hay algún sócket abierto en alguna parte que está a la escucha de una petición, y que tras hacérsela, nos regresará un archivo con información. Lo primero que habríamos que hacer es abrir un sócket propio. Y ya con eso, usamos alguno de los constructores de NetworkStream.

En el siguiente ejemplo, creamos un sócket y asumimos que hay algo en la IP 192.168.26.1 en el puerto 42, que nos va a enviar un bloque de bytes con el siguiente formato: una cadena de texto de 200 caracteres que representa el nombre de un empleado (asumimos que la cadena viene prefijada con su longitud y su codificación), un entero que representa su clave, y un número flotante que representa su sueldo.

 

using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
…

Socket socket = null;
string ip = "192.168.26.1";
int port = 42;

IPHostEntry host = Dns.GetHostEntry(ip);
foreach (IPAddress address in host.AddressList)
{
    IPEndPoint endpt = new IPEndPoint(address, port);
    Socket temp = new Socket(endpt.AddressFamily, SocketType.Stream, 
        ProtocolType.Tcp);
    temp.Connect(endpt);
    if (temp.Connected)
    {
        socket = temp;
        break;
    }
}

if (socket == null)
    throw new Exception("No pudimos conectarnos a " + ip);

NetworkStream stream = new NetworkStream(socket, true);
if (!stream.CanRead)
    throw new Exception("No podemos leer de este flujo.");

// ¡uju! ¡ya tenemos el stream! podríamos leer bloques de
// bytes, pero… vamos, que estamos en .NET, hay que aprovechar
// la abstracción y usar algún lector de flujos…

BinaryReader reader = new BinaryReader(stream);
string name = reader.ReadString();
int id = reader.ReadInt32();
double salary = reader.ReadDouble();
reader.Close();

// reader.Close cierra el Stream, y éste a su vez cierra el sócket,
// porque le pasamos como parámetro al constructor del NetworkStream
// un true, que quiere decir que el flujo es el dueño del sócket y que
// lo cierre al terminar. Si hubiéramos pasado false, tendríamos que
// cerrar el sócket nosotros mismos.

Console.WriteLine("Empleado {0} - {1}:\t ${2}", name, id, salary);

 

Bueno, como podemos ver, es más lío abrir el sócket que usar el flujo. Al final, usar el NetworkStream nos facilita mucho las cosas, puesto que hereda de Stream lo podemos usar en infinidad de lugares. Por ejemplo, en lugar de usar un BinaryReader, podríamos haber creado un FileStream para crear un archivo, y usar NetworkStream.CopyTo para vaciar el contenido en el FileStream y por tanto, guardando la información en el archivo mismo. ¡Easy peasy!

Por cierto, que la clase Socket tiene métodos Send y Receive para enviar y recibir bytes. En efecto, puedes hacerlo como si no fuera un flujo, pero es muuuucho más engorroso. Mira este ejemplo que saqué del MSDN para hacer una petición a un servidor web, usando sóckets.

 

string request = "GET / HTTP/1.1\r\nHost: " + server + 
    "\r\nConnection: Close\r\n\r\n";

Byte[] bytesSent = Encoding.ASCII.GetBytes(request);
Byte[] bytesReceived = new Byte[256];

// Create a socket connection with the specified server and port.
Socket s = ConnectSocket(server, port);
if (s == null)
    return ("Connection failed");

// Send request to the server.
s.Send(bytesSent, bytesSent.Length, 0);
// Receive the server home page content. int bytes = 0;
string page = "Default HTML page on " + server + ":\r\n";
// The following will block until te page is transmitted. 

do {
    bytes = s.Receive(bytesReceived, bytesReceived.Length, 0);
    page = page + Encoding.ASCII.GetString(bytesReceived, 0, bytes);
}
while (bytes > 0);

 

Vamos, tienes que llevar tu control de bytes leídos, escritos, etc., además de tener que interpretar los bytes por tu cuenta. Checa cómo hay que saber si la cadena se codifica como ASCII, Encoding, etc., mientras que con el BinaryReader y BinaryWriter, eso se hace en automático. Entonces, verás que trabajar así no está cool.

Trabajando con servidores de Internet

Como decía, es muy difícil que en la vida trabajemos con sóckets. En realidad son muy contados los escenarios en los que nos interesarán. La mayoría de las veces nos va a interesar trabajar con protocolos de más alto nivel, estándares y definidos. Uno de estos protocolos es el HTTP.

HTTP se construye sobre TCP/IP. El ejemplo anterior que puse, que copié del MSDN, muestra cómo usar sóckets para realizar una petición HTTP. En realidad, este protocolo define una serie de intercambios de textos. Por ejemplo, el texto:

 

GET / HTTP/1.1
Host: 192.168.26.1
Connection: Close

envía una petición sencilla para obtener la página default del servidor web 192.168.26.1. Esto ilustra de forma rápida cómo funciona HTTP. Básicamente, se le envía un encabezado como el anterior, y el servidor responde con un encabezado de respuesta, más el contenido solicitado, que suele ser un binario o un documento HTML.

Bueno, afortunadamente para nosotros, cuando queramos trabajar con HTTP no hace falta que conozcamos todo el protocolo, ni hacen falta sockets. Para hacer peticiones y responder, tenemos un objeto muy conocido por los programadores de ASP.NET, pero que no siempre saben que podemos usar en aplicaciones que no son ASP.NET. Me refiero, por supuesto, a HttpWebRequest y HttpWebResponse.

Estas clases heredan de WebRequest y WebResponse, respectivamente. Estas clases sirven como base para hacer peticiones basadas en TCP/IP. De hecho, soporta tres protocolos: HTTP (http://), cuyas clases derivadas ya vimos; FTP (ftp://) mediante las clases FtpWebRequest y FtpWebResponse; y el protocolo FILE (file://) mediante FileWebRequest y FileWebResponse.

Sin embargo, la mayoría de las veces, sobre todo si estamos haciendo aplicaciones de escritorio o móviles, no vamos a querer lidiar con los aspectos técnicos de los WebRequest y WebResponse. Afotrunadamente, .NET nos ofrece una alternativa mucho más sencilla: utilizar la clase WebClient.

La clase WebClient contiene métodos que nos permiten enviar y recibir información mediante cualquier protocolo web. De hecho, lo interesante es que WebClient se basa en la URI, la cual representa un recurso web, y con base en eso resuelve si ha de usar HTTP, FTP o FILE. De tal suerte que no tenemos que preocuparnos (mucho) por la fuente que estamos intentando acceder.

El siguiente ejemplo muestra cómo usar un WebClient para descargar la página de Microsoft.

 

using System;
using System.Net;
using System.IO;
… 

string userAgent = "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.2; .NET CLR"
    + "1.0.3705;)";

WebClient client = new WebClient();
client.Headers.Add("user-agent", userAgent);

Stream stream = client.OpenRead("http://www.microsoft.com");
StreamReader reader = new StreamReader(stream);
string html = reader.ReadToEnd();
reader.Close();

Console.WriteLine("HTML recibido: \n\n");
Console.WriteLine(html);

 

Mucho más fácil que hacerlo con sóckets, ¿no crees?

En fin, WebClient puede hacer varias cosas, pero hay otras que no. Si necesitas más sofisticación por el motivo que sea, deberás hacer uso de los WebRequest y WebResponse antes mencionados. Todo comienza con WebRequest.Create. A este método le pasas la URI a la que te quieres conectar. Usualmente saber qué protocolo vas a utilizar, así que puedes hacer la conversión hacia el WebRequest correspondiente. Después, lo que haces es llenar tu WebRequest con información adicional: encabezados, credenciales, etc. Cuando estés listo, ejecutas el método GetResponse. Al hacerlo, digamos que ejecutas la transacción y se va la petición al servidor. El resultado: un WebResponse precisamente con la respuesta del servidor.

Para ilustrar lo anterior, veamos este ejemplo. Lo que hace es leer un archivo de texto cualquiera, y –tadáaa– nos conectamos a un hipotético servidor FTP y subimos dicho archivo. Easy peasy.

using System;
using System.Net;
using System.IO;
… 

// 1
byte[] fileBuffer = File.ReadAllBytes("C:\\archivo.txt");
// 2
var request = WebRequest.Create("ftp://ftp.miservidor.com") as FtpWebRequest;
request.Credentials = new NetworkCredentials("anonymous", "fer@miservidor.com");
request.ContentLength = fileBuffer.Length;
// 3
Stream stream = request.GetRequestStream();
stream.Write(fileBuffer, 0, fileBuffer.Length);
stream.Close();
// 4
var response = request.GetResponse() as FtpWebResponse;
Console.WriteLine("Estado: {0}", response.StatusDescription);
response.Close();

Repasemos. En 1, obtenemos los bytes que componen al hipotético archivo que queremos subir al FTP. En 2 creamos un FtpWebRequest (nota la conversión que hacemos de WebRequest a FtpWebRequest), establecemos las credenciales y el tamaño del contenido que vamos a enviar. En 3, obtenemos el flujo del FTP como tal y escribimos el contenido del archivo que vamos a enviar. En 4, finalmente, invocamos a GetResponse, método que hará el envío de información al servidor y nos regresará un objeto FtpWebResponse (también nota que se hace conversión) con los resultados, entre otras cosas, el StatusDescription. Por último, cerramos el objeto y terminamos.

 

Conclusiones

 

Bueno, hemos llegado lejos. Creo que ha quedado claro que la representación dle un intercambio de paquetes por red como un flujo de trabajo es perfecta. La abstracción hace muy sencillo el hacer trabajar NetworkStream con lectores y escritores y otras clases que ofrece .NET. También vimos cómo aprovechar las clases para intercambiar archivos entre pares y servidores.

Eso es todo por ahora. ¿Quién se echa un peer-to-peer para compartir archivos? Open-mouthed smile

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

Creación de archivos ZIP en .NET


Hace algún tiempo publiqué un artículo sobre cómo manejar flujos de datos: la clase Stream y sus derivadas. Entre lo que expuse en aquella entrada está el uso de las clases GZipStream y DeflateStream como flujos para comprimir datos. Una de las limitantes, sin embargo, de estas clases, es que aunque generan un flujo que puede guardarse eventualmente en disco duro vía FileStream, no son compatibles con programas populares como WinZip, WinRAR o el mismo componente Zip que viene con el Windows Explorer.

O mejor dicho: GZipStream y DeflateStream comprimen un archivo, pero no crean un contenedor para archivos compresos, que al final del día es lo que es un archivo ZIP. En fin, que hasta hace poco, estas clases se podían usar para comprimir archivos de uso propio de la aplicación, pero no para que pudiesen distribuirse los archivos.

Con la salida de .NET 4.5, sin embargo, apareció unas clases en System.IO.Compression que no causaron mucho ruido o atención, pero que a mi entender son súper importantes, porque permiten precisamente comprimir archivos dentro de un contenedor en formato ZIP estándar. Las clases son las siguientes.

1.- ZipArchiveEntry. Representa un archivo compreso dentro de un archivo ZIP (ZipArchive).

2.- ZipArchive. Representa un archivo ZIP, que tiene archivos compresos (ZipArchiveEntry).

3.- ZipFile. Contiene métodos estáticos para trabajar con archivos ZIP y archivos compresos.

4.- ZipFileExtensions. Contiene métodos de extensión para ZipArchive y ZipArchiveEntry.


Veamos entonces algunos ejemplos sobre cómo utilizar estas clases.

Crear un archivo zip

 

using System.IO;
using System.IO.Compression;
…

// 1
FileInfo sourceFile = new FileInfo(@"C:\reporte1.xlsx");
FileStream sourceStream = sourceFile.OpenRead();
// 2
FileStream stream = new FileStream(@"C:\reportes.zip", FileMode.Open);
// 3 
ZipArchive archive = new ZipArchive(stream, ZipArchiveMode.Create);
// 4 
ZipArchiveEntry entry = archive.CreateEntry(sourceFile.Name);
// 5
Stream zipStream = entry.Open();
// 6
sourceStream.CopyTo(zipStream);
// 7
zipStream.Close();
sourceStream.Close();
archive.Dispose();
stream.Close();

 

Bueno, veamos que sucede. Asumimos que en C:\ existe un archivo, reporte1.xlsx, y que tenemos permisos de escritura en C:\. La verdad que escogí ese directorio para no hacer tan largo el ejemplo. En fin. Por tanto, primero abrimos el archivo que queremos añadir a un archivo zip. Para ello, usamos FileInfo y FileStream en modo lectura, como siempre.

El segundo paso es que necesitamos crear un flujo de archivo para nuestro archivo zip contenedor. Volvemos a usar un FileStream tradicional, hasta aquí no hay nada nuevo. Lo nuevo viene en el tercer paso: creamos un ZipArchive y le pasamos como primer parámetro el flujo que hemos creado para nuestro zip (así garantizamos que el ZipArchive escriba sobre éste), y como segundo parámetro pasamos la enumeración ZipArchiveMode.Create para indicarle que vamos a crear entradas. Y ahora sí, viene el cuarto paso: creamos una entrada invocando ZipArchive.CreateEntry, y le pasamos el nombre con el que queremos identificarlo. Para más fácil, le pasamos el nombre original mediante FileInfo.Name. Y ahora a tenemos un ZipArchiveEntry, aunque vacío. En el quinto paso abrimos el flujo de esa entrada y obtenemos un Stream donde podemos escribir. El sexto paso vacía el flujo del archivo que queremos comprimir a la entrada dentro del zip que hemos creado. Aquí es donde realmente ocurre la compresión. Y finalmente, en el paso 7 cerramos flujos y limpiamos memoria. Nota que puedes usar directivas "using" en lugar de invocar a Close y Dispose.

Resumiendo hasta el momento: 1) abrimos archivo fuente, 2 y 3) creamos archivo zip, 4) creamos una entrada vacía en el zip.

Extraer archivo de un zip

 

using System.IO; 
using System.IO.Compression; 
… 

// 1 
FileInfo zipFile = new FileInfo(@"C:\reportes.zip"); 
FileStream zipStream = zipFile.OpenRead(); 
// 2 
ZipArchive archive = new ZipArchive(zipStream, ZipArchiveMode.Read); 
// 3 
ZipArchiveEntry entry = archive.GetEntry("reporte1.xlsx"); 
// 4 
Stream stream = entry.Open(); 
// 5 
FileInfo destFile = new FileInfo(@"C:\reporte1.xlsx"); 
if (file.Exists) { 
    file.Delete(); 
    file.Refresh(); 
} 
FileStream destStream = destFile.OpenWrite(); 
// 6 
stream.CopyTo(destStream); 
// 7 
stream.Close(); 
destStream.Close(); 
archive.Dispose(); 
zipStream.Close();

 

Es un proceso similar, de hecho, pero a la inversa. Veamos. En 1, abrimos un flujo proveniente del archivo ZIP del cual queremos extraer el archivo. Luego, en 2 creamos el ZipArchive, pasándole el flujo que acabamos de abrir, y la bandera ZipArchiveMode.Read, para indicarle que vamos a leer. Esto es de suma importancia, porque las banderas de lectura, escritura y búsqueda de los objetos Stream dependen de esta bandera. Si pones un Read e intentas escribir, seguramente tendrás un InvalidOperationException entre manos.

Bueno bueno, ya. En 3 usamos el método GetEntry, pasándole el nombre del archivo dentro del zip, es decir, de la entrada, y obtenemos un objeto de tipo ZipAchiveEntry. Alternativamente, puedes iterar sobre ZipArchive.Entries para ver todas las entradas existentes. Ya con el ZipArchiveEntry podemos abrir el stream mediante ZipArchiveEntry.Open, como se muestra en 4. Ya tenemos el flujo, ahora sólo debemos guardarlo en disco duro. El paso 5 abre el archivo destino donde guardaremos el archivo descompreso. Antes revisamos que si el archivo existe, lo eliminemos. Y abrimos el flujo de dicho archivo en modo escritura.

Y ahora sí, en 6 vemos cómo copiamos los bytes del flujo del archivo compreso al flujo del archivo en disco duro. Perfecto, ya estamos del otro lado. Lo último que resta es cerrar los objetos invocando a Close y Dispose, y listo.

Comprimir un directorio entero

Muy similar a comprimir un archivo, sólo que nos basamos en DirectoryInfo… Veamos.

 

using System.IO; 
using System.IO.Compression; 
… 

// 1 
DirectoryInfo dir = new DirectoryInfo(@"C:\reportes\"); 
// 2 
FileStream stream = new FileStream(@"C:\reportes.zip", FileMode.Open); 
// 3 
ZipArchive archive = new ZipArchive(stream, ZipArchiveMode.Create); 
// 4 
FileInfo[] sourceFiles = dir.GetFiles(); 
foreach (FileInfo sourceFile in sourceFiles) 
{ 
    // 5 
    Stream sourceStream = sourceFile.OpenRead(); 
    // 6 
    ZipArchiveEntry entry = archive.CreateEntry(sourceFile.Name); 
    // 7 
    Stream zipStream = entry.Open(); 
    // 8 
    sourceStream.CopyTo(zipStream); 
    // 9 
    zipStream.Close(); 
    sourceStream.Close(); 
} 
// 10 
archive.Dispose(); 
stream.Close();

  

Como puedes apreciar, es un código muy similar, sólo que usamos DirectoryInfo.GetFiles para obtener los archivos, e iteramos sobre estos. Veamos. En 1 abrimos el directorio que queremos comprimir. En 2, creamos el archivo donde guardaremos el zip y en 3 asociamos el flujo de éste al archivo zip. En 4 obtenemos los archivos del directorio y comenzamos a iterar para cada uno de ellos. En 5 abrimos el flujo de cada archivo, en 6 creamos la entrada dentro del zip, en 7 abrimos el flujo de la entrada dentro del zip, y en 8 vaciamos los bytes del archivo a la entrada dentro del zip. En 9 limpiamos los flujos locales. Finalmente, en 10 hacemos limpieza del archivo zip y del flujo. ¡Easy Peasy!

Bueno, si no quieres hacer tanto, huevas, puedes usar ZipFile.CreateFromDirectory:

ZipFile.CreateFromDirectory(@"C:\reportes\", @"C:\reportes.zip");

y listo. Pero bueno, lo interesante del código anterior era mostrar cómo hacerlo manual. Ahora que sabes cómo, usa la versión corta. Nota: la versión corta está disponible en .NET 4.5, pero no en .NET para Windows Store Apps. Así que si estabas pensando hacer alguna app para Windows 8 / RT, te la pelas: tendrás que emplear la versión larga.

Extraer los archivos de un zip

 

Vamos ahora a extraer todos los archivos de un zip a un directorio. Sea pues.

 

using System.IO; 
using System.IO.Compression; 
… 

// 1 
FileInfo zipFile = new FileInfo(@"C:\reportes.zip"); 
FileStream zipStream = zipFile.OpenRead(); 
// 2 
ZipArchive archive = new ZipArchive(zipStream, ZipArchiveMode.Read); 
// 3 
foreach (ZipArchiveEntry entry in archive.Entries) 
{ 
    // 4 
    Stream stream = entry.Open(); 
    // 5 
    FileInfo destFile = new FileInfo(@"C:\reporte\" + entry.Name); 
    if (file.Exists) { 
        file.Delete(); 
        file.Update(); 
    } 
    FileStream destStream = destFile.OpenWrite(); 
    // 6 
    stream.CopyTo(destStream); 
    // 7 
    stream.Close(); 
    destStream.Close(); 
} 
// 8 
archive.Dispose(); 
zipStream.Close(); 

Pues easy peasy también, ¿no? Ya nos la sabemos: abrimos archivo zip (1), asociamos el archivo con un zip (2), iteramos por cada entrada del zip (3), abrimos el flujo de una entrada (4), revisamos que el archivo no exista en el directorio destino (5) y abrimos el flujo del archivo destino, luego copiamos el flujo de la entrada del zip hacia el flujo del archivo abierto (6), para por último, cerrar flujos locales (7) y globales (8).

Para los huevas, pueden hacerlo más fácil, si no programan para .NET en Windows Store Apps.

ZipFile.ExtractToFile(@"C:\reportes.zip", @"C:\reportes\");

¡Jo! Bueno, siempre es mejor saber cómo se hacen las cosas por detrás. Sin albur.

Agregar y eliminar archivos a un zip

Y ya como último ejemplo, y creo que cubrimos todos los escenarios más importantes, está éste, el cual muestra cómo añadir un archivo a algún zip existente a la vez que eliminamos una entrada al mismo zip. Aquí la clave está en abrir el ZipArchive en modo ZipArchiveMode.Update en lugar de Read o Create. Lo demás es igual a lo que ya hemos visto.

 

using System.IO; 
using System.IO.Compression; 
… 

// 1 
FileInfo zipFile = new FileInfo(@"C:\reportes.zip"); 
FileStream zipStream = zipFile.Open(FileMode.Open); 
ZipArchive archive = new ZipArchive(zipStream, ZipArchiveMode.Update); 
// 2 
ZipArchiveEntry oldEntry = archive.GetEntry("reporte1.xlsx"); 
oldEntry.Delete(); 
// 3 
FileInfo sourceFile = new FileInfo(@"C:\reporte2.xlsx"); 
FileStream sourceStream = sourceFile.OpenRead(); 
ZipArchiveEntry newEntry = archive.CreateEntry(sourceFile.Name); 
Stream stream = newEntry.Open(); 
sourceStream.CopyTo(stream); 
stream.Close(); 
sourceStream.Close(); 
// 4 
archive.Dispose(); 
zipStream.Close();

  

 

Mucho más compacto porque ya no necesitamos re-explicar miles de cosas, vamos por pasos. En 1 abrimos el archivo y el flujo del mismo. Nota que ahora no usamos OpenRead u OpenWrite, pues necesitamos que el flujo sea tanto de lectura como de escritura, así que usamos Open con FileMode.Open. Luego asociamos el flujo a un zip, pero ojo, aquí pasamos como parámetro ZipArchiveMode.Update como segundo parámetro.

Luego, en 2 utilizamos GetEntry para obtener el archivo reporte1.xlsx, que debe encontrarse en el zip. Esto nos regresa un ZipArchiveEntry. Para eliminarlo, sólo tenemos que invocar su método Delete. Easy peasy esta parte.

Luego continuamos con 3, lo cual es lo mismo que hemos visto: abrir el archivo que queremos añadir al zip (en este caso, el ingeniosamente llamado reporte2.xlsx), abrir el flujo, crear la entrada en el zip, copiar bytes de la fuente a la entrada, y cerrar. Esto no varía en nada, todo el trabajo lo hace ya ZipArchive cuando le dijimos que se abriera en modo Update.

Por último, cerramos con 4 cerrando los objetos que hemos creado, haciendo Close y Dispose. And be done with it.

Conclusiones

Es bueno que Microsoft haya añadido estas clases a .NET. Me parece que hace mucho tiempo que eran necesarias. Hasta ahora uno tenía que utilizar librerías de terceros. Y no es que sean malas. SharpZipLib, de iC# Code, es muy buena, por ejemplo. Sin embargo tienen el estigma "no Microsoft", y eso hace que algunas empresas le pongan peros al momento de aceptar desarrollos que la utilicen. No todas, pero sí las hay. Además, de esta forma se estandariza la forma de hacerlo.

Quizás lo que me llama la atención es que no haya mucha publicidad sobre este tema importante. Es decir, el Task Parallel Library recibió mucha atención cuando salió .NET 4, y la integración de C# con el TPL (i.e. las palabras reservadas async y await) ahora que salió el .NET 4.5. Y estas clases se me hacen muy importantes también. Ojalá sólo sea falta de información de mi parte.

Pero bueno, que ahora ya las conoces y no hay pretexto para comenzar a utilizar estas clases y comenzar a migrar tu código legado… ¿Comentarios, preguntas, dudas?

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

Certificación 70-483


Si has estado conectado al mundo de Microsoft, habrás notado que este 2012 se está cerrando a tambor batiente. ¡Uf! Qué de cosas hemos visto a últimas instancias. Microsoft sacó su tan esperado Windows 8, culminando así con un año de pruebas desde que liberaron el primer Community Preview en el Build del año pasado. Asimismo, han sacado ya el Windows Phone 8 con su SDK, ya listo para que nosotros programadores comencemos a programar. Microsoft ha sacado también su Windows Surface, que corre sobre Windows RT, la cual desafortunadamente todavía no llega a México, pese a mis berrinches.

 
 

En fin, todo esto significa muchos cambios para los programadores. Entre ellos, obviamente, está el tema de las certificacioens. Microsoft ha publicado dos grandes caminos de certificaciones: una para Windows Store Developer, y otra para Web Developer.

 
 

Resulta que si quieres obtener la Windows Store Developer, que por cierto sustituye al Windows Developer que venía con las certificaciones de 2010, necesitas pasar los siguientes exámenes:

 
 

1.- 70-483 Programming in C#

2.- 70-484 Essentials of developing Windows Store Apps in C#

3.- 70-485 Advanced Windows Store Apps development using C#

 
 

Una vez que los pases, habrás obtenido el Microsoft Certified Solution Developer: Windows Store Apps using C#.

 
 

Ahora bien, enfocándonos en la certificación 70-483, vemos que es muy similar a la certificación 70-536, tan famosa durante la era de .NET 1.0 hasta .NET 3.5, y que desapareció con .NET 4.0. Aunque, obviamente, actualizada para incluir las últimas características. Pero… ¿qué mide esta certificación? Básicamente los siguientes temas.

 
 

1.- Creación de tipos: valores vs referencias, conceptos asociados (boxing, unboxing, etc.), tipos genéricos, enumeraciones, constructores, variables, métodos, clases, interfaces, propiedades, etc., conversión entre tipos de datos,  encapsulación, herencia, polimorfismo, reflexión, cadenas de texto e implementación del patrón Disposable.

2.- Flujo del programa: implementar un programa, estructuras de decisión, iteración, operadores, evaluación de decisiones, etc.; iterar colecciones, crear eventos y delegados, expresiones lambda, etc.; implementar el manejo de excepciones, gestionar la multiárea basada en hilos y conocer la librería de tareas paralelas.

3.- Depurar aplicaciones: crear directivas de compilación, manejo de símbolos de programa; implementar diagnóstico en la aplicación mediante trazas, bitácoras, aplicaciones de perfil, visor de eventos, crear y monitorear contadores de rendimiento.

4.- Implementar seguridad: validar entrada y salida de valores de usuario, validar integridad de datos; encriptar datos con algoritmos simétricos y asimétricos; gestión de ensamblados con versiones, firmados, aplicar niveles de seguridad en ejecución de código, etc.

5.- Acceso a datos: realizar operaciones de entrada y salida mediante archivos y flujos, consumir datos en JSON y XML, recibir y enviar datos mediante servicios web; consumir datos desde bases de datos en diferentes formatos incluyendo XML y JSON; consultar y manipular datos y objetos mediante LINQ; serializar y deserializar datos desde archivos, flujos comprimidos, flujos de red, etc., incluyendo formatos binarios, XML y JSON.

 
 

Ahora bien, dado que esta certificación está recién horneadita, no hay aun mucho material oficial para estudiar: cursos y libros. Bueno, según la página de Microsoft existe un libro de Microsoft Press: Exam Ref 70-483: Programming in C#. Sin embargo, lo estuve buscando en Amazon, y aunque lo tienen registrado, no tienen fecha de para cuándo estará disponible. Yo calculo que para enero, pues ese fue el tiempo aproximado que les tomó sacar libros y referencias allá en 2010.

 
 

¡Pero no quiero esperar tanto para certificarme! Y seguro que tú tampoco. Es así que me estaré dando a la tarea, en estos días, de armar un paquete con cada uno de los temas descritos, para que pueda sernos sencillo obtener dicha certificación. Un manual de estudio, si tu quieres. Serán una serie de entradas en el blog que traten los temas específicos, y estoy seguro que podremos reutilizar entradas publicadas anteriormente. La guía, o el índice de la misma, La estaré publicando en la página de tutoriales. ¡Hay que estar pendiente!

 
 

Y si tienes contribuciones, ¡son bienvenidas!

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

An operations error occurred con Directorio Activo


Si me has seguido últimamente sabrás que he estado trabajando con el Directorio Activo. Primero escribí sobre cómo realizar búsquedas de usuarios en el AD. Luego, hablé sobre un problema de acceso denegado al actualizar propiedades del AD. Pues bien, ahora nos hemos topado con otro problema.

Resulta que al ejecutar el código mostrado en el artículo anterior todo funciona bien. Hasta que metimos dicho código en una página de aplicación de SharePoint. Cuál no sería nuestra sorpresa al encontrarnos con un System.DirectoryServices.DirectoryServicesCOMException con el siguiente mensaje de error: “an operations error occurred”. Sin mayor información.

La frustración fue mayor cuando vimos que el error ocurría al realizar la búsqueda de usuario, ni siquiera al actualizar algún dato.

auto searcher = gcnew DirectorySearcher();            
searcher->Filter = String::Format(L"(SAMAccountName={0})", account);            
            
auto result = searcher->FindOne(); // <-- An operations error occurred

Pues menudo lío. Comenzamos a hacer una búsqueda en Internet, con varias recomendaciones. En particular dimos con esta: Common System.DirectoryServices Issues and Solution. Intentamos seguir las recomendaciones expuestas en la tercera pregunta, con estos resultados.

1.- Usar acceso anónimo. No podemos porque todo el portal de SharePoint no usa acceso anónimo, y por reglas de seguridad del cliente no podemos emplearlo.

2.- Utilizar un componente COM+. ¿En serio? ¿Quién emplea COM+ hoy en día? Además instalar este tipo de componentes nos hubiera llevado días, por políticas del cliente.

3.- Delegación mediante <identity impersonate=”true”> en el web.config. Lo intentamos, pero no nos funcionó.

4.- Correr el proceso del IIS como usuario de dominio. El cliente casi nos corre de su oficina. Y no podemos usar al usuario que ya nos había dado porque pondría en riesgo toda la granja ya existente.

5.- Personificar programáticamente al usuario. Lo intentamos, pero ¿cómo?

Es decir, 5) parecía el camino a seguir. Intentamos correr SPSecurity.RunWithElevatedPrivileges sin suerte. El problema es que el usuario y la contraseña los ponemos después de encontrar al usuario, cuando queremos modificar. ¿Entonces?

DirectorySearcher tiene un parámetro en su constructor: un DirectoryEntry (al cual podríamos poner un usuario y una contraseña), el cual usualmente tiene la unidad organizacional de la raíz del dominio. El problema en mi caso es que mi cliente tiene un bosque con ocho dominios, y ciertamente quiere que buscásemos en cada uno de estos al usuario, porque cambian constantemente. De ahí que DirectorySearcher siempre lo iniciábamos con el constructor vacío.

Fue ahí cuando se me ocurrió: ¿y si creo un DirectoryEntry vacío, le pongo un usuario y contraseña, y lo paso al DirectorySearcher?

auto entry = gcnew DirectoryEntry();
entry->Username = "arthur.dent";
entry->Password = "42";

auto searcher = gcnew DirectorySearcher(entry);
searcher->Filter = String::Format(L"(SAMAccountName={0})", account);

auto result = searcher->FindOne();

 

Y para mi sorpresa, el código anterior funcionó. Nota que el DirectoryEntry, en lugar de pasarle el dominio o alguna OU, lo dejamos vacío y sólo le pusimos las credenciales. Luego, creamos el searcher con éstas, y buscamos al usuario. Y vaya: nos trajo todo completito. Y ahora ya está funcionando esta parte.

¡Espero que esto sirva de ayuda!