Archivo

Archive for noviembre 2012

Gestión de memoria y recursos en .NET


Una de las características más importantes de C# y .NET es que tanto lenguaje como plataforma gestionan la memoria en automático. Para quienes venimos de C++, estábamos muy acostumbrados a gestionar nuestros propios recursos.

class address {
    public:
        std::wstring street;
        int number;
        std::wstring city;
        std::wstring postal_code;

        address() { … }
        address(const address& copy) { … }
        ~address() { }

        bool full_address () const { … }
        bool validate() const;
};

class employee {
    public:
        std::wstring name;
        int id;
        double income;
        address* main_address;
        address* office_address;

        employee() { … }
        ~employee() {
            if (main_address != nullptr)  
                delete main_address;    // liberar memoria
            if (office_address != nullptr)
                delete office_address;  // liberar memoria
        }

    …
};

employee* e = new employe();
e->name = L"Fernando Gómez";
e->income  = 500.00;
e->id = 42;
e->main_address = new address();
e->main_address->street = "Odessa";
e->main_address->number = 42;
e->main_address->postal_code = "38102";
e->office_address = nullptr;

// hacer algo con e

delete e; // liberar memoria

Este es un ejemplo sencillo. La clase address tiene un destructor que no hace nada, pero la clase employee tiene un destructor que sí hace algo: libera la memoria de los punteros existentes main_address y office_address. En C++ estábamos acostumbrados a liberar la memoria de forma manual, invocando la sentencia delete, como se ha visto. También estábamos acostumbrados, por supuesto, a que a alguien se le olvidada liberar la memoria y por tanto teníamos fugas de memoria.

En .NET, por supuesto, todo esto cambió. La máquina virtual de .NET incorpora un componente, llamado el colector de basura (garbage collector, GC), cuyo propósito es liberar la memoria, previo análisis de recursos. En palabras sencillas, el programador ya no tiene que preocuparse por liberar los recursos de memoria, y por tanto ya nunca tendremos fugas de memoria.

Lo cuál, por supuesto, es algo falso.

Fugas de memoria en .NET

En .NET uno piensa que no hay fugas de memoria. En principio esto es cierto. Pero en general lo es sólo para las clases y componentes más sencillos. En efecto, cualquier componente complejo que use recursos de sistema, puede presentar fugas de memoria.

Para entender las fugas, debemos hacer algunas definiciones. Primero, diremos que cuando una liberación de recursos se ejecuta al instante, en cuanto se invoca, entonces ésta es una liberación determinista de recursos. Por ejemplo, la sentencia delete de C++ que vimos arriba es una liberación determinista: en cuanto llegamos a la sentencia, en ese momento se invocan destructores en orden, uno a uno, y se liberan los recursos. Una liberación no determinista es, por supuesto, cuando un recurso no se libera al instante. Es decir, se da la orden de liberar, pero no se hace al momento, e incluso puede tardar tiempo en ejecutarse. Se infiere que una liberación no determinista es totalmente asíncrona, respecto del proceso que contiene el elemento a liberar.

Pues bien, en .NET tenemos el colector de basura. Este colector libera los recursos, y lo hace de forma no determinista. Las reglas bajo las cuales esto funciona son algo complejas, pero básicamente se fija si un objeto es alcanzable o no. Por ejemplo:

class A
{
}

class B 
{
    public A a;
}

B b = new B();    // se ubica B en memoria.
b.a = new A();    // se ubica A en memoria.
…
b.a = null;       // ya no hay forma de recuperar a A. 

Este sencillo ejemplo es ilustrativo. B contiene un objeto A. Creamos un objeto B, y luego creamos un objeto A y se lo asignamos a B.a. Después de unas operaciones cualesquiera, asignamos B.a a null. En este momento, ya no hay forma de recuperar una referencia a A. Aunque el objeto A siga vivo, ya no puede recuperarse. A partir de este momento, el colector de basura puede decidirse a liberar o reclamar los recursos. Pero puede decidir no hacerlo, e incluso puede pasar mucho tiempo antes de que suceda. Es más, puede ser que el objeto B sea reclamado antes que A. Pero el punto es que a partir de ahí, como dicen en EE. UU., all bets are off.

Entonces, según lo anterior, ¿cómo puede haber fugas de memoria? La respuesta pasa por entender que una fuga de memoria nunca es permanente. En los sistemas operativos actuales, modernos, la memoria es liberada automáticamente por éstos, cuando el hilo ejecutor termina. Así, las fugas de memoria de hoy en día hacen más referencia a la pérdida temporal de memoria. Es decir: tenemos un objeto que debería ser liberado de memoria, y como no lo hacemos, vamos consumiendo más y más memoria. En casos extremos, ¡hasta podemos acabárnosla!

Es en este sentido que .NET permite las fugas de memoria. Considera este ejemplo. Tenemos una clase que utiliza un recurso de sistema (por ejemplo, usamos el sistema de geo-localización que viene con Windows 7). Estos recursos de sistema, como son provistos por el sistema operativo, no son administrados. Esto quiere decir que son responsabilidad del programador liberarlos (por supuesto, un recurso administrado es un recurso que el colector de basura libera en automático de forma no determinista).

class GeoLocalizador
{
    private IntPtr _manejadorSistema;

    public GeoLocalizador() {
        _manejadorSistema = InvocarApiDeSistema();
    }

    ...
}

Esto nos presenta un dilema, por supuesto. Cuando instanciamos nuestra clase GeoLocalizador adquirimos recursos no administrados, representados por la variable miembro _manejadorSistema de tipo IntPtr (usada para los famosos HANDLEs del API de Win32). Sin embargo, dado que el colector de basura se encarga de liberar la memoria administrada, pero no los recursos no administrados, los recursos de _manejadorSistema nunca serán liberados (hasta que termine el proceso, como pasaría con cualquier programa de Windows). Esto, mis estimados, es una fuga de memoria.

Liberación no determinista de recursos

¿Qué hacemos entonces para liberar estos recursos? En C++ tenemos el concepto de destructor. Un destructor es un método especial de una clase que SIEMPRE es invocado cuando el objeto deja de existir (bien porque sale de alcance, bien porque se invoca una sentencia delete). Es la contraparte del constructor.

En C# tenemos un concepto similar: los finalizadores. En principio, el finalizador es el mismo concepto de un destructor, y quienes venimos de C++ solemos llamarlos con este segundo nombre, probablemente por nostalgia. Pero gente fuera de C++ y C# los conoce como finalizadores (muy particularmente, la gente que viene de Visual Basic .NET).

En .NET, la clase Object tiene un método protegido virtual, llamado Finalize. ¿Qué te crees? Pues que ese método es, justamente, ¡el finalizador! Y como todas las clases heredan de Object, pues entonces todas las clases tienen su método Finalize. Para liberar recursos cuando el objeto sea reclamado, bastará con sobrescribir al método Finalize y colocar ahí el código para liberar.

class GeoLocalizador
{
    private IntPtr _manejadorSistema;

    public GeoLocalizador() {
        _manejadorSistema = InvocarApiDeSistema();
    }

    protected override void Finalize() {
        InvocarLiberación(ref _manejadorSistema);
        base.Finalize();
    }

    ...
}

Fácil, ¿no? Si escribes un código similar… éste no compilará. ¡Auch! Y es que resulta que por conveniencia, C# define una sintaxis especial para los finalizadores, y por tanto no puede sobrescribirse Finalize. Esto por un par de razones. Primero, para emular la sintaxis de C++. Segundo, para evitar que algún programador olvide llamar a base.Finalize, o que declaren como sealed al método y no sealed a la clase. Así, como decía, tenemos que usar una sintaxis similar a la de C++:

class GeoLocalizador
{
    private IntPtr _manejadorSistema;

    public GeoLocalizador() {
        _manejadorSistema = InvocarApiDeSistema();
    }

    ~GeoLocalizador() {
        InvocarLiberación(ref _manejadorSistema); // ok
    }

    ...

}

 

Como ves, el finalizador utiliza el símbolo ~ seguido del nombre de la clase. No puede tener modificadores, como public, protected, virtual, etc. Y ya no es necesario invocar a la clase base: el compilador lo hace por nosotros.

Una nota importante, sobre todo si vienes de C++. En este lenguaje, se recomienda siempre incluir un destructor, aunque éste esté vacío, por buena práctica. Todo lo contrario en C#: incluye destructores (i.e. finalizadores) sólo cuando vayas a liberar recursos. Si no, abstente. Esto, porque el colector de basura es más eficiente cuando no tiene que ejecutar finalizadores, por lo que incluir uno vacío sólo ralentiza el proceso de liberación de memoria.

Cabe resaltar que incluir finalizadores, aunque nos garantiza que nuestros recursos no administrados se liberen, no nos garantiza cuándo. Es decir, en un escenario tradicional como en nuestra clase GeoLocalizador, incluir el finalizador nos garantiza que no exista una fuga de memoria: después que la instancia muera, el objeto será reclamado y por tanto el recurso no administrado será liberado. Pero no nos garantiza que nuestra memoria sea óptima.

Considera esta situación: tienes una forma de Windows Forms donde capturas los datos de contacto de una persona. Entre esos datos, quieres capturar sus coordenadas geográficas, por lo que incluyes un botón que al hacer clic, utilice la clase GeoLocalizador para obtener las coordenadas. Ese método podría lucir así:

private _geolocButton_Click(object sender, EventArgs args)
{
    GeoLocalizador loc = new GeoLocalizador();
    loc.CalcularPosicion();
    _longText.Text = loc.Longitud.ToString();
    _latText.Text = loc.Latitud.ToString();
}

En el método creamos instancias de GeoLocalizador. Una vez que el flujo del programa sale del método, GeoLocalizador queda referenciada y por tanto es susceptible de ser reclamada por el colector de basura, y por tanto, cuando eso ocurra, se liberarán los recursos no administrados. Pero como ya vimos que esto puede no ser instantáneo, imaginemos: ¿qué pasa si el GC decide no colectar objetos, y el usuario hace clic 10 veces en el botón? ¡Tendremos 10 instancias con memoria sin liberar! Más aún: ¿qué pasa si el sistema operativo sólo permite una instancia del geo localizador? Tendríamos que esperarnos a que el GC colectara para poder volver a utilizar la clase GeoLocalizador, y no hay forma de saber cuándo ocurrirá eso.

En situaciones similares, vemos que a veces es necesario contar con liberación de recursos de forma determinista.

Liberación determinista de recursos

Una liberación determinista debería ser muy sencilla. O más o menos. Es decir, al final, si queremos liberar un recurso, podemos simplemente crear un método LiberarRecursos y ya, ¿no?

class GeoLocalizador
{
    private IntPtr _manejadorSistema;

    public GeoLocalizador() {
        _manejadorSistema = InvocarApiDeSistema();
    }

    ~GeoLocalizador() {
        InvocarLiberación(ref _manejadorSistema); // ok
    }

    public double Latitud { get; private set; }
    public double Longitud { get; private set; }

    public void CalcularPosicion() { 
        if (_manejadorSistema == null)
            throw new InvalidOperationException("No hay manejador. ");

        … // etc
    }

    public void LiberarRecursos() {
        if (_manejadorSistema != null) {
            InvocarLiberación(ref _manejadorSistema);
            _manejadorSistema = null;
        }
    }

    ...
}

GeoLocalizador loc = new GeoLocalizador();
loc.CalcularPosicion();
_longText.Text = loc.Longitud.ToString();
_latText.Text = loc.Latitud.ToString();
loc.LiberarRecursos();

Pues sí, hemos alcanzado una liberación determinista de esta forma. Al final, invocamos LiberarRecursos y pues ya liberamos los recursos, ¿no? Hay que tener cuidado, sin embargo, porque en teoría una vez que invocamos LiberarRecursos hemos dejado el objeto en un estado inválido. Es decir, en teoría una vez liberado, ya no debería ser posible ser utilizado. Sin embargo, C# no nos impide hacer esto:

GeoLocalizador loc = new GeoLocalizador();
…
loc.LiberarRecursos();
loc.CalcularPosicion(); // ¡oh-oh!

Después de LiberarRecursos, si invocamos a CalcularPosicion pues ya no tendría sentido y de hecho nuestro recurso no administrado ya está liberado y no podemos volver a utilizarlo. De hecho hemos puesto algunas salvaguardas para evitar que suceda: revisamos que si la variable interna en nula, lanzamos un InvalidOperationException. Sin embargo, nota que si se invoca a LiberarRecursos dos veces, no lanzará una excepción, simplemente la segunda vez no pasará nada.

He aquí una idea interesante:

~GeoLocalizador() {
    LiberarRecursos();
}

public void LiberarRecursos() {
    if (_manejadorSistema != null) {
        InvocarLiberación(ref _manejadorSistema);
        _manejadorSistema = null;
    }

}

Pues sí, parece lógico: el finalizador llamará a LiberarRecursos, así que ya tenemos implementada una liberación determinista y una no determinista.

Disponer objetos en .NET

La liberación determinista en .NET tiene un nombre en particular: disponer de un objeto (en inglés, to dispose an object). Y dado que .NET al final engloba muchas de las características del sistema operativo (Windows), es lógico ver que muchos componentes necesitan liberación determinista. Y por tanto es lógico suponer que hay alguna forma estándar de hacerlo.

En efecto, .NET tiene una interfaz muy muy muy importante: IDisposable. Esta interfaz implementa un método Dispose. Este método tiene la misma finalidad que el método LiberarRecursos que nos inventamos hace rato. Así, de esta forma en .NET se estandariza la liberación determinista. Podemos cambiar la clase para adaptarnos.

class GeoLocalizador : IDisposable
{
    private IntPtr _manejadorSistema;

    public GeoLocalizador() {
        _manejadorSistema = InvocarApiDeSistema();
    }

    ~GeoLocalizador() {
        Dispose();
    }

    public void Dispose() {
        if (_manejadorSistema != null) {
            InvocarLiberación(ref _manejadorSistema);
            _manejadorSistema = null;
        }
    }

    ...
}

Ahora sí, tenemos una regla a seguir: siempre siempre sieeeeeeeeeeeempre que veas que una clase en .NET implementa la interfaz IDisposable, lo más probable es que se pretenda que los recursos se liberen de forma determinista. Si tienes un objeto así, cuando ya no necesites el objeto, manda llamar a Dispose. Hazlo así muchcahón / muchachona, y evitarás tener fugas de memoria.

Nota: algunas clases implementan IDisposable, y por su naturaleza, también implementan un método Close. Semánticamente, Close puede significar lo mismo que Dispose (por ejemplo, el cerrar una conexión a base de datos implica liberar los recursos también). Por lo que los métodos Close suelen llamar al Dispose. No pasa nada si primero llamas a Close y luego a Dispose, pero bueno, no es necesario hacer la doble llamada de hecho.

Esto nos presenta un dilema interesante. Supongamos que nuestro método CalculaPosicion puede lanzar un InvalidOperationException cuando el localizador no pueda conectarse al satélite o no pueda hacer una triangulación por WiFi o por cualquier motivo. Entonces corremos el riesgo que el Dispose no se invoque. Pero eso lo podemos solucionar con un buen try-finally o try-catch-finally.

GeoLocalizador loc = null;

try {
    loc = new GeoLocalizador();
    …
} finally {
    if (loc != null)
        loc.Dispose();

}

Nota que para usar esta construcción, tienes que declarar la variable fuera del try-finally.

Los bloques try-catch-finally me gustan, de hecho, pero hay veces en que es medio tedioso hacer esto. Digo, el try-finally es entendible cuando hay un catch de por medio. Pero si no hay catch, puro try-fianlly, parece una construcción muy grande nada más para liberar un recurso. Pues bien, C# tiene un poco de azúcar sintáctica para nosotros: la sentencia using.

Esta sentencia tiene la siguiente forma:

using (declaración = asignación)
{
    // código aquí
}

Dentro del using debe haber una variable, la cual puede estar seguida de una asignación o no. El tipo de dicha variable DEBE implementar la interfaz IDisposable. Si no lo hace, tendrás un error de compilación (i.e. using (int i = 42) { } generará error de compilación). Luego, entre las llaves se pone el código que utiliza la variable en cuestión. Lo interesante sucede cuando alcanzamos la llave de cierre: en ese momento el compilador, tras bambalinas, ¡INVOCA al método Dispose! Precisamente por eso es que el tipo de la variable debe implementar IDisposable. Veamos como quedaría nuestro ejemplo:

using (GeoLocalizador loc = new GeoLocalizacion())
{
    loc.CalcularPosicion();
    _longText.Text = loc.Longitud.ToString();
    _latText.Text = loc.Latitud.ToString();
} // loc.Dispose() llamado tras bambalinas

// aquí loc ya no está disponible

Si estamos seguro que el constructor no lanza excepción, podemos hacerle así:

GeoLocalizador loc = new GeoLocalizacion();
using (loc)
{
    loc.CalcularPosicion();
    _longText.Text = loc.Longitud.ToString();
    _latText.Text = loc.Latitud.ToString();
} // loc.Dispose() llamado tras bambalinas

loc.CalcularPosicion(); // ¡aquí ya lanza una excepción!

Por cierto, en CalcularPosicion pusimos que se lanzara un InvalidOperationException si ya habíamos depuesto el objeto, ¿verdad? Hay una excepción mejor: ObjectDisposedException.

 

public void CalcularPosicion() { 
    if (_manejadorSistema == null)
        throw new ObjectDisposedException();

    … // etc

}

Probablemente también sea mejor cambiar la condición y llevar una variable interna, bool _disposed, que indique si el objeto ya fue depuesto o no. Pero bueno, ya dependerá de ti cómo manejar el estado de tu objeto.

Liberando recursos en objetos compuestos

Hasta ahora hemos visto cómo liberar recursos no administrados vía el destructor, y cómo forzar liberación de recursos vía el método IDisposable.Dispose. Ya vimos también que es una buena práctica que cada que creemos un objeto que implemente IDisposable, invoquemos su método Dispose una vez que dejemos de utilizarlo. Eso está bien cuando creamos objetos en un bloque de código como un método. Pero ¿qué pasa si son miembros de otra clase?

Para ilustrar esto, imaginemos que crearemos una clase llamada Mapa. Esta clase tiene un miembro: GeoLocalizador, a partir del cual pinta la ubicación actual de la persona en el mapa. Asumamos que la clase Mapa tiene que cargar imágenes como recursos no administrados, aprovechando recursos del sistema operativo. Esta clase podría lucir similar a esta.

class Map : IDisposable
{
    private GeoLocalizador _loc; // recurso administrado
    private IntPtr _mapaBits; // recurso no administrado

    public Map() {
        _loc = new GeoLocalizador();
        _mapaBits = InvocarApiDeSistema();
    }

    ~Map() {
        ???
    }

    public void Dispose() {
        ???
    }

    … 
}

Aquí el punto es importante. Tenemos un miembro administrado y uno no administrado. Sí, _loc es administrado desde el punto de vista de Map, puesto que es una clase de .NET. El colector de basura reclamará a _loc en algún momento y ésta liberará recursos, por tanto se considera que es un recurso administrado. ¿Cómo implementamos Dispose en este caso?

Pues bien, primero pensemos en el recurso no administrado. Éste tiene que estar en el finalizador para asegurar que de una u otra forma se liberen los recursos. Ahora bien, la liberación del recurso administrado no tiene sentido hacerse en el finalizador. ¿Por qué? Pues porque puede ser que para cuando se invoque el finalizador ya se haya liberado el objeto GeoLocalizador. Recordemos que los destructores son no determinados y por tanto asíncronos, así que no podemos hacer esto:

~Map() {
    LiberarMapaBitsApi(ref _mapaBits);
    _loc.Dispose(); // en la mauser
}

Eso nos da en la torre, pues si _loc ya fue liberado, no podemos invocar _loc.Dispose toda vez que el objeto ya no existe. Por tanto, regla del mundo: en el finalizador, nunca intentes liberar recursos administrados.

Sin embargo, para la liberación determinista de Map, debemos invocar necesariamente a _loc.Dispose. Esto nos lleva al siguiente dilema: el destructor debería invocar a Dispose para liberar recursos no administrados, y la invocación directa de Dispose debería liberar tanto recursos administrados como no administrados. Es decir, tenemos que diferenciar cuándo estamos liberando de forma determinista y cuando no. Esto podemos hacerlo de esta forma: creamos un método Dispose(bool) protegido, al cuál le pasamos una variable: bool indica que estamos liberando de forma determinsta y por tanto liberamos recursos administrados y no administrados. Un false indica que estamos liberando de forma no determinista, y por tanto sólo liberamos recursos no administrados. Entonces sí: desde Dispose(), invocamos a Dispose(true), y desde el destructor, invocamos a Dispose(false).

class Map : IDisposable
{
    private GeoLocalizador _loc; // recurso administrado
    private IntPtr _mapaBits; // recurso no administrado

    public Map() {
        _loc = new GeoLocalizador();
        _mapaBits = InvocarApiDeSistema();
    }

    ~Map() {
        Dispose(false);
    }

    public void Dispose() {
        Dispose(true);
    }

    protected virtual void Dispose(bool isDeterministic)
    {
        if (isDeterministic) {
            _loc.Dispose();
        }

        LiberarMapaBitsApi(ref _mapaBits);
        _loc = null;
        _mapaBits = null;
    }

    … 

}

¡¡Braaaaaaaaavo!! ¡Qué inteligente eres Fernando, guaaaau! Ajem… bueno, a decir verdad… esto que hemos hecho es en realidad un patrón de diseño. Ya sabes, como el singleton o el factory. Es un patrón de diseño de .NET, y se llama "Disposable Object Pattern". Es en realidad un patrón de diseño muy utilizado, y es la forma en la que Microsoft recomienda que se gestionen recursos administrados y no administrados.

Por supuesto, esta es una versión incompleta. Hay algunas cosas que decir. En primer lugar, para el nombre de variable yo emplee isDeterministic. Esto, para mantenerlo en concordancia con los términos que hemos utilizado. En la literatura de .NET, sin embargo, suelen utilizar otro nombre: disposing, queriendo decir que disposing == true significa que el método Dispose fue invocado. En fin, por supuesto puedes ponerle el nombre que quieras a la variable, no importa.

En segundo lugar, omitimos en el ejemplo el marcar que el objeto ya ha sido depuesto. Esto es muy importante, no lo olvides: un objeto que ha sido depuesto debe quedar en estado inválido y no debe poder volver a utilizarse. Es la convención, por supuesto. Puedes ir contra ella, pero le estarías dando al traste con la semántica de IDisposable.

Otra cuestión importante: un método Dispose debería ser posible llamarse en varias ocasiones. La implementación debería ser consciente de esto, y NO LANZAR EXCEPCIONES. Por favor. No lances excepciones desde un destructor. No lances excepciones desde el método Dispose. Es buena práctica y sentido común. Esto nos da una razón más para llevar el control sobre si el recurso ha sido liberado o no. ¡Ala pues!

class Map : IDisposable
{
    private GeoLocalizador _loc;
    private IntPtr _mapaBits;
    private bool _disposed;
    private Size _size;

    public Map() {
        _loc = new GeoLocalizador();
        _mapaBits = InvocarApiDeSistema();
        _disposed = false; // naturalmente…
        ...
    }

    ~Map() {
        Dispose(false);
    }

    public void Dispose() {
        Dispose(true);
        GC.SupressFinalize(this); // ¿¡¿y ahora?!?
    }

    protected virtual void Dispose(bool isDeterministic)
    {
        if (!_disposed) {
            if (isDeterministic) {
                _loc.Dispose();
            }

            LiberarMapaBitsApi(ref _mapaBits);

            _loc = null;
            _mapaBits = null;
            _disposed = true;
        } 
        // si ya fue depuesto, no hacemos nada. 
    }

    private void EnsureDisposed()
    {
        if (_disposed)
            throw new ObjectDisposedException();
    }

    public Size Size
    {
        get { 
            EnsureDisposed();
            return _size;
        }
        set {
            EnsureDisposed();
            _size = value;
        }
    }

    public void DrawMap()
    {
        EnsureDisposed();

        … // etc
    }

    … 
}

Bien, podemos ver que creamos una variable _disposed que indica si el objeto ha sido depuesto o no. En nuestro método Dispose(bool), revisamos el valor de esta variable para determinar si liberamos los recursos o no. Si no los liberamos (i.e. _disposed == true) simplemente no hacemos nada, en lugar de lanzar una excepción. Sin embargo, en cualquier otra propiedad o método, revisamos si _disposed es true. De serlo, entonces lanzamos un ObjectDisposedException. Así dejamos súper claro que el objeto ya no sirve.

Por último, vemos que en el método Dispose(), tras la llamada al método interno Dispose(bool), hemos puesto una sentencia que no habíamos visto antes: GC.SupressFinalize. ¿De qué se trata?

En realidad es una forma de mejorar el rendimiento (performance) de la aplicación. System.GC es en realidad una clase que se encarga de exponer algunos métodos estáticos para que podamos dar ciertas instrucciones al colector de basura (GC == Garbage Collector). SupressFinalize es uno de sus métodos, que tiene un parámetro de tipo object. Invocar a ese método es decirle al colector de basura que cuando reclame la memoria del objeto que se pasa como parámetro, ya no se invoque al finalizador (destructor, método Finalize). En este caso, el destructor invoca a Dispose(false). Pero Dispose invoca a Dispose(true), por lo que si ya se liberaron los recursos, no tiene caso que se invoque a Dispose(false) de nueva cuenta. Entonces por eso le decimos al colector de basura que ya ni se moleste. Esto mejorará el rendimiento de la aplicación un poco, porque como hemos dicho, la invocación de finalizadores puede ser costosa.

Naturalmente hay que tener cuidado y no invocar a lo güey SupressFinalize, pues es fácil provocar problemas.

GeoLocalizador loc = new GeoLocalizador();
GC.SupressFianlize(loc); // fuga de memoria asegurada

So say we all

Sean dichas unas últimas palabras antes de cerrar esta entrada. De verdad ten mucho cuidado con la liberación de recursos. En repetidas ocasiones me he topado con programadores que no se preocupan por los recursos, pues .NET los libera. Y claro, cuando liberas una aplicación para 10,000 usuarios y 100,000 transacciones, nos preguntamos por qué está tan lenta. O una aplicación web, nos preguntamos por qué se consume tanta memoria. De verdad, te salvará muchos problemas. Cuando entrevisto programadores, es una de las preguntas que hago, y si no la contestan bien, no continúan. Ya he tenido suficientes problemas por ello como para hacerlo así.

So say we all.

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

C# 101: símbolos de depuración


En un proyecto estándar generado por Visual Studio, existen dos tipos de configuraciones básicas: Debug y Release. La primera, Debug, suele definir opciones que permiten al compilador generar información sobre el programa: variables, información sobre objetos y clases, etc. Esta información la solemos referir como símbolos de depuración.

Generar los símbolos de depuración son necesarios, pues los depuradores los leen para poder presentar la información al programador durante, digamos, un breakpoint. El programa compila a código binario (o MSIL, en el caso de .NET) y por tanto es imposible saber si quiera el nombre de las variables. Los símbolos de depuración ayudan al depurador con esto. Incluso, esta información permite realizar enlazado de objetos incrementales, reduciendo considerablemente el tiempo de compilación y generación del ensamblado.

Ahora bien, todos los símbolos de depuración se almacenan en una base de datos, llamada Program Database (PDB). Este archivillo, que puedes encontrar en el directorio de salida con la extensión PDB, es de hecho una base de datos que puede ser abierta con el motor SQL Server Compact Edition (i.e. puedes usar el SQL Server Management Studio).

El depurador, que es quien usa el PDB, lo carga al momento de iniciar una sesión. Lo busca en el directorio donde se encuentra el EXE. La parte importante es que el depurador revisa que coincida, a nivel binario, todo lo que dice el PDB con el ejecutable mismo. Si no coinciden, el PDB no es cargado. Si el PDB no existe, el Visual Studio nos presenta una ventana para que escojamos el directorio donde se encuentra. Esto último es importante, pues nos permite cargar símbolos que se encuentren en otras ubicaciones.

Hay un escenario en particular donde esto es útil, y lo ilustraré con una anécdota personal. Cuando desarrollamos nuestras aplicaciones, usualmente llega el momento de pasarlas a algún servidor de Q&A, antes del Go Live. Ahí, los clientes suelen realizar sus pruebas. En esta ocasión, el sistema estuvo jalando bastante bien, hasta que en una ocasión un usuario logró hacer tronar la aplicación bajo ciertas condiciones. Rápidamente nos reportó el problema.

Pues resultó que no pudimos duplicar el problema en nuestro ambiente de desarrollo. Estuvimos tres días intentándolo, copiando las características del servidor de Q&A. Nada. Funcionaba a la perfección. Hasta intentamos ver que el problema no estuviera "entre la silla y el teclado". Nada. Los logs y traces de poca ayuda fueron.

Desafortunadamente, la fecha para la liberación del sistema se acercaba peligrosamente, así que no nos quedó de otra: solicitamos permiso para instalar el Visual Studio en el servidor de Q&A para de ahí ejecutar el programa y ver qué sucedía. Naturalmente, nos mandaron a freír espárragos: no sólo necesitaban autorización del corporativo, la cuál tardaría mínimo un mes, sino que no estaba permitido instalar programas en los servidores de Q&A, y que el hacerlo provocaría que nos impidieran el paso a producción, pues ellos no podrían certificar que la app funcionara bien.

En fin, tras varias reuniones y alegatos, propusimos que se hiciera todo de forma remota: a final de cuentas, el Visual Studio podía conectarse de forma remota. Sin embargo, les dijimos, necesitamos que coloquen el archivo PDB… y ardió Troya: que no, no se podía alterar el ambiente de Q&A, yada yada yada. En fin, terminamos generando el PDB y gracias a esta opción de seleccionar el archivo, pudimos conectarnos de forma remota con el PDB en nuestro equipo local. De esta forma encontramos que era el Directorio Activo quien daba un timeout, y pudimos solucionar el problema.

Un truco antes de partir

Bueno, esta entrada ha sido más informativa que otra cosa. Pero voy a dejar un pequeño truco antes de concluirlo, para que no te vayas en cero.

Cuando depuramos un programa, podemos ver el valor de las variables de tipo de dato en las ventanas de Quick Watch del Visual Studio.

clip_image001

Ahora bien, esto funciona padre para tipos de datos simples como int, bool o string. ¿Qué pasa con tipos de datos complejos, como un DataTable? ¿Qué pasa con las clases que nosotros creamos?

Si ves la imagen anterior, en el caso del DataTable muestra este valor: {Length=1}. Es decir, que el DataTable explícitamente indica que el valor que interesa es el valor de la propiedad Length. ¿Cómo puedo hacer esto para mis clases?

Este es el truco: para hacer esto, tenemos que utilizar la clase DebuggerDisplayAttribute, en el espacio de nombres System.Diagnostics. En este atributo, al constructor le pasamos una cadena de texto con un formato sobre cómo va a mostrarse el valor. Por ejemplo:

[DebuggerDisplay("Length = {Length}")]
class MyClass
{
    public int Length { get; set; }
}

Y eso es suficiente. Con esto, aparecerá una cadena de texto donde el texto entre llaves se reemplaza por el valor del elemento: miembro o propiedad, cuyo nombre corresponda con el nombre entre las llaves.

Y este es el truco. Puedes revisar la documentación de DebuggerDisplayAttribute para mayor información.

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

C# 101: directivas de preprocesador


Recuerdo mi época de programador de C++. No fue hace mucho, mi última aplicación la terminé hace unas tres semanas. Quizás debí decir: "recuerdo cuando comenzaba a programar en C++", y entonces sí nos vamos 10 años atrás… En fin, como sea. En C y C++, uno podía ver código como este:

#ifdef _WIN32
#ifdef UNICODE
#define _tcscpy wcscpy
#elif
#define _tcscpy strcpy
#endif
#endif

Todas las directivas que comienzan con un gato (singo de libra, de número o como se llame en tu villa) son directivas de pre-procesamiento. Esto quiere decir que hay un software antes que el compilador, que básicamente sustituye los símbolos (llamados macro) por otros, es decir, los pre-procesa. En el caso anterior, cualquier llamada a _tcscpy se sustituido por wcscpy o por strcpy, dependiendo de si el símbolo _WIN32 y el símbolo UNICODE están definidos.

En fin, en C# también tenemos directivas de pre-procesamiento, aunque en realidad es el mismo compilador. A diferencia de C y C++, no pueden crearse macros. Pero sí se puede utilizar para crear compilaciones condicionales, que nos ayudan a generar diferentes ensamblados.

Definición de símbolos

Las primeras directivas que veremos son las directivas para definir símbolos. Estas son #define y #undef. A diferencia de C y C++, #define no asigna valor a una macro, sino que simplemente define el símbolo. Éste podrá ser utilizado después, pero hasta ahí.

#define DEBUG

public void TraceDebugInfo()
{
}

#undef DEBUG

En el ejemplo anterior, definimos el símbolo DEBUG, y luego lo indefinimos. Estas directivas no son muy útiles por sí mismas, pero se convierten en útiles cuando las combinamos con directivas de decisión.

Definir un símbolo con #define es equivalente a definir un símbolo al momento de compilar mediante el parámetro /define.

Decisión de compilación

La decisión de compilación significa que una porción determinada de código se compila si se cumple una condición en particular. La condición que debe cumplirse es si un símbolo está definido o no. Pueden concatenarse símbolos con los operadores lógicos &&, || y !, pero a eso se limita. Ahora sí hace más sentido el #define, ¿no?

public void Trace(string msg)
{

#if DEBUG && ENABLE_TRACE
    Debug.WriteLine(msg);
#elif !DEBUG && ENABLE_TRACE
    File.WriteAllText("C:\\log\\debug.log", msg);
#else
    Console.WriteLine(msg);
#endif

}

#if es la directiva para evaluar un símbolo. Si la expresión se evalúa a true, todo lo que haya después y hasta alguna otra directiva complementaria, se compila. Si no, no. Tanto #else como #elif se usan como alternativas, pero #elif permite poner una expresión a evaluar, mientras que #else no. Finalmente, el bloque debe cerrarse con un #endif.

En el ejemplo anterior, si compilamos con /define:DEBUG /define:ENABLE_TRACE, el mensaje se imprimirá en la consola de depuración (útil sólo cuando depuramos desde Visual Studio). Si compilamos con /define:ENABLE_TRACE solito, escribimos todo a un archivo de texto. Si no definimos ENABLE_TRACE, entonces escribimos en la consola de la aplicación. Por supuesto, en lugar de la compilación con /define, podemos usar #define.

Hemos hablado de cómo compilar con /define para definir símbolos, y por tanto utilizar las directivas #if, #elif, #else, #endif. Realmente así es como hace sentido, pues hacer los #defines no siempre es muy práctico, pues un cambio implica modificar el código fuente.

Visual Studio incluye un componente llamado Configuration Management. Este componente permite definir construcciones (builds). Cada construcción define qué ensamblados va a compilar, qué ensamblados referenciar, y otras configuraciones de compilación, etc. De hecho, por defecto, el Visual Studio nos genera un par de construcciones: Debug y Release. Si comparas ambas, verás que Debug tiene opciones de depuración habilitadas, que no suelen ser necesarias en la versión de producción.

Pues bien, entre las cosas que nos permite configurar están precisamente los símbolos con los que queremos compilar. En las propiedades del proyecto, tab Build, nos aparecen las configuraciones existentes. Abajo, aparece "Conditional combination symbols", ahí podemos escribir los símbolos que queramos definir. Adicionalmente, tenemos dos opciones: definir la constante DEBUG, que ya conocimos, y la constante TRACE.

clip_image001[4]

Adicional a las construcciones predefinidas, podemos crear las nuestras personalizadas. Quizás queremos establecer símbolos para diferentes plataformas (Windows, Silverlight, 32 vs 64 bits, etc.). Para crearlas, seguimos estos pasos.

1.- Seleccionar la solución o proyecto en el Solution Explorer.

2.- Seleccionar el menú Build y escoger la opción Configuration Manager.

3.- En Active Solution Configuration, seleccionamos la opción "<New>". Con esto nos pedirá el nombre y si queremos basarnos en alguna configuración. Terminando, hacemos clic en OK.

4.- Una vez creada, ya podemos cambiar la configuración que hemos creado.

Más información sobre este tema:

1.- how to: create and edit configurations.

2.- how to: modify project properties and configuration settings

Advertencias y errores

A veces querremos indicar alguna condición bajo la cuál queramos lanzar una advertencia o algún error. Por ejemplo, supongamos que todavía no hemos probado el código en modo de depuración. Entonces queremos lanzar una advertencia al compilar.

#if !DEBUG

#warning No hemos probado este código en modo Release. ¡Aguas!

#endif

O por ejemplo, nuestro código no va a soportar aplicaciones de 32 bits para Windows NT. Podemos definir símbolos con /define: para Windows NT.

#if WINNT && PLATFORM_x86

#error No se soporta WINNT en 32 bits. 


#endif

Vemos que se usa #warning y #error para ello, seguido del mensaje que queramos mostrar.

Por otro lado, tenemos que el compilador de C# genera muchas advertencias. Esto, independientemente de los errores, por supuesto. El compilador tiene diferentes niveles de advertencias, y la finalidad de éstas consiste en que los programadores reciban recordatorios sobre potenciales problemas detectados en código, o posibles omisiones. Considera este método:

void foo()
{
    int i = 0;

    throw new Exception();

    goo();
}

Este código genera dos advertencias. En primer lugar, nos dice que la variable i es inicializada pero nunca utilizada. Lo cual tiene sentido: ¿para qué declarar una variable si nunca la utilizas? Por otra parte, también nos da otra advertencia: el método foo tiene código inaccesible que nunca se ejecuta. En efecto, dado que el throw hace que la función termine anticipadamente, goo nunca será ejecutada. De ahí la advertencia.

Usualmente las advertencias deben ser corregidas. Pero en dado caso de que el código que provoca la advertencia sea intencional, entonces podemos deshabilitar la advertencia para que el compilador no la muestre. Para ello usamos la directiva #pragma warning disable, seguida del número de advertencia que queremos deshabilitar.

class Base
{
    public virtual void foo() { }
}

class Derivada : Base
{
#pragma warning disable 0108 

    // 'member1' hides inherited member 'member2'. Use the new keyword 
    // if hiding was intended.
    public void foo()

#pragma warning enable 0108
}

Vemos también que las advertencias pueden ser habilitadas de nuevo usando el #pragma warning enable, seguido del número de advertencia.

Regiones

En C# podemos crear regiones de código y asignarles una descripción. Esto lo logramos mediante las directivas #region y #endregion.

class Employee
{
#region Atributos
    private int _id;
    private string _name;
    private double _income;
#endregion

#region Propiedades
    public int ID { … }
    public string Name { … }
    public double Income { … }
#endregion

#region Métodos
    public void CalcIncome(DateTime from, DateTime until) { … }
    public override string ToString() { … }
#endregion
}

Esta es una forma de utilizarse. La verdad es que desde aquí no se ve que haga nada. Y de hecho no hacen nada: las regiones sirven para dividir el código, pero no afecta al compilador. De hecho sólo tienen utilidad dentro de Visual Studio. El Visual Studio permitirá expandir y contraer regiones, haciendo más fácil la navegación en el IDE.

Misceláneos

Existen un par de directivas misceláneas. La directiva #line permite cambiar la línea del código fuente, afectando los mensajes de advertencia y errores de compilación que puedan suceder.

void foo() 
{
    // CS0429
    if (false) {
        Console.WriteLine("Foo");
    }

}

El código anterior nos muestra lo siguiente, suponiendo que void foo sea la primera línea:

Warning CS0429: Unreachable expression code detected in file ‘File.cs’ line ‘5’

Ahora usemos la directiva #line:

#line 37
void foo() 
{
    // CS0429
    if (false) {
        Console.WriteLine("Foo");
    }
}

Ahora tenemos:

Warning CS0429: Unreachable expression code detected in file ‘File.cs’ line ’42’

Con #line, movimos el número de línea. Ahora, ¿cómo para qué sirve esto? Ni idea. Pero lo puedes hacer, si quieres sacarle canas blancas al resto de tu equipo de trabajo.

La otra directiva miscelánea es el #pragma checksum. Esta directiva genera "checksums" para un archivo determinado. Un checksum es un número o identificador que se asigna a algún elemento, para comprobar que se esté utilizando la última versión. En particular, esto es útil para págians de ASP.NET, ya que los ASPX están separados de los ensamblados.

Este es un ejemplo:

#pragma checksum "file.cs" "{3673e4ca-6098-4ec1-890f-8fceb2a794a2}" 
     "{012345678AB}" 

That’s that!

Pues eso es todo. Esas son todas las directivas de preprocesador existentes en C# hasta la versión actual. Ciertamente la parte importante es #if / #endif y #define. Se les puede sacar mucho provecho. Sin embargo, no hay que abusar: un error común que no queremos heredar de C y C++ es precisamente el llenarnos de directivas.

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

C# 101: eventos y retro-llamadas


Este tema, pienso yo, sobra un poco para todos aquellos que ya conocen C#. Sin embargo, viene dentro de los temas a tratar para la certificación 70-483, así que -nevertheless- lo incluiré, pero a guisa de cuestionario, para que sea más concreto.

¿Qué es un evento?

Un evento es un mecanismo mediante el cual una clase permite a objetos externos subscribirse a un delegado, pero sin poder invocarlo. El delegado, en cambio, pue…

Pera, ¿qué es un delegado?

¡Ah! Un delegado es un objeto que permite hacer retro-llamadas, también llamadas callba…

¡Pero no! ¿Qué es una retro-llamada?

Pues un puntero a una función. En fin, te decía que las retro-llama…

Carambas, ¿por qué no comienzas por el principio?

Vale, vale, ¡tranquis! En lenguajes como C y C++, existe un concepto mediante el cual uno puede declarar una variable que, en lugar de apuntar a una ubicación de memoria que contenga un valor, como un entero o una cadena de texto, contienen la ubicación en memoria de una función. Algo así:

void foo(int val) {

    printf("foo %d", val);

}

void goo(int val) {

    printf("goo %d", val);

}

 

Aquí tenemos un par de funciones de C que imprimen en pantalla el valor que les pasan como parámetro, precedido por una cadena de texto. La firma es un valor de retorno void, más un parámetro de tipo int. Un puntero a una función, bajo lo escrito anteriormente, sería así:

typedef (void *func)(int);

Y ahora podemos utilizar func como una variable, sólo que su tipo de dato es una función que regresa void y toma un parámetro int.

typedef (void *func)(int);


func f = foo;

f(5); // imprime "foo 5";

f = goo;

f(42); // imprime "goo 42";

Cuando una función tiene un parámetro de tipo puntero a función, y ésta es invocada bajo ciertas circunstancias desde dicha función, se dice que la función pasada como parámetro es una retro-llamada, ya que será invocada usualmente para indicar alguna notificación: errores, cambios de estado, o que todo salió bien. Por su nombre en inglés, una retro-llamada es un callback.

Ok, creo que ya entendí. Pero ¿para qué sirve un callback?

Bueno, un ejemplo es el que ya dimos. Una función necesita notificar algo a algún tercero, pero no quiere dejarlo fijo, sino que sea parametrizable. Piensa en una barra de progreso. La función Foo hace cálculo intensivo, y pide un callback para notificar el porcentaje de avance. Cada que avanza algo, Foo invoca al callback con el nuevo valor. Ya cada quién decide qué función pasar como callback: una que imprima algo en consola, una que guarde en log en texto plano, etc.

Pero también hay más. Piensa que usamos un array o lista de punteros a funciones. ¡Podríamos crear todo un sistema de notificaciones! De hecho, a través de los callbacks uno puede implementar el patrón de observador: en lugar de una función Update, invocamos a cada callback dentro del array.

Los callbacks son muy utilizados en todos lados en el API de Windows 32 y en general en el mundo de C. En C++, aunque también válidos y utilizados, se utiliza más el concepto de functor.

¿Qué es un functor de C++?

Un functor en C++ es una clase con el operador () sobrecargado, de tal suerte que puede comportarse como una función.

 

class print {

    public:
       std::string msg;
        int times;

        print() : msg("Hola mundo"), times(1) { }

        void execute() {

            for (int i = 0; i < times; i++)
                std::cout << msg << std::endl;

        }

        void operator()() {
            execute();
        }

        void operator()(int tms) {
            times = tms;
            execute();
        }

};

print p;
p.msg = "Hallo Welt!";
p.times = 1;
p(); // imprime Hallo Welt!
p.msg = "Auf wiedersehen Welt!";
p(2); // imprime dos veces Auf wiedersehen Welt!

A pesar de que p es un objeto, ¡se invoca como si fuera una función! Esto es muy útil porque pueden crearse clases de plantillas que permitan utilizar callbacks o functores, transparentemente.

Bueno: callbacks, functores, ¿qué tienen que ver con C#?

Ah, pues que los callbacks son objetos muy útiles. Sin embargo, los punteros son inherentemente inseguros, y ciertamente los punteros están casi prohibidos en C#. Sin embargo, los functores son una buena idea, y en .NET la retomaron, haciéndolos más robustos, más seguros y más fáciles de utilizar. Además, en C# se integraron al lenguaje: ¡les asignaron su propia sintaxis y palabras reservadas y toda la cosa!

A estos functores de .NET y C#, sin embargo, se les conoce con otro nombre: el de delegados.

Wow: ¿y qué es un delegado entonces?

Un delegado es una clase (que hereda de la clase base System.Delegate, aunque la herencia la hace el compilador de C# por ti) que define una firma de un método (tipo de retorno más firma de parámetros) mediante la cual define una colección de callbacks que pueden ser invocadas al unísono.

Usando terminología de .NET: un delegado es un tipo de dato que referencia uno o más métodos, y que se comporta exactamente como un método.

¿Y cómo se usa un delegado?

Para declarar un delegado, se indica el modificador, seguido por la palabra reservada delegate, el tipo de retorno del método, el nombre del delegado, y la lista de parámetros. Por ejemplo:

public int delegate Operacion(int a, int b);

Luego, para instanciar un delegado, pues es igual que con cualquier tipo, con la salvedad que en el constructor hay que pasar el nombre del método al que hace referencia.

int Suma(int a, int b) { return a + b; }
int Mult(int a, int b) { return a * b; }

Operacion op = new Operacion(Suma);
int val = Suma(5, 10); // val == 15
op = new Operacion(Mult);
val = op(5, 10); // val == 150

A partir de .NET 2.0, los delegados no necesitan la sintaxis "new Operación", sino que pueden asignar directamente al método:

Operacion op = Suma;
int val = Suma(5, 10); // val == 15
op = Mult;
val = op(5, 10); // val == 150

¿Dónde puedo usar un delegado?

¡Donde quieras, tío, donde quieras! O mejor dicho, donde puedas usar cualquier variable: a nivel de clase, de método, como parámetro de un método, etc. ¡Las posibilidades son amplísimas!

¿Y siempre debe saberse el tipo del delegado?

Mmm… técnicamente sí. Sin embargo, en C# existe el concepto de "delegado anónimo", o método anónimo. Esto es un delegado que no tiene un tipo explícito, sino que se define un cuerpo directamente. El tipo de dato existe, lo genera el compilador de C#, pero no es explícito para el programador. De ahí que se le denomine anónimo. Esto se hace así:

var suma = new delegate(int a, int b) { return a + b; }
sum(5, 10); // regresa 15

 

Necesitamos utilizar "var" porque no sabemos el tipo de retorno. Si te das cuenta no definimos el nombre del delegado, sólo su firma. En este caso, tenemos dos parámetros de tipo entero, y un parámetro de retorno de tipo entero también. Esto, aunque no es explícito, lo define el "return a + b". Es decir, el compilador deduce el tipo de retorno. Si no hubiera return, el tipo de retorno sería void.

Por cierto, ¡también puedes usar expresiones lambda! ;-)

var mult = (int a, int b) => a * b;

mult(5, 10); // regresa 150

¿Existen delegados en .NET Framework que pueda utilizar?

¡Por supuesto! Hay una gama importante de delegados ya existentes en la biblioteca base de clases. Mostrarlas aquí llevaría mucho tiempo, pero podemos ver algunos. Quizás el más conocido es EventHandler y su versión genérica, EventHandler<T>. Estos delegados se usan como estándares para manejadores de eventos, que veremos más adelante.

El delegado Action representa eso: una acción. Un método que regresa void y no tiene argumentos. Hay muchas variantes de Action, hasta con quince parámetros. Veamos un ejemplo sobre cómo usar este delegado.

void Execute<T>(IEnumerable<T> items, Action<T>  action)
{
    foreach (T item in items)
    {
        action(item);
    }
}

int[] nums = new int[] { 5, 10, 15, 20, 25 };
Action<int> action = delegate(int x) { Console.WriteLine("Número {0}", x); };

Execute(nums, action);

 

Similar a Action, tenemos a Func. La diferencia es que Func sí regresa un valor. Representa una función de cero a dieciséis parámetros posibles, según la versión que se incorpore.

IEnumerable<int> Transform(IEnumerable<int> input, Func<int, int> func)
{
    foreach (int n in input) {
        int value = func(n);
        yield return value;
    }
}

int[] nums = new int[] { 1, 2, 3, 4, 5 };
Func<int, int> sqr = x => x * x;
Func<int, int, int> cube = x => x * x * x,

var ret1 = Transform(nums, sqr); // ret1: { 1, 4, 9, 16, 25 }
var ret2 = Transform(nums, cube); // ret2: {1, 8, 27, 64, 125 }

En el ejemplo anterior, creamos un método que toma una colección y la transforma, según una función que pasemos como parámetro. Luego creamos dos delegados función: uno regresa el cuadrado de un número, y el otro el cubo.

Un tercer delegado es Predicate<T>. Este delegado representa una función que toma un parámetro de entrada y regresa verdadero o falso. El significado del predicado depende de su contexto, por supuesto. Pero básicamente es eso: regresar un valor booleano a partir de un parámetro. Por ejemplo, un predicado que separe números pares y nones podría lucir así:

int[] nums = new int[] { 1, 2, 3, 4, 5 };
Predicate<int> p = x => x % 2 == 0;

foreach (int n in nums)
{
    Console.WriteLine("Es par? {0}",  p(n));
}

De hecho, los predicados se usan mucho en colecciones. Por ejemplo, método Array.Exists determina si un array tiene un elemento que concuerde con el criterio pasado como parámetro.

int[] nums = new int[] { 1, 2, 3, 4, 5, 42 };
bool exists = Array.Exists(nums, x => x == 42); // regresa true

¡Ay jolines! Nota que pasamos una función lambda directamente como predicado… C# permite convertir lambdas al tipo de delegado que se requiera, siempre y cuando la firma concuerde.

Por último, también es importante Comparison<T>, muy usado para ordenar colecciones, representa una comparación entre dos objetos. El delegado tiene dos parámetros del mismo tipo, y regresa un entero. La convención es que si el delegado regresa un número menor a cero quiere decir que el primer parámetro es menor al segundo, si es mayor a cero entonces el primer parámetro es mayor al segundo, y si es cero, entonces son iguales.

List<int> lst = new List<int>(new int[] { 1, 2, 3, 4, 5 });
lst.Sort( (x, y) => x.CompareTo(y));
// lst = 1, 2, 3, 4, 5
lst.Sort( (x, y) => x.CompareTo(y) * -1);
// lst = 5, 4, 3, 2, 1

También podemos usar lambdas, por supuesto, como en el ejemplo anterior.

Entonces ahora sí: explícame qué es un evento

Decíamos que un evento es un mecanismo mediante el cual una clase permite a otros objetos enviar notificaciones (es decir, disparar un evento). La clase que tiene el evento se llama "publicador", y los objetos externos que recibirán la notificación, son los subscriptores.

Las subscripciones se hacen a través de delegados. Un delegado puede ser invocado por objetos externos, pero los eventos no: sólo pueden ser invocados dentro de la clase que los declara.

Un evento se declara utilizando el modificador de acceso, seguido de la palabra reservada "event", más el delegado que define el manejador de eventos (esto es, las funciones callback que serán llamadas cuando se dispare el evento), más el nombre del evento.

modificador event Delegado NombreEvento;

Por ejemplo:

public event EventHandler MyEvent;

Ahora veamos un ejemplo completo.

public delegate int Operación(int a, int b);

class Prueba {
    public event Operación RealizaOperacion;

    public void Invocar()
    {
        if (RealizaOperacion != null)
            RealizaOperacion(5, 10);
    }
}

private int Suma(int a, int b) { return a + b; }

Prueba p = new Prueba();
p.RealizaOperacion += new Operación(Suma);
p.RealizaOperacion += new delegate(int a, int b) { return a - b; }
p.RealizaOperacion += (int a, int b) => a * b;
p.Invocar();

Vemos varias cosas aquí. En primer lugar, declaramos un delegado. Luego, en la clase Prueba, declaramos un evento llamado RealizaOperación. El método Invocar revisa si el evento es nulo, es decir, si nadie se ha subscrito. Si no lo es, entonces invoca al delegado. Esto hará que se dispare el evento y llegue la notificación a todos los subscriptores del mismo.

Posteriormente, instanciamos un objeto prueba, y subscribimos tres delegados al evento: uno creando el delegado explícitamente, otro usando un método anónimo, y otro usando una expresión lambda. El siguiente paso invoca a todos los métodos vía el evento.

¿Y siempre hay que crear un delegado?

Todos los eventos requieren un delegado. Ahora bien, aunque puedes crear los tuyos propios, lo usual, la convención, es que los delegados de los eventos sean métodos que regresen void y tomen dos parámetros: un objeto que representa quien invoca el evento, y un objeto que herede de la clase System.EventArgs, y que represente los parámetros pasados a los delegados con información sobre un evento; si no hay argumentos, puede pasarse el objeto estático EventArgs.Empty.

El delegado más sencillo con lo anterior se llama EventHandler.

class Aviso 
{
    public event EventHandler Avisar;

    public void Invocar() {
        if (Avisar != null)
            Avisar(this, EventArgs.Empty);
    }
}

Aviso a = new Aviso();
a.Avisar = delegate(object sender, EventArgs args) { Console.WriteLine("Un aviso!"); };
a.Invocar(); // imprime "Un aviso!";

Si queremos pasar argumentos personalizados, necesitamos heredar de EventArgs y crear nuestro delegado que siga la convención.

 

class ContadorEventArgs : EventArgs
{
    public int Numero { get; private set; }

    public bool EsPar { get { return Numero % 2 == 0; } }

    public ContadorEventArgs(int num) {
        Numero = num;
    }
}

public delegate void ContadorHandler(object sender, ContadorEventArgs args);

class Contador
{
    public List<int> Numeros { get; private set; }

    public event ContadorHandler ParContado;

    public event ContadorHandler NonContado;

    public Contador() { Numeros = new List<int>(); }

    public void ContarPares()
    {
        foreach (int num in Numeros) {
            if (num % 2 == 0) {
                if (ParContado != null)
                    ParContado(this, new ContadorEventArgs(num));
            }
        }
   }

    public void ContarNones()
    {
        foreach (int num in Numeros) {
            if (num % 2 != 0) {
                if (NonContado != null)
                    NonContado(this, new ContadorEventArgs(num));
            }
        }
    }
}

Contador c = new Contador();
c.Numeros.AddRange(new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 });
c.ParContado = (sender, args) =>
    { Console.WriteLine("Es un par! {0}", args.Numero); };
c.NonContado = (sender, args) =>
    { Console.WriteLine("Oh, {0} es un non.", args.Numero); };
c.ContarPares();
c.ContarNones();

A partir de .NET 2.0, por cierto, existe el delegado EventHandler<T>, donde T es una clase que hereda de EventArgs. Así, ya no tenemos que crear nuestros delegados para los eventos, sino utilizar EventHandler:

class Contador
{
    … etc …

    public event EventHandler<ContadorEventArgts> ParContado;

    … etc …
}

Y a todo esto, ¿cómo me des-subscribo de un evento?

Con el operador -=. Más o menos así:

EventHandler<ContadorEventArgs> handler = delegate(object sender, ContadorEventArgs args) { … };

Contador c = new Contador();
c.ParContado += handler;
…
c.ParContado -= handler;

 

Easy peasy, ¿no?

Ya vimos cómo levantar un evento, pero… ¿hay alguna convención?

De hecho, sí la hay. La convención es que para un evento Evento, exista un método protegido y virtual, llamado OnEvento, que tome un derivado de EventArgs cuando aplique como parámetro.

class Button
{
    public event EventHandler Click;

    protected virtual void OnClick(EventArgs args)
    {
        if (Click != null)
            Click(this, args ?? EventArgs.Empty);
    }
}

Nota que OnClick es protegido y virtual. Esto, porque es preferido en clases derivadas que en lugar de subscribirse al evento de la clase base, sobrescriban el método. Es decir, es preferible hacer esto:

class CircleButton : Button
{
    protected override void OnClick(EventArgs args)
    {
        DrawCircleButton();
        base.OnClick();
    }
}

que esto:

class CircleButton : Button
{
    public CircleButton() : base()
    {
        this.Click += (s, a) => { DrawCircleButton(); };
    }
}

¿Y cómo se guarda internamente estas referencias a los delegados?

Los eventos en realidad guardan en memoria una referencia a cada delegado. Lo que pasa es que eso lo hace internamente el compilador. Si tienes cinco eventos en una clase, por ejemplo, se crearán cinco listas para cada evento, y cada una de estas almacenará delegados. Esto implica que si una clase tiene muchos eventos, su tamaño en memoria irá creciendo proporcionalmente. Si no tenemos cuidado, podemos vernos con una clase sumamente grande.

¿Hay forma de controlar este proceso?

De hecho, sí la hay. El concepto es el de "Event properties" o "eventos propiedades". Básicamente consiste en que tú decides cómo se almacenan los delegados, y puedes almacenarlo en una sola estructura (como, digamos, un Hashtable). De esta forma puedes reducir la huella de memoria de la clase.

Para hacer esto, primero hay que definir una colección de delegados que levanten los eventos, definir una clave para cada evento, definir los eventos propiedades, y usar la colección de delegados para implementar los accesores   de los eventos. Finalmente, se implementa un evento, pero como una propiedad: así como éstas tienen getters y setters, los eventos también pueden tener estructuras similares, llamadas adders y removers.

¿Cómo declaro event properties? ¿Tienes algún ejemplo?

Veamos un ejemplo. Tenemos esta clase.

class Button
{
    public event EventHandler Click;
    public event EventHandler Draw;
    public event EventHandler KeyPress;
}

Y queremos cambiarla para que use eventos propiedades. El primer paso es elegir una colección de delegados. Vamos a utilizar la clase EventHandlerList, del espacio de nombres System.ComponentModel. Esta clase está pensada para ser utilizada justamente en este escenario.

protected EventHandlerList _eventDelegates;

Button() {
    _eventDelegates = new EventHandlerList();
}

Luego, tenemos que elegir una llave para cada evento. Esta llave puede ser cualquier valor: un número, una cadena de texto. O puede ser un vil object vacío. Dado que es una llave por evento, bien podemos hacerlos estáticos y readonly.

private static readonly object _clickEventKey;
private static readonly object _drawEventKey;
private static readonly object _keyPressEventKey;

static Button() {
    _clickEventKey = new object();
    _drawEventKey = new object();
    _keyPressEventKey = new object();
}

Perfecto. Siguiente paso, añadimos el evento. Cambiamos los eventos que teníamos. Esto es nuevo, así que pon atención:

public event EventHandler Click
{
    add { _eventDelegates.AddHandler(_clickEventKey, value); }
    remove { _eventDelegates.RemoveHandler(_clickEventKey, value); }
}

¡Sasquatch! Al evento le hemos añadido un par de adders y removers, que es el símil de los getters y setters de las propiedades. Por ello se llaman eventos propiedades. Las palabras add y remove son contextuales. El add se invoca cuando se subscribe un evento, el remove cuando se elimina, por supuesto. El value, como en las propiedades, es contextual, y tiene el valor del manejador de evento (y por tanto es de tipo EventHandler en el ejemplo anterior).

Lo que hacemos en este caso es invocar a AddHandler y RemoveHandler del EventHandlerList que creamos hace rato, respectivamente. Ambos métodos se encargan de revisar referencias válidas y que existan llaves y así, hacen todo por ti. Si utilizaras alguna otra colección, como un Hashtable, tendrías que hacerlo por tu cuenta. De ahí que sea mejor usar esta colección. Por cierto, ambos métodos reciben como segundo parámetro un objeto de tipo Delegate. Esta clase es la base de todos los delegados, así que no importa qué delegado tenga tu evento como referencia. Es decir, no tienes que usar EventHandler, puede ser cualquier delegado.

Esto fue para el evento Click. Algo similar debe hacerse para los otros dos eventos. Y luego viene el último paso: el invocar el evento. Quedamos que por convención usamos un método protegido, virtual, y cuyo nombre se forma prefijando un On al nombre del evento. Este método lo que debe hacer es , por supuesto, obtener una referencia al delegado correcto del evento, a partir de la llave del mismo (en este caso, a partir de _clickEventKey). Y después simplemente se invoca como se invocaría cualquier delegado. Sólo recuerda hacer la conversión de Delegate al tipo de tu delegado (en este caso, todos son de tipo EventHandler).

protected virtual void OnClick(EventArgs args)
{
    EventHandler handler = _eventDelegates[_clickEventKey] as EventHandler;
    if (handler != null)
        handler(this, args ?? EventArgs.Empty);
}

¿Cómo veis? Aquí está la clase final.

class Button : IDisposable
{
    protected EventHandlerList _eventDelegates;
    private static readonly object _clickEventKey;
    private static readonly object _drawEventKey;
    private static readonly object _keyPressEventKey;

    Button() {
        _eventDelegates = new EventHandlerList();
    }

    static Button() {
        _clickEventKey = new object();
        _drawEventKey = new object();
        _keyPressEventKey = new object();
    }

    public event EventHandler Click
    {
        add { _eventDelegates.AddHandler(_clickEventKey, value); }
        remove { _eventDelegates.RemoveHandler(_clickEventKey, value); }
    }

    public event EventHandler Draw
    {
        add { _eventDelegates.AddHandler(_drawEventKey, value); }
        remove { _eventDelegates.RemoveHandler(_drawEventKey, value); }
    }

    public event EventHandler KeyPressed
    {
        add { _eventDelegates.AddHandler(_keyPressedEventKey, value); }
        remove { _eventDelegates.RemoveHandler(_keyPressedEventKey, value); }
    }

    protected virtual void OnClick(EventArgs args)
    {
        var handler = _eventDelegates[_clickEventKey] as EventHandler;
        if (handler != null)
            handler(this, args ?? EventArgs.Empty);
    }

    protected virtual void OnDraw(EventArgs args)
    {
        var handler = _eventDelegates[_drawEventKey] as EventHandler;
        if (handler != null)
            handler(this, args ?? EventArgs.Empty);
    }

    protected virtual void OnKeyPressed(EventArgs args)
    {
        var handler = _eventDelegates[_keyPressedEventKey] as EventHandler;
        if (handler != null)
            handler(this, args ?? EventArgs.Empty);
    }

    public void Dispose() {
        _eventDelegates.Dispose();
    }
}

¿Por qué añadiste ese Dispose al final?

Ahm… porque EventHandlerList imlementa IDisposable. Es una parte esencial. Recuerden que cuando subscribimos un evento, éstos obtienen una referencia a la clase donde se subscriben (o mejor dicho, a sus métodos). Al final hay que liberar esas referencias, porque si no la clase no liberará recursos (por referencias circulares). Entonces hay que mandar llamar al Dispose de _eventDelegates. Y para ello, ponemos un Dispose propio e implementamos IDisposable. Digo, ya entrados en gastos…

¿Algo más?

Pues no. Ya conoces lo básico de manejar eventos en .NET, conoces los delegados y callbacks y cómo emplearlos. A partir de .NET 3.0, con la llegada de Windows Presentation Foundation, Silverlight y ahora .NET para Metro (.NET for Windows Store Apps), han surgido otra modalidad de eventos, llamados eventos ruteados o RoutedEvents. Pero eso es harina de otro costal.

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

C# 101: control de un programa


Este tema, la verdad, es que sobra un poco para todos aquellos que ya conocen C#. Sin embargo, viene dentro de los temas a tratar para la certificación 70-483, así que -nevertheless- lo incluiré, pero a guisa de cuestionario, para que sea más concreto.

 

¿Qué es una expresión?

Una expresión, usualmente ubicada entre dos paréntesis, consiste en la ejecución secuencial de operaciones (las cuales pueden incluir llamadas a métodos de funciones) y operadores que pueden ser evaluadas a un solo valor, objeto, método. Las expresiones pueden tener valores literales, invocación de métodos, o elementos de nombre simple (como variables).

Ejemplo de expresiones:

 

x == 5

10 < x && x <= Convert.ToInt32("256")

(x + Int32.Parse("42")) / 7

 

¿Cómo se evalúa una expresión?

Primero se determina el tipo de dato de la expresión. Si x, y son enteros, entonces x + y será un entero. Si las variables en la expresión son diferentes, se mandan llamar los operadores para los objetos, y si no existen, se realiza una conversión implícita. Si no existe conversión implícita, se lanza un error e compilación.

Si la expresión no incluye operadores, se evalúan los valores de retorno del método invocado. Por ejemplo, new object() evalúa a una nueva referencia.

¿Puede una expresión que involucra variables de un tipo determinado, al evaluarse, generar otro tipo de dato?

Cuando dicha evaluación se realiza mediante métodos, es posible, ya que el resultado depende del tipo de retorno del mismo. Cuando se aplica sobre tipos de datos que tienen operadores sobrecargados, es posible, si éstos así lo determinan.

Cuando se aplica sobre tipos de datos primarios (i.e. estructuras de System), no. Sin embargo, si una expresión da como resultado un valor mayor al que puede almacenar, se lanza un OverflowException. Por ejemplo:

 

int i = int.MaxValue;
int v = i + 1; // OverflowException

 

¿Cómo puede evitarse un OverflowException?

 

Una alternativa es utilizar la palabra reservada unchecked.

int i = int.MaxValue;
int v = 0;

unchecked {
    v = i + 1;
} 

La palabra unchecked hace que si hay un overflow, en lugar de lanzar una excepción, se trunca el valor de la variable. Para hacer lo contrario: forzar un overflow, se utiliza la palabra reservada "checked".

 

¿Qué es una estructura de decisión?

 

Las estructuras de decisión son construcciones de un lenguaje de programación, en este caso C#, que permiten determinar si un bloque de código se ejecuta o no.

Las estructuras de decisión evalúan una o más expresiones. Éstas, en el caso de C#, deben de evaluarse a un valor booleano: verdadero o falso. Si la expresión se evalúa a verdadero, entonces se ejecuta el bloque de código. Algunas estructuras permiten que se ejecute un bloque alterno cuando se evalúa a falso.

 

¿Qué estructuras de decisión existen?

 

Existen tres tipos de estructuras de decisión: las estructuras de implicación, las estructuras de selección y un operador.

Las primeras ejecutan bloques de código de acuerdo a la evaluación de una expresión en particular. Las segundas, deciden qué bloque, de un conjunto amplio, deberán ejecutarse, dependiendo de la coincidencia entre un valor dado y una lista de posibles valores. Finalmente, existe en C# un operador ternario que permite seleccionar un valor dada la evaluación de una expresión booleana.

 

¿Cuáles son las sentencias de implicación?

 

La más común y más utilizada es la sentencia implicativa: si x entonces y.

if (expresión) {
    ...bloque…
}

 

Si expresión es verdadera, se ejecuta el bloque de código. Si el bloque es de una sola sentencia, pueden omitirse las llaves. Si la expresión es falsa, el programa se salta todo el bloque enterito y continúa la ejecución.

Una variante es la sentencia implicativa doble: si x entonces y, y si no, z.

if (expresión) {
    ...bloque 1…
} else {
    ...bloque 2…
}

Si expresión es verdadera, se ejecuta el bloque de código 1. Si no, en automático se ejecuta el bloque 2.

 

Otra variante es la implicación múltiple: si x1 entonces y1, o si no, si x2 entonces y2, o si no, …, o si no, z.

if (expresión 1) {
    ...bloque 1…
} else if (expresión 2) {
    ...bloque 2…
} else if (expresión 3) {
    ...bloque 3…
} else  {
    ...bloque 4…
}

Como puedes ver, podemos combinar la evaluación de diversas expresiones. Si expresión 1 no se cumple, se evalúa expresión 2. Si ésta tampoco se cumple, pasamos a expresión 3. Y así sucesivamente. La última sentencia, el bloque else, es opcional, y si se incorpora entonces se ejecuta el bloque cuya expresión o  haya sido  aprobada por los bloques.

Ejemplo:

Random rnd = new Random();
int n = rnd.Next();

if (n % 2 == 0)
    Console.WriteLine("{0} es un número par", n);
else if (n % 3 == 0)
    Console.WriteLine("{0} es un múltiplo de 3", n);
else if (n % 5 == 0)
    Console.WriteLine("{0} es un múltiplo de 5", n);
else 
    Console.WriteLine("{0} es un número impar", n);

 

 

¿Cuáles son las sentencias de selección?

 

La estructura de decisión de tipo sentencia de selección es la que permite evaluar un valor y luego compararlo con una lista de valores posibles. Esta sentencia se llama switch, y tiene la siguiente estructura:

switch (expresión)
{
    case valor 1:
        ...bloque 1…
        break;

    case valor 2:
        ...bloque 2…
        break;

    …

    case valor n:
        ...bloque n…
        break;

    default:
        ...bloque predefinido…
        break;
}

En esta estructura se evalúa expresión y se compara contra los valores puestos en cada una de las sentencias case. Se compara case a case, y cuando coincide, se ejecuta el bloque de código que existe entre dicho case y la sentencia break. Los break son obligatorios siempre, si no hay error de compilación (nota: esto es diferente a C++, donde si no hay un break se ejecuta código en cascada). Si ningún case concuerda, entonces se ejecuta el bloque de código de la sentencia default.

opcional, pero siempre debe haber por lo menos una sentencia case o una default para que compile.

Por cierto, que los valores a comparar en las sentencias case, DEBEN SER CONSTANTES. Muy importante, no podemos

El bloque default es colocar expresiones ahí, sino solo valores constantes: números, caracteres, o texto, pero no llamadas a funciones ni operadores. Hacerlo generará un error de compilación.

enum Command {
    New, Open, Save, Exit
}

Command cmd = GetCommand();
switch (cmd)
{
    case Command.New:
        SaveCurrentDocument();
        CloseCurrentDocument();
        OpenNewDocument();
        break;

    case Command.Open:
        SaveCurrentDocument();
        CloseCurrentDocument();
        string file = ShowOpenDialog();
        OpenDocument(file);
        break;

    case Command.Save:
        SaveCurrentDocument();
        break;

    case Command.Exit:
        SaveCurrentDocument();
        CloseCurrentDocument();
        Exit();
        break;

    default:
        MessageBox.Show("Comando no reconocido.");
        break;
}

¿Y el operador ternario?

 

El operador ternario ? : permite seleccionar un valor de dos opciones, dependiendo de si la expresión se evalúa a verdadero o falso.

var variable = expresión ? valor 1 : valor 2;

En este caso, expresión debe evaluarse a algún valor booleano. De ser verdadero, se evalúa la expresión de valor 1, si no, se hace lo propio con valor 2.

Algo a notar es que valor 1 y valor 2 deben ser del mismo tipo de dato, y este tipo es el resultado de toda la expresión, y por tanto la variable que recibe debe coincidir con el tipo de dato regresado.

string str = DateTime.Now.Month == 12 ? "Feliz Navidad" : "Es un mes cualquiera.";

try 
{
    …
} 
catch (Exception ex) 
{
    throw new Exception(
        ex is ArgumentException ? "Bug encontrado." : ex.Message
    );
}

El primer ejemplo muestra cómo usar el operador ternario de forma tradicional. En el segundo, el operador se pasa como una expresión que evalúa hacia una cadena de texto, la cual es pasada como parámetro al constructor de Exception.

 

¿Qué es una estructura de iteración?

 

Es un conjunto de sentencias que permiten realizar bucles hasta que cierta condición se cumpla. Estas sentencias pueden tener los siguientes componentes:

1.- Inicialización de variables. Las variables que se utilizan para controlar el bucle se inician a algún valor en particular.

2.- Actualización de valores. En cada iteración, es posible que se actualice el valor de las variables usadas para controlar el bucle.

3.- Evaluación de expresión. En cada bucle, se evalúa una expresión, la cual determina si el bucle debe continuar o no.

4.- Salida forzada. Un bucle puede terminar de forma explícita, mediante la palabra reservada "break".

5.- Continuación forzada. Un bucle puede saltarse a la siguiente iteración, abandonando la actual, mediante la palabra reservada "continue".

Existen cuatro estructuras de iteración: while, do, for y foreach.

 

¿Cuál es la estructura while?

 

Es aquella estructura de iteración que evalúa una expresión, y si ésta es verdadera, ejecuta un bloque de código; cuando éste termina, vuelve a evaluar la expresión, y así sucesivamente. No tiene inicialización de variables ni actualización de valores integrado, así que eso corre por cuenta del programador.

while (expresión)
{
    bloque de código;
}

Si el bloque de código es de una sola línea, pueden omitirse las llaves.

Hay que tener cuidado con la salida del bucle while. Los valores de expresión nunca son actualizados, al menos el bucle no obliga a hacerlo, por lo que si no tenemos cuidado podemos provocar que la estructura quede iterando de forma infinita, trabando nuestro programa o acabándose la memoria. Lo normal es que se inicialice una variable al inicio, que se usa en expresión, y en el bloque de código, ésta se actualiza.

int n = 0;
while (n < 42)
{
    Console.WriteLine("Número {0}", n);
    n++;
}

Este sencillo ejemplo muestra cómo iterar sobre una variable: la iniciamos a cero, y en cada iteración la actualizamos en uno. Aquí la expresión evalúa verdadero mientras el número sea inferior a 42.

string cmd = "M";
while (cmd != "Q")
{
    if (cmd == "M")
        ImprimirMenu();
    else if (cmd == "C")
        CrearReporte();
    else if (cmd == "H")
        ImprimirAyuda();
    else if (cmd == "E")
        EliminarReportes();
    else
        Console.WriteLine("Comando inválido. ");

    cmd = Console.ReadLine();
}

Este ejemplo está un poco más complejo. Se inicializa un comando, y dependiendo de la entrada del usuario, el bucle repite el mismo comportamiento hasta que el usuario da el comando de "Q" para Quitar.

 

¿Cuál es la estructura do?

 

Es una estructura muy similar a la while. De hecho también se le conoce como do-while. Mientras que el while evalúa la expresión y luego ejecuta el bloque, el do primero ejecuta el bloque y luego evalúa la expresión. Tampoco tiene inicialización ni actualización de variables integrado.

do
{
    bloque de código;
} 
while (expresión);

Vamos a modificar el ejemplo anterior para usar un do-while.

string cmd = string.Empty;
do
{
    if (cmd == "M")
        ImprimirMenu();
    else if (cmd == "C")
        CrearReporte();
    else if (cmd == "H")
        ImprimirAyuda();
    else if (cmd == "E")
        EliminarReportes();
    else
        Console.WriteLine("Ingrese un comando válido. ");

    cmd = Console.ReadLine();
} 
while (cmd != "Q");

Con esta estructura retrasamos la actividad de actualizar la variable. Esto puede ser bueno, particularmente cuando dicha actualización consume muchos recursos o es intensiva.

 

¿Cómo elijo entre usar un while o un do?

 

A veces es cuestión de gusto. Pero en general, una buen regla a considerar es la siguiente: si tu bucle puede que nunca ejecute una iteración (es decir, la iteración puede realizarse 0 o más veces), entonces usa un while. Si tu bucle siempre va a ejecutarse una sola vez, entonces una un do.

Esto, porque la evaluación de la expresión en un do se hace al final, forzando ya por lo menos una iterción. En el caso del while se hace al inicio, por lo que si la primera evaluación de la expresión es false, el bucle nunca se ejecutará.

 

¿Cuál es la estructura de iteración for?

 

Es una estructura que tiene tres secciones en su declaración: la inicialización de variables (usualmente utilizada para controlar el bucle), la evaluación de continuidad (i.e. si se evalúa a verdadero, el bucle continúa ejecutándose), y la expresión de actualización de variables (i.e. actualizar las variables de control).

for (inicialización; continuación; actualización)
{
}

Por ejemplo, queremos ejecutar 10 veces un bucle e imprimir un mensaje en pantalla. Inicializamos una variable a 0, la continuación sería que dicha variable sea menor a 10, y la actualización: aumentamos nuestra variable en 1. 

for (int i = 0; i < 10; i++)
{
    Console.WriteLine("Ejecutando {0}… ", i+1);
}

Si no queremos inicializar o actualizar, dejamos vacíos los espacios, pero con el punto y coma:

for (; i < 10;) {
    …
}

Así, podemos recorrer un array muy fácilmente:

string[] strs = GetStringArray();
for (int i = 0; i < strs.Length; i++)
{
    Console.WriteLine(strs[i]);
}

Hay muchas otras formas de utilizar un bucle for, no necesariamente tiene que ser con contadores. Por ejemplo, podemos cambiar el ejemplo anterior por esto:

string[] strs = GetStringArray();
IEnumerable enumerable = strs;
IEnumerator e = enumberable.GetEnumerator();

for (e.Reset(); e.MoveNext(); ) 
{
    string str = e.Current as string;
    Console.WriteLine(str);
}

En este caso, MoveNext regresa false si ya no hay más elementos a evaluar. Por lo que no necesitamos una sentencia de actualzación y la dejamos vacía.

 

¿Cuál es la estructura foreach?

 

La estructura foreach es en realidad azúcar sintáctica para trabajar con enumeraciones. Aunque no es objetivo de esta entrada hablar sobre las enumeraciones, recordaremos que una interfaz IEnumerable (que implementa cualquier colección, array, etc.) tiene un método: GetEnumerator, que nos regresa una interfaz IEnumerator.

Esta última interfaz tiene  dos métodos y una propiedad: Current, que representa el objeto al que apunta el iterador, Reset, que mueve el apuntador del iterador al inicio de la colección, y MoveNext, que avanza en uno el apuntador del iterador. Así, para recorrer una colección, podríamos hacerlo con un bucle for, como vimos en la pregunta pasada:

string[] strs = GetStringArray();
IEnumerable enumerable = strs;
IEnumerator e = enumberable.GetEnumerator();

for (e.Reset(); e.MoveNext(); ) 
{
    string str = e.Current as string;
    Console.WriteLine(str);
}

Sin embargo, esto  es un poco complejo, y C# pone a nuestra disposición los bucles foreach. Estos bucles se convierten en bucles for como el anterior, pero ocultan la complejidad. Por tanto, el bucle foreach sólo funcionará con colecciones que implementen IEnumerable. Básicamente declaramos una variable (equivalente al Current) y declaramos sobre qué colección se iterará. Internamente, el bucle foreach se hará cargo de llamar a Reset y MoveNext para saber dónde acaba la colección. 

foreach (Tipo variable in colección)
{
}

El ejemplo anterior podríamos dejarlo como:

string[] strs = GetStringArray();

foreach (string str in strs)
{
    Console.WriteLine(str);
}

Mucho más fácil, ¿no?

Por supuesto, el foreach se adhiere a las reglas de IEnumerator. En particular, mientras se está iterando de esta forma, la colección no puede ser modificada. Si se modifica, tendremos una bonita excepción InvalidOperationException en nuestras manos.

List<string> strs = GetStringList();

foreach (string str in strs)
{
    strs.Remove(str); // InvalidOperationException
}

 

Palabras finales

 

Bueno, hemos visto las estructuras de decisión e iteración. Fáciles de utilizar, se emplean en todo programa por todos lados. Esperemos que esto ayude en los esfuerzos para certificarnos en 70-483.

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