Archivo

Posts Tagged ‘.NET Base’

C# 101: cadenas de texto


Las cadenas de texto representan al tipo de dato más importante en un lenguaje, tras el tipo de dato número entero. En efecto, toda la información (al final el propósito de un programa informático es manipular información) consiste principalmente en cadenas de texto. Una gran parte al menos. Así, manipular las cadenas de texto se vuelve esencial en cualquier lenguaje.

Si uno compara plataformas antiguas con plataformas modernas como Java y .NET, podemos encontrar diferencias significativas. Probablemente la mayor de éstas sea que hoy en día las cadenas de texto son inmutables. Esto quiere decir que una cadena de texto que se crea ya no puede cambiarse. Puede, en cambio, crear nuevas cadenas a partir de una. En fin, poco a poco.

¿Qué es una cadena de texto?

Una cadena de texto es una colección secuencial de caracteres, los cuales forman palabras y demás tipo de información. Cada carácter es en realidad un carácter en formato Unicode.

La clase que representa a una cadena de texto es System.String, y la que representa un carácter, es System.Char. El operador " " representa una cadena de texto constante, y por tanto puede asignarse directamente a una cadena de texto. El operador ‘ ‘ representa un carácter, y por tanto puede asignarse a una variable apropiada.

string str = "Hola mundo";
char ch = 'H';
char[] chs = new char[] { ch, 'o', 'l', 'a', ' ', 'm', 'u', 'n', 'd', 'o' };

La clase string tiene un método, llamado Empty, que representa una cadena de texto vacía. Equivale a colocar la constante "".

string str = string.Empty;
str = "";

La clase string puede inicializarse de varias formas:

// operador " "
string s1 = "Hola mundo";

// mediante el constructor
string s2 = new strnig("Hola mundo");

// a partir de un array de caracteres
string s3 = new string(
new char[] { 'H', 'o', 'l', 'a', ' ', 'm', 'u', 'n', 'd', 'o' });

// a partir de un carácter que se repite n veces
string s4 = new string('*', 10); // **********

// a partir de un grupo de bytes con signo
sbyte[] bytes = { 0x41, 0x42, 0x43, 0x44, 0x45, 0x00 };

unsafe {
    sbyte* p = bytes;
    string s5 = new string(p);
}

Los caracteres son una estructura, por tanto son tipos valor (ValueType). Las cadenas de texto son clases, y por tanto son tipos referencia.

¿Qué es eso de Unicode?

Unicode es el nombre del formato en el que se presenta una cadena de texto en .NET. Un byte con signo (8 bits) puede almacenar números del -127 al 128. Los 128 números son suficientes para representar toda la tabla de caracteres ASCII. Así, por ejemplo, el número 64 equivale a una A, el 65 a una B, etc. Así nació la primera tabla de caracteres.

Después a alguien se le ocurrió que podía utilizarse un byte sin signo (8 bits), el cual puede almacenar números del 0 al 255. Con esto podemos almacenar más caracteres, y se creó la tabla ASCII extendida. 255 caracteres son muchos. Las letras acentuadas, por ejemplo, forman de ASCII extendido. Prácticamente se incluyeron todas las letras, caracteres y símbolos del alfabeto latino.

Sin embargo, a pesar de ello, ASCII+ es insuficiente todavía para guardar todos los caracteres de otros lenguajes, como el chino mandarín, la escritura hebrea, griega, cirílica, etc. Así, en los 90s se propuso un formato, llamado Unicode, el cual considera caracteres de dos bytes sin signo (16 bits), los cuales pueden almacenar números del 0 al 65536. Ahora, con 65536 caracteres sí que caben los chinos, los sánscritos y hasta el élfico antiguo de Lord of the Rings. Hay otro formato, Unicode 32 (de 4 bytes sin signo), pero la verdad es que no se utiliza mucho. El bueno, estándar, es el Unicode, al menos para .NET.

¿Eso quiere decir que cada carácter me cuesta 2 bytes de memoria?

Así es. La cadena de texto "Hola mundo" te cuesta 20 bytes. Esto implica, por supuesto, que mucho texto puede salirte caro en términos de memoria. El tamaño máximo de una cadena de texto en .NET es de 2 GB, o 1,073,741,824 caracteres.

Hay que tener cuidado, al final las cadenas de texto pueden ser potencialmente grandes, así que ten cuidado nada más.

¿Y qué es eso de que son inmutables?

Ah, eso quiere decir que una cadena de texto no cambia, una vez que le has asignado un valor. Es como si el valor en memoria fuera de solo lectura. Por supuesto que internamente no es así, pero la clase string no expone ni un solo método o propiedad que modifique el contenido. Todos los métodos operan sobre el valor del objeto actual, pero construyen uno nuevo, que es lo que devuelven.

La decisión de hacerlo de esta forma viene por razones históricas. Si uno programa para Win32 con C o C++, recordará que en estas plataformas las cadenas de texto pueden o no ser mutables. Por un lado, tenemos las cadenas estáticas o array de caracteres que no cambian su tamaño. Pueden ser declaradas en la pila de memoria o en el montículo de memoria. Si se declara en el montículo, entonces la cadena puede cambiar de tamaño. Desde el punto de vista de una función, el parámetro que recibe no sabe si es una cadena fija o dinámica.

void SetWindowText(HWND hWnd, LPCTSTR lpszText) // LPCTSTR == const TCHAR* 
{ 
… 
} 

TCHAR sz[20] = L"Hola mundo!"; // cadena fija 
TCHAR* psz = new TCHAR[20]; 
wcscpy(psz, L"Hola mundo!"); 

// indistinguible para SetWindowText 
SetWindowText(sz); 
SetWindowText(psz); 

delete [] psz; 

Y peor aún es cuando una cadena de texto tiene que ser creada y devuelta por una función. En ese caso pides la cadena en referencia más el tamaño de la misma. O bien, regresas una cadena dinámicamente creada y esperas que el programador no se olvide de liberar la memoria antes de salir. Ah, y si tu cadena permite tener nulos intermedios, entonces tienes que usar otros métodos.

Por esta misma complicación, salieron muchas muchas muchas clases y opciones para paliar el dolor. De entrada, Win32 define CHAR, WCHAR y TCHAR, con sus equivalentes punteros: LPSTR, LPWSTR y LPTSTR, más sus constantes LPCSTR, LPCWSTR, LPCTSTR. El Microsoft Foundation Classes, MFC, tiene la clase CString, y el Active Template Library, ATL, (junto con Windows Template Library, WTL) define su propia clase CStringT. Ah, y el estándar de C++ define std::string y std::wstring, que heredan de std::basic_string<T>. Por ahí el Component Object Model define BSTR, con varios métodos como SysAllocString y SysFreeString, más la clase _bstr_t y CComStr.

En fin, con tanto relajo, la gente que ideó C# y .NET debía tomar una decisión sobre cómo manejar cadenas en .NET, para evitar caer en el relajo que se tenía. Tanto cadenas estáticas como dinámicas presentan ventajas y desventajas. Las estáticas provocan algoritmos complejos para crear nuevas cadenas de texto, pero son seguras. Las dinámicas son inherentemente inseguras pero pueden crecer a conveniencia. Al final, la decisión fue que las cadenas iban a ser dinámicas (i.e. referencias) PERO sólo internamente: la clase que las representa no expondría ningún método para que un programador pudiera cambiar su valor interno. De esta forma se aseguraban lo mejor de los dos mundos: dinamismo pero a su vez, inmutabilidad. Incluso gracias a eso pudieron diseñar algunos algoritmos para optimizar su manejo.

Ok, entiendo el dilema, pero si son inmutables, ¿cómo podemos realizar operaciones para crear nuevas cadenas?

Las operaciones que pueden realizarse sobre cadenas de texto son variadas: concatenación, substitución de bloques de texto, búsquedas, indización, reemplazo de caracteres, formateo, conversiones, etc. Todas estas operaciones en realidad devuelven una nueva cadena de texto, basada en la cadena actual más la operación en cuestión. Pero la cadena actual nunca se ve afectada.

Pongamos como ejemplo el reemplazo de un bloque de texto. El método String.Replace toma dos parámetros: una cadena que representa el valor que queremos buscar, y otra cadena que representa el valor con el que reemplazaremos a la primera. El método opera sobre una cadena determinada, y regresa otra cadena con el valor modificado.

string strorg = "Todos los perros van al cielo"; 
string strmod = strorg.Replace("perros", "pollos"); 

Console.WriteLine("Cadena original: {0}", strorg); 
Console.WriteLine("Cadena nueva: {0}", strmod); 

/* imprime: 
Todos los perros van al cielo 
Todos los pollos van al cielo 
*/ 

Vemos que aunque aplicamos el método Replace sobre strorg, éste no modifico su valor, sino que creó una nueva cadena con la operación aplicada, la cual regresó.

Momento: ¿no significa eso que entonces que estamos creando muchas instancias de cadenas?

En efecto. Hay que tener mucho cuidado, es muy importante estar conscientes de esto. En particular, la concatenación de cadenas de texto es particularmente cara en cuanto a recursos.

int i = 42; 
int n = 9; 
int m = 6; 

string s = "Según la guía interestelar, " + i + " es la respuesta última a la " + 
"vida, el universo y todo lo demás, siendo la pregunta: ¿cuánto es " + 
n + " por " + m + "? "; 

El operador + está sobrecargado para precisamente concatenar dos valores. Si uno de los valores no es una cadena de texto, se le convierte en cadena en automático, bien mediante una conversión directa, bien mediante el método ToString. Así las cosas, veamos las cadenas que se han creado.

 

Expresión

Cadena

Estática

Según la guía interestelar,

i.ToString()

42

Concatenación

Según la guía interestelar, 42

Estática

es la respuesta última a la

Concatenación

Según la guía interestelar, 42 es la respuesta última a la

Estática

vida, el universo yo todo lo demás, siendo la pregunta: ¿cuánto es

Concatenación

Según la guía interestelar, 42 es la respuesta última a la vida, el universo y todo lo demás, siendo la pregunta: ¿cuánto es

n.ToString()

9

Concatenación

Según la guía interestelar, 42 es la respuesta última a la vida, el universo y todo lo demás, siendo la pregunta: ¿cuánto es 9

Estática

 por

Concatenación

Según la guía interestelar, 42 es la respuesta última a la vida, el universo y todo lo demás, siendo la pregunta: ¿cuánto es 9 por

m.ToString()

6

Concatenación

Según la guía interestelar, 42 es la respuesta última a la vida, el universo y todo lo demás, siendo la pregunta: ¿cuánto es 9 por 6

Estática

?

Concatenación

Según la guía interestelar, 42 es la respuesta última a la vida, el universo y todo lo demás, siendo la pregunta: ¿cuánto es 9 por 6?

 

Perdonen la expresión, pero ¡en la madre! Quince cadenas se han creado, y se duplican muchísimos valores. ¡De entrada todas las cadenas "Concatenación" deberían ahorrarse, no hacen sentido! Y ahora se me caen los calzones: 1738 bytes por 869 caracteres, es decir 1.7 KB. La cadena original que queríamos formar, al final, fue de 266 bytes por 133 caracteres. Es decir que para formar una cadena de 266 bytes tuvimos que gastarnos 1472 bytes: 553% más de consumo de memoria.

Desafortunadamente, es la desventaja (o "trade-off") que tenemos que dar por poder manejar este modelo de cadenas de texto inmutables. La lógica detrás de esto es que es mejor tener un sistema más ineficiente a propiciar excepciones y errores de programación en las aplicaciones. Pero eso no nos exime de problemas ulteriores. He visto sistemas (sobre todo aplicaciones web hechas con ASP.NET) donde la aplicación está muy lenta, consume muchos recursos o de plano muere por falta de memoria, precisamente por tener un muy mal manejo de cadenas de texto.

¿Entonces no hay esperanza? ¿No podremos escribir aplicaciones intensivas en texto?

Depende mucho del programador. Una concatenación como la anterior causará muchos problemas, sobre todo cuando ésta se repite mucho. Y eso depende del programador, depende de ti.

Sin embargo, vale la pena aclarar, por si hubo alguna confusión: las cadenas son inmutables para los clientes de la clase String, es decir, para nosotros simples programadores mortales. Otras clases amigas (internas en C#) sí que pueden tener acceso a los miembros que permitan modificar la cadena de texto.

¿Eso quiere decir que hay clases especiales para ayudarnos?

Precisamente. .NET pone a nuestra disposición varias clases que nos ayudan muchísimo con la gestión de recursos de texto. Estas clases, en particular, hacen uso de estos principios:

1.- uso dinámico de la cadena de texto, ampliando y reduciendo su tamaño, pero protegido.

2.- creación de búferes lo suficientemente grandes para no estar redimensionando la cadena en cada momento, pero no tanto que consuma demasiada memoria.

3.- preferir el movimiento de los bytes de una cadena en lugar de copiarlos.

Esto disminuye significativamente la creación de las cadenas y por tanto el consumo de memoria. Un método de la clase String: String.Format, nos ayuda también con esto. Veamos:

int i = 42; 
int n = 9; 
int m = 6; 

string s = string.Format("Según la guía interestelar, {0} es la respuesta última 
a la vida, el universo y todo lo demás, siendo la 
pregunta: ¿cuánto es {1} por {2}?", i, n, m); 

Este código genera las siguientes cadenas:

Expresión

Cadena

Estática

Según la guía interestelar, {0} es la respuesta última a la vida, el universo y todo lo demás, siendo la pregunta: ¿cuánto es {1} por {2}?

i.ToString()

42

n.ToString()

9

m.ToString()

6

Formateo

Según la guía interestelar, 42 es la respuesta última a la vida, el universo y todo lo demás, siendo la pregunta: ¿cuánto es 9 por 6?

String.Format hace uso internamente de otra clase, que veremos más adelante, y que se adhiere a los tres principios expuestos anteriormente. En este caso, creamos 275 caracteres, lo cual representa 550 bytes. Esto representa un 37% de los bytes empleados al usar la concatenación del ejemplo anterior. Es decir, nos ahorramos un 63% de memoria. Una diferencia significativa.

String.Format parece adecuado para cadenas pequeñas, pero ¿qué pasa con las grandes cadenas?

En estos casos tienes unas cuantas opciones. La primera, más fácil y más obvia, es utilizar la clase StringBuilder. Esta clase construye cadenas de forma eficiente, y de hecho el método String.Format utiliza internamente un StringBuilder.

Básicamente, StringBuilder mantiene su propio búfer, y cuando se hace alguna operación sobre el texto que cambie la longitud de la cadena, la clase revisa si tiene suficiente tamaño en su búfer, y si no, el tamaño del búfer es incrementado en un tamaño determinado. Durante todo este tiempo, ¡StringBuilder NO utiliza ningún string! Sino que guarda todo a nivel de bytes. Sólo hasta que se invoca al método StringBuilder.ToString es que la clase construye el string.

Nuestro ejemplo anterior se vería así, utilizando StringBuilder.

StringBuilder text = new StringBuilder() 
.Append("Según la guía interestelar, ") 
.Append(i) 
.Append(" es la respuesta última a la vida, el universo y todo lo demás, 
siendo la pregunta: ¿cuánto es ") 
.AppendFormat("{0} por {1}?", n, m); 

string s = text.ToString(); 

Este código puede resultar extraño, así que expliquémoslo punto por punto. Primero, vemos que existe un método Append, el cuál añade al búfer una cadena de texto. De hecho, el método se encuentra sobrecargado para incorporar como parámetros todos los tipos base de .NET: bytes, ints, floats, decimales, DateTimes, etc.

Otro método es AppendFormat. Éste funciona igual que Append, con la diferencia que reemplaza en el texto las llaves {0}, {1}, {2}, etc., por el parámetro correspondiente en la posición 0, 1, 2, etc. De hecho, si pudieras ver el código de String.Format, verías algo así:

public static string Format(string text, params object[] objs) 
{ 
    StringBuilder b = new StringBuilder(); 
    b.AppendFormat(text, objs); 
    
    return b.ToString(); 
} 

Ahora bien, tanto Append como AppendFormat regresan una instancia a StringBuilder. De hecho, la instancia que regresan es la propia instancia. Es decir, hacen un return this. Si pudieras ver el código de Append, verías algo así:

public StringBuilder Append(string text) 
{ 
    // hacer algo con text 

    return this; 
} 

¿Por qué hacer esto? Ah, pues para que precisamente puedes encadenar las llamadas a Append y AppendFormat de la forma en que hicimos en el ejemplo. Este es un patrón de diseño que comúnmente se aplica en las clases que implementan el patrón Builder.

¿Se puede escoger las opciones de formato de un texto?

¡En efecto! Una de las sobrecargas de AppendFormat incluye un IFormatProvider como parámetro, así que para formatear el texto con una cultura especial puedes usar dicha sobrecarga.

float f = 1983.42; 
CultureInfo esmx = new CultureInfo("es-MX"); 
CultureInfo eses = new CultureInfo("es-ES"); 
StringBuilder b = new StringBuilder() 
    .AppendFormat(eses, "Número español: {0}\n", f) 
    .AppendFormat(esmx, "Número mejicano: {1}\n", f); 
Console.WriteLine(b); 

/* Imprime: 
Número español: 1.983,42 
Número mejicano: 1,983.42 
*/ 

Aparte de un CultureInfo, puedes pasar un NumberFormatInfo para el formateo de números, un DateTimeFormatInfo para el formateo de fechas, o bien puedes implementar tu propio IFormatProvider / ICustomFormatter.

¿Qué operaciones podemos hacer sobre el texto con el StringBuilder?

Evidentemente Append y AppendFormat, junto con ToString, son los métodos más utilizados. Sin embargo, también puedes realizar las siguientes operaciones.

1.- Eliminar todo el contenido del búfer, mediante el método Clear.

2.- Añadir un texto seguido de una nueva línea, mediante AppendLine.

3.- Copiar un bloque de caracteres por posición, a un array de caracteres, mediante CopyTo.

4.- Insertar caracteres en una posición determinada. El método Insert, funciona similar a Append, sólo que se hace en una posición del búfer determinada.

5.- Eliminar un rango de caracteres del búfer, mediante Remove.

6.- Asegurar la capacidad de un búfer, mediante EnsureCapacity.

7.- Remplazar una sub-cadena de texto por otra, mediante Replace.

StringBuilder text = 
    new StringBuilder("Tres tristes tigres tragaban trigo en un trigal"); 
text.EnsureCapacity(200); 

// text.Capacity >= 200 
text.AppendLine(); 

// añade un carácter \n al final 
char[] chars = new char[10]; 
text.CopyTo(13, chars, 0, 6); 
// chars == { t, i, g, r, e, s } 
text.Insert(12, 42); 
// text == "Tres tristes42 tigres tragaban trigo en un trigal" 
text.Remove(5, 10); 
// text == "Tres tigres tragaban trigo en un trigal" 
text.Replace("tigres", "pollos"); 
// text == "Tres pollos tragaban trigo en un trigal"; 
text.Clear(); 
// text == "" 
// text.Capacity >= 200, ésta propiedad no se afecta 

¿Qué otros métodos para manipular texto existen?

Básicamente hay dos categorías: la manipulación de texto directa y la búsqueda de cadenas avanzada. Esta última utiliza expresiones regulares, lo cual queda fuera del alcance de esta entrada.

Para la manipulación de texto directa, la clase String expone diferentes métodos que nos ayudan con esto. Básicamente puedes realizar lo siguiente.

1.- Comparar cadenas de texto contra otras cadenas, otros valores o sub-cadenas.

2.- Concatenar cadenas, valores o sub-cadenas.

3.- Buscar valores, sub-cadenas de texto, etc.

4.- Formateo de cadenas de texto.

5.- Normalizar cadenas de texto (i.e. cambiar caracteres Unicode combinados por un solo carácter).

6.- Unir y separar cadenas de texto a partir de arrays, cadenas u objetos.

7.- Remover y remplazar caracteres y valores.

8.- Transformar los valores internos de la cadena de texto.

9.- Convertir cadena de texto a otros tipos de datos.

¿Cómo se pueden comparar cadenas de texto entre sí?

Hay diferentes tipos de comparaciones. La primera es la de igualdad, y el método en cuestión es Equals. Este método viene en muchos sabores. En primer lugar, permite comparar contra cualquier objeto, dado que sobrescribe Object.Equals. Sin embargo, esta comparación regresará false si el parámetro no es un string.

El segundo sabor es el Equals con una cadena de texto como parámetro. Este método hace una comparación carácter a carácter, se acuerdo a la cultura actual. String tiene un método estático homónimo que compara dos cadenas. Ambos métodos hacen lo mismo.

string s1 = "Pollo"; 
string s2 = "Pollo"; 
string s3 = "pollo"; 

bool val; 
val = s1.Equals(s2); // val == true 
val = s2.Equals(s3); // val == false 
val = string.Equals(s1, s3); // val == false 
val = string.Equals(s2, s1); // val == true 

El tercer sabor es similar al anterior: dos métodos Equals, uno de instancia y otro estático, que adicional a la cadena de texto, toma como parámetro una enumeración que indica el tipo de comparación a realizar: cultura actual o invariante, ignorar capitalización (mayúsculas y minúsculas) para la cultura actual o cultura invariante, o bien comparar de forma ordinal tomando en cuenta e ignorando la capitalización.

string s1 = "Pollo"; 
string s2 = "Pollo"; 
string s3 = "pollo"; 

bool val; 
// val == true 
val = s2.Equals(s3, StringComparison.CurrentCultureIgnoreCase); 
// val == true 
val = string.Equals(s1, s3, StringComparison.InvariantCultureIgnoreCase); 

¿Y cómo comparo cadenas alfabéticamente?

Con el siguiente tipo de comparación de relación: mayor qué y menor qué. Cuando se comparan dos caracteres, se dice que uno es mayor que el otro, si el primero está después en la tabla de posiciones Unicode. Una forma fácil de ver esto es en el programa Mapa de Caracteres, de Windows.

clip_image001

Aquí vemos que N es menor que Q, y que Q es menor que n minúscula. Por otra parte, también podemos comparar cualquier tipo de símbolos, según dicha tabla.

Así pues, decimos que una cadena de texto es menor que otra cuando conforme se van comparando carácter a carácter, se encuentra conque un carácter es menor que el de la otra en el mismo índice.

azul < carro, porque a < c.

auto < azul porque a == a, y u < z.

ampolla < amputar porque a == a, m == m, p == p, o < u.

Y así sucesivamente. Aquí vale la pena aclarar que las comparaciones pueden hacerse de dos formas: invariantes o por cultura. Una comparación por cultura toma en cuenta el idioma, y ciertas reglas explícitas de comparación pueden ser verdaderas para una cultura, pero falsas para otra. Por ejemplo:

ß == ss para de-DE (alemán de Alemania) y falso en español.

eßen == essen // de-DE

eßen > essen // es-ES

á == a para español, pero á > a para alemán.

ámpula == ampula // es-ES

ámpula > ampula // en-US

La comparación invariante no distingue entre culturas.

El primer método de comparación que veremos, según las reglas que hemos visto, es Compare. Éste es un método estático que nos permite comparar entre cadenas de texto. Prácticamente todos los métodos de comparación que veremos regresan un valor entero (Int32): menor a cero cuando la primera cadena es menor que la segunda, mayor a cero cuando la primera cadena es mayor a la segunda, o cero si son iguales (en cuyo caso, Equals regresaría true).

El método Compare tiene muchas sobrecargas, que permiten indicar cultura, si se ignora capitalización o no, si se comparan sub-cadenas, si se compara con opciones (ignorar capitalización, símbolos, espacios en blanco, etc.) y así sucesivamente. Algunos ejemplos a continuación (suponiendo que mi cultura actual es español de México).

int ret; 

CultureInfo dede = new CultureInfo("de-DE"); 
ret = string.Compare("Das Maedchen", "Das Mädchen"); 
// ret < 0 porque a < ä 
ret = string.Compare("Das Maedchen", "Das Mädchen", true, dede); 
// ret == 0 porque ae == ä en alemán 
ret = string.Compare("das maedchen", "DAS MÄDCHEN", 
    false, dede); 
// ret == 0 porque se ignora capitalización y ae == Ä en alemán 
string url = "https://fermasmas.wordpress.com"; 
ret = string.Compare(url, 0, "http:", 0, 5, 
StringComparison.InvariantCultureIgnoreCase); 
// ret == 0 porque se compara url con http: en los primeros cinco caracteres. 

Etcétera. El método Compare es estático, pero la clase String tiene un método a nivel de instancia, llamada CompareTo. Este método funciona prácticamente igual que Compare, aunque sólo tiene dos sobrecargas: una que compara contra un objeto, el cual se evalúa a string (i.e. ToString()); y otra que compara contra otro string, tomando en cuenta capitalización y cultura actual.

int ret; 
string str = "Das Maedchen"; 
ret = str.CompareTo("Das Mädchen"); // ret < 0 porque a < ä 
ret = str.CompareTo("Das maedchen"); // ret > 0 porque M > m 
str = "42"; 
ret = str.CompareTo(42); // ret == 0 porque "42" == (42).ToString() 

Otro asunto: existe un método estático llamado CompareOrdinal. Este método hace la comparación evaluando los valores numéricos de cada carácter. Así, es totalmente agnóstico de la cultura empleada.

Ya por último, si en algún momento quieres comparar una cadena de texto vacía o nula, en lugar de hacer esto:

if (str != null || str != "") { … } 

Mejor hacer esto:

if (!string.IsNullOrEmpty(str)) { … } 

El método IsNullOrEmpty regresa verdadero si el parámetro es nulo o está vacío. También existe isNullOrWhiteSpace, si el parámetro es nulo, está vacío o sólo contiene espacios blancos.

¿Qué hay respecto a la concatenación de cadenas?

La forma más fácil es usar los operadores + y +=. Pero ya vimos que no es muy eficiente, sobre todo cuando concatenamos muchas cadenas y valores. Asimismo, ya hablamos sobre String.Format y StringBuilder. Podemos usar ambas para concatenar cadenas.

Adicional a esto, la clase String cuenta con un método estático, llamado Concat. Este método, con todas sus sobrecargas, permite concatenar dos o más cadenas, objetos y valores. Al final, regresa una cadena de texto con la cadena final.

object[] vals = { "Dos y dos son ", 
    2 + 2, 
    ", cuatro y dos son ", 
    4 + 2 }; 

string s = string.Concat(vals); // "Dos y dos son 4, cuatro y dos son 6"

 

¿Y cómo podemos realizar búsquedas de cadenas de texto?

Hay muchos métodos encaminados a buscar cadenas de texto de forma directa (es decir, sin utilizar expresiones regulares). Se me ocurre clasificarlos en tres grupos: aquellos que indican concordancia, aquellos que regresan un índice, y aquellos que regresan una porción de texto.

Para el primer grupo, tenemos los siguientes métodos que presentamos a continuación. Todos regresan true o false, dependiendo de si se cumple la condición de búsqueda. Nota: consideremos la siguiente cadena para los ejemplos.

string s = "Cuántos pollos comen melón, cuántos sandía"; 

1.- El método Contains regresa true si una cadena de texto está contenida dentro de otra.

s.Contains(""); // true, una cadena vacía siempre está contenida. 
s.Contains("pollo"); // true; 
s.Contains("chabacano"); // false 

2.- El método EndsWith regresa true si una cadena de texto tiene una terminación determinada. Toma en cuenta cultura y comparación de cadenas (i.e. ordinal, invariante o sensible a la cultura actual).

s.EndsWith("sandía"); // true 
s.EndsWith("sandia"); // false 
s.EndsWith("SANDÍA", StringComparison.CurrentCultureIgnoreCase); // true 
s.EndsWith("SANDIA", true, new CultureInfo("es-MX")); // true 

3.- La contraparte al método anterior es StartsWith, que regresa true si una cadena de texto tiene un comienzo determinado.

s.StartsWith("Cuántos"); // true 
s.StartsWith("Cuantos"); // false 
s.StartsWith("CUÁNTOS", StringComparison.CurrentCultureIgnoreCase); // true 
s.StartsWith("CUANTOS", true, new CultureInfo("es-MX")); // true 

El segundo grupo cuenta también con varios métodos, que básicamente regresan un número. Éste es el índice a partir del cual se encuentra la cadena a ser buscada. Si el valor es menor a cero, es que la cadena no fue encontrada.

1.- El método directo para encontrar el índice es IndexOf. Este método busca la primera coincidencia de la cadena de texto pasada como parámetro. IndexOf busca también carateres, y permite especificar opciones de formato, como cultura o capitalización.

int index; 

index = s.IndexOf("pollo"); // index == 8 
index = s.IndexOf("Cuántos"); // index == 0 
index = s.IndexOf("cuántos"); // index == 28 
index = s.IndexOf("cuántos" 
    StringComparison.CurrentCultureIgnoreCase); // index == 0 
index = s.IndexOf("Melón"); // index == -1 
index = s.IndexOf("Melón", 
    StringComparison.CurrentCultureIgnoreCase); // index == 21 

2.- Un converso de IndexOf es LastIndexOf. Hace lo mismo, sólo que busca la última coincidencia, a diferencia de IndexOf que busca la primera coincidencia.

int index; 

index = s.LastIndexOf("Cuántos"); // index == 0 
index = s.LastIndexOf("cuántos"); // index == 28 
index = s.LastIndexOf("cuántos", 
    StringComparison.CurrentCultureIgnoreCase); // index == 28 

3.- Existe un método que permite buscar un conjunto de caracteres, en un array, y regresa el índice del primer carácter encontrado. Éste es IndexOfAny.

int index; 
char[] chars = new char[] { 'n', 'o', 's' }; 
index = s.IndexOfAny(chars); // index == 3, en "Cuántos" 

4.- El converso es LastIndexOfAny, que busca caracteres pero comenzando por el último.

int index; 
char[] chars = new char[] { 'o', 'n', 's' }; 
index = s.IndexOfAny(chars); // index == 38, en "sandía" 

El último grupo es un solo método, y es similar a la familia IndexOf, pero al revés: le pasas un índice y te regresa una sub-cadena. Este método es Substring, el cual tiene dos sobrecargas: la primera obtiene la cadena a partir de un índice, y la segunda obtiene la cadena a partir del índice y de una longitud determinada.

string str = s.Substring(8, 6); // str == "pollos" 

// Este método funciona particularmente bien en conjunto con IndexOf. 

string s = "Cuántos {pollos} comen melón, cuántos sandía"; 
string io = s.IndexOf('{'); 
string if = s.IndexOf('}'); 
string ss = s.Substring(io, if - io); 
Console.WriteLine(ss); // pollos 

Ok, basta de búsquedas. ¿Cómo se puede formatear cadenas de texto?

Ya hemos analizado tanto string.Format como StringBuilder.AppendFormat, métodos que nos ayudan a generar formato. Pero podemos ahondar un poquito más. El método string.Format le pasa el control directamente a StringBuilder, por lo que hablaremos de ambos métodos de forma indistinta, bajo este entendido.

En fin, String.Format siempre interactúa con respecto a la cultura local. Normalmente esto suele ser suficiente: si la computadora está en español de México, pues tendrás puntos decimales, comillas de miles, etc. Si estás en español de España, pues es al revés. Hay ocasiones, sin embargo, en las que necesitamos forzar el uso de un idioma o cultura en particular. Para ello, String.Format (y de hecho prácticamente cualquier método que implique conversión entre un tipo de dato y texto) tiene una sobrecarga que permite pasar por parámetro un IFormatProvider.

El IFormatProvider por excelencia es CultureInfo. Esta clase representa una cultura, y por lo tanto el objeto de formato que implementa formatea las cadenas de texto conforme a las reglas de esa cultura.

CultureInfo japones = new CultureInfo("ja-JP"); 
CultureInfo aleman = new CultureInfo("de-DE"); 
CultureInfo espanol = new CultureInfo("es-MX");
 
float c = "42.06"; 
string s = string.Format("{0:C}", japones); 
Console.WriteLine(s); 
string s = string.Format("{0:C}", aleman); 
Console.WriteLine(s); 
string s = string.Format("{0:C}", espanol); 
Console.WriteLine(s); 

/* 
* ¥ 42.06 
* 42,06 € 
* $ 42.06 
*/ 

Veamos un par de cosas que pasaron. En primer lugar creamos tres objetos de cultura: japonés de Japón, alemán de Alemania, y español de México. Luego, usamos string.Format para realizar un formato. Vemos que en la cadena de formato, pasamos {0:C}. Normalmente pasamos {0}. Ese :C de más quiere decir que formatearemos la entrada como una moneda, en este caso el yen japonés, el euro alemán y el peso mexicano. La C determina el tipo de formato, y la cultura, la forma de llevarlo a cabo.

Otros ejemplos son: D para decimal, E para notación exponencial o científica, F para punto decimal fijo (i.e. 4 => 4.0), G para la versión general, es decir, la más compacta posible; N para un número con separador de miles, decimales, etc.; P para porcentaje, R para redondear, X para hexadecimal.

s = string.Format("{0:D}", 99901); // s == 99901 
s = string.Format("{0:D10}", 99901); // s == 0000099901 
s = string.Format("{0:E}", 1983.42"); // s == 1.98342E+003 
s = string.Format("{0:F2}", 1983"); // s == 1983.00 
s = string.Format("{0:F3}", 1983.4266"); // s == 1983.427 
s = string.Format(japones, "{0:N}", 1983.55); // s == 1.983,55 
s = string.Format("{0:P}", 0.42); // s == 42 % 
s = string.Format("{0:X4}", 255); // s == 00FF 

Si ninguna de estas nos satisface, podemos crear nuestro propio formato mediante el formato de números especializado. Para hacer esto, podemos colocar en lugar del D, E, C, etc., un 0 para representar un dígito, si existe, o un cero si no; un # para representar un dígito si existe, si no, no muestra nada; El punto . para separar decimales y la coma para separar grupos. Asimismo, tenemos % para porcentaje, ‰ para por-miles, E0 para notación exponencial, ‘cadena’ para poner una cadena de texto fija.

s = string.Format("{0:0,00.00#:D}", 99.1); // s == 0,99.10 
s = string.Format(aleman, "{0:0,00.00#:D}", 99.1); // s == 0.99,10 
s = string.Format("{0:000,000.000}", 9991); // s == 009,991.000 
s = string.Format("{0:#,#00}", 99.1); // s == 99 
s = string.Format("{0:#,#00}", 9.1); // s == 09 
s = string.Format("{0:#,#00}", 9999.1); // s == 9,999 
s = string.Format("{0:##-###-##}", 99942); // s == 999-42 
s = string.Format("{0:##-###-##}", 4299942); // s == 42-999-42 

El formato también podemos aplicarlo a las fechas, de forma muy similar a como se aplica para números. Existen muchos formatos, pero los más utilizados son los siguientes: se usa la d y D para una fecha corta y una fecha larga, respectivamente; f y F para fecha con tiempo corto y largo, u y U para formato universal corto y largo.

CultureInfo enUS = new CultureInfo("en-US"); 
CultureInfo deDE = new CultureInfo("de-DE"); 
CultureInfo esMX = new CultureInfo("es-MX"); 

DateTime tm = new DateTime(2012, 12, 04, 11, 05, 42); 
s = string.Format(esMX, "{0:d}", tm); // s == 12/04/2012 
s = string.Format(esMX, "{0:d}", tm); // s == 04/12/2012 
s = string.Format(deDE, "{0:D}", tm); // s == Dienstag, 4. Dezember 2012 
s = string.Format(enUS, "{0:f}", tm); // s == Tuesday, 4 Dic, 2012 11:05 

Etcétera. También podemos utilizar formato especial usando y, M, d, m, h, M, s para representar año, mes, día, minuto hora, segundo, etc. Por ejemplo, cuatro yes suponen año en formato de cuatro dígitos, como 1983. Dos yes y tendríamos 83.

s = string.Format(esMX, "{0:ddd d MMM}", tm); 
// s == mar 4 dic 
s = string.Format(esMX, "{0:dddd d MMMM}", tm); 
// s == martes 4 diciembre 
s = string.Format(esMX, "{0:H mm ss}", tm); 
// s == 11 05 42 
s = string.Format(esMX, "{0:y yy yyyy yyyyy}", tm); 
// s == 2 12 2012 02012 

¿Y qué significa eso de ‘normalizar’?

Cuando hablamos de normalización, en general, queremos decir que estandarizamos algo, es decir, que lo dejamos en su forma más sencilla. Haciendo una comparación un poco simple, pero ilustrativa, consideremos el siguiente número.

clip_image002

A primera instancia luce complicado. Sin embargo, si resuelves, te darás cuenta que el número es 42. Pues bien, decimos que 42 es la forma normalizada del número anterior.

Ahora bien, cuando hablamos de texto, la normalización también significa simplificar la cadena conservando el mismo valor. En este caso, la simplificación se da a nivel binario.

Para entender lo anterior, necesitamos recordar que las cadenas de texto son una secuencia de caracteres Unicode. Cada carácter Unicode está formado por dos bytes. Ahora bien, a nivel textual, existen algunos caracteres que pueden representarse por dos o más pares de bytes totalmente diferentes. Es decir, a nivel binario los valores son diferentes, pero a nivel de texto son iguales.

Por ejemplo, el carácter ắ podemos representarlo de tres formas:

s1 = 0x1EAF

s2 = 0x0103 + 0x0301

s3 = 0x0061 + 0x0306 + 0x0301

Cada uno de los números significa representa un byte. Así, podemos crear las siguientes cadenas de texto.

string a1 = new string( new char[] { '\u1EAF' }); 
string a2 = new string( new char[] { '\u0103', '\u0301' }); 
string a3 = new string( new char[] { '\u0061', '\u0306', '\u0301' }); 

Las tres cadenas imprimirían el mismo carácter. Pero al compararlas entre sí, especialmente usando la forma de comparación con String.CompareOrdinal, ninguna de las tres será igual a las demás. Estas divergencias pueden complicar ciertos algoritmos, como búsquedas de cadenas de texto.

Como quizás ya hayas adivinado, la normalización de una cadena de texto consiste, precisamente, en simplificar la representación binaria de un carácter o secuencia de caracteres. En el ejemplo anterior, significaría convertir a2 y a3 en a1. Esto es posible debido a que el estándar de Unicode define algunos algoritmos para hacer esto, llamados imaginativamente C, D, KC, y KD. Cada uno define ciertas reglas a seguir. El más utilizado, y el estándar en .NET, es el C.

La clase String, así, define un método llamado Normalize con dos sobrecargas, una que utiliza el algoritmo C por default, y una que permite seleccionar el tipo de algoritmo. Asimismo, define un método IsNormalized, que nos permite saber si una cadena está normalizada o no.

string a1 = new string( new char[] { '\u1EAF' }); 
string a2 = new string( new char[] { '\u0103', '\u0301' }); 
string a3 = new string( new char[] { '\u0061', '\u0306', '\u0301' }); 

if (!a1.IsNormalized()) 
    a1 = a1.Normalize(); 
if (!a2.IsNormalized(NormalizationForm.FormKD)); 
    a2 = a2.Noramlize(NormalizationForm.FormKD); 
if (!a3.IsNormalized()); 
    a3 = a3.Noramlize(); 

Cuando trabajamos con archivos o bases de datos y tenemos campos sobre los que haremos búsquedas (por ejemplo, cláusula "where" en una consulta "select" de SQL), es muy recomendable que normalices el texto antes de guardarlo en el archivo o base de datos.

¿Cómo puedo unir y separar cadenas de texto?

Ésta es útil y facilita. Para unir cadenas de texto, necesitamos ponerlas todas en un array o cualquier colección que implemente IEnumerable. Luego, invocamos al método estático Join. Este método toma como primer parámetro el separador. Es decir, cuando las unas puedes querer separarlas por algún carácter o cadena. Esta es tu oportunidad. El segundo parámetro es la colección.

string[] cylons { 
    "John", 
    "Leoben", 
    "D'Anna", 
    "Simon", 
    "Caprica Six", 
}; 

string text1 = string.Join("\nCylon: ", cylons); 
Console.WriteLine(text1); 

/* imprime: 
Cylon: John 
Cylon: Leoben 
Cylon: D'Anna 
Cylon: Simon 
Cylon: Caprica Six 
*/ 

string text2 = string.Join(", ", cylons); 
Console.WriteLine(text2); 

/* imprime: 
John, Leoben, D'Anna, Simon, Caprica Six 
*/ 

Para separar la cadena, hay que usar el método Split. Este método no estático toma como parámetro una cadena que representa un separador. Cada que se encuentre un separador, se corta la cadena y el bloque se añade a un array. Si el separador nunca es encontrado, entonces el array tendrá una sola cadena: la cadena original.

string text = "John-Leoben-D'Anna-Simon-Caprica Six"; 
string[] cylons = text.Split('-'); 

foreach (string cylon in cylons) { 
    Console.WriteLine(cylon); 
} 

/* imprime: 
John 
Leoben 
D'Anna 
Simon 
Caprica Six 
*/ 

Ok, ¿y ahora cómo remuevo o reemplazo caracteres?

Tenemos un par de métodos para ello. En primer lugar, Remove se encarga de remover los caracteres a partir de un índice y una longitud determinada.

string text = "John-Leoben-D'Anna-Simon-Caprica Six"; 
text = text.Remove(5, 7); 
// text == "John-D'Anna-Simon-Caprica Six" 
text = text.Remove(0, 5); 
// text == "D'Anna-Simon-Caprica Six" 
text = text.Remove(6); // del 6to carácter en adelante 
// text == "D'Anna" 

Y así es que nos quedamos con Lucy Lawless.

También podemos usar Replace. Este método substituye un carácter por otro, o una cadena de texto por otra.

string text = "John-Leoben-D'Anna-Simon-Caprica Six"; 

text = text.Replace("John", "Saul") 
    .Replace("Leoben", "Galen") 
    .Replace("Simon", "Ellen") 
    .Replace("D'Anna", "Anders"); 
// text == "Saul-Galen-Anders-Ellen-Caprica Six" 
text = text.Replace('a', '*') 
    .Replace('l', '1'); 
// text == "S*u1-G*1en-Anders-E11en-C*prica Six" 

Dado que cada Replace regresa una cadena regresa de texto, es posible encadenarlas de esta forma. Por cierto, que Replace puede actuar también como Remove, si somos imaginativos:

string text = "John-Leoben-D'Anna-Simon-Caprica Six"; 

text.Replace("John-", string.Empty) 
    .Replace("D'Anna-", string.Empty) 
    .Replace("-Caprica Six", string.Empty); 
// text == "Leoben-Simon 

¿Y de qué forma podemos transformar valores de una cadena?

Existen varias transformaciones que podemos hacer. La primera es que podemos insertar caracteres dentro de una cadena, mediante el método Insert. A este método le pasamos un índice y la cadena a insertar.

En segundo lugar, podemos alinear texto al margen izquierdo o margen derecho mediante las funciones PadLeft y PadRight, respectivamente. El primer parámetro es el número de caracteres totales a alinear. El segundo, el carácter con el que queremos llenar la alineación.

Convertir entre mayúsculas y minúsculas también entran en acción. Tenemos a ToLower y ToUpper, por supuesto, para convertir la cadena a puras minúsculas o puras mayúsculas. Una sobrecarga de estos métodos toma como parámetro un objeto CultureInfo, que nos permite especificar el idioma. Si no pasamos cultura, entonces se toma la cultura por default. Si queremos convertir sin importar la cultura, usamos ToLowerInvariant y ToUpperInvariant.

Y ya por último, a veces querremos eliminar espacios en blanco. Esto es muy útil sobre todo cuando trabajamos con bases de datos, que nos llega con espacios en blanco antes y después. Entonces podemos usar el método TrimStart y TrimEnd, para eliminar los espacios al inicio y al final de la cadena, respectivamente. O Trim, que lo hace en las dos al mismo tiempo. Por cierto, si a algún método le pasamos un array de caracteres, dichos métodos los eliminarán antes o después. Básicamente estos tres métodos son el converso de PadLeft y PadRight.

string str = "Todos los pollos van al cielo."; 

str = str.Insert(17, "siempre "); 
// str == "Todos los pollos siempre van al cielo." 
str = str.PadLeft(45); 
// str == " Todos los pollos siempre van al cielo." 
str = str.PadRight(50); 
// str == " Todos los pollos siempre van al cielo. " 
str = str.PadRight(55, '*'); 
// str == " Todos los pollos siempre van al cielo. *****" 
str = str.ToUpper(); 
// str == " TODOS LOS POLLOS SIEMPRE VAN AL CIELO. *****" 
str = str.ToLower(); 
// str == " todos los pollos siempre van al cielo. *****" 
str = str.TrimLeft(); 
// str == "todos los pollos siempre van al cielo. *****" 
str = str.TrimRight('*'); 
// str == "todos los pollos siempre van al cielo. " 
str = str.Trim(); 
// str == "todos los pollos siempre van al cielo."

Y ya para concluir la lista (¡uf!), ¿cómo se convierten cadenas de texto en otros tipos de dato?

La verdad es que String implementa explícitamente la interfaz IConvertible. Esta interfaz permite convertir a otros tipos de datos. Por lo que a partir de una cadena, sólo tienes que obtener la referencia directa a IConvertible y usar el método que apetezcas. Nota que para todos los tipos de datos numéricos aplican cuestiones de formato.

CultureInfo ci = new CultureInfo("es-ES"); 
string str "42,06"; 
IConvertible cv = str as IConvertible; 
double d = cv.ToDouble(ci); 
str = "True"; 
cv = str as IConvertible; 
bool b = cb.ToBoolean(); 
str = "04/12/2012"; 

ci = new CultureInfo("es-MX"); 
cv = str as IConvertible; 

DateTime tm = cv.ToDateTime(ci); 

Esto es quizás un poquito engorroso. Una alternativa quizás un poco más sencilla es utilizar los métodos de conversión (usualmente estáticos) que proveen las clases de los tipos básicos. Por ejemplo, casi todas estas clases tienen dos métodos sobrecargados: Parse y TryParse. Ambas intentan convertir una cadena de texto en su tipo base (por ejemplo, Int32.Parse convierte a int, Double.Parse convierte a double, etc.). La diferencia es que Parse lanza una excepción si no puede realizarse la conversión, y TryParse regresa un valor por default si no puede realizarla.

CultureInfo ci = new CultureInfo("es-ES"); 
string str = "42,06"; 
double d = double.Parse(str, ci); 
int i; 
bool ret = int.TryParse("NoEsNúmero", out i); 
// ret == false y i == 0 
str = "True"; 
bool b = bool.Parse(str 
str = "04/12/2012"; 
ci = new CultureInfo("es-MX"); 
DateTime tm; 
ret = DateTime.TryParse(str, ci); 
// ret == true y tm == 4/12/2012 

Por último, la plataforma también contiene la clase Convert. Esta clase estática tiene muchos métodos para convertir de cualquier tipo de dato a cualquier otro tipo de dato.

CultureInfo ci = new CultureInfo("es-ES"); 

string str = "42,06"; 
double d = Convert.ToDouble(str, ci); // d == 42.06 

int i = Convert.ToInt32("1.983", ci); // i == 1983 
i = Convert.ToInt32("0xFF", 16); // i == 255 
i = Convert.ToInt32("100110000011111001", 2) // i == 155897 

str = "True"; 
bool b = Convert.ToBoolean(str); // b == true; 

str = "04/12/2012"; 
ci = new CultureInfo("es-MX"); 
DateTime tm = Convert.ToDateTime(str, ci); // tm == 4/12/2012

¿Qué nos falta?

Bueno, hasta ahora hemos visto cómo manipular cadenas de texto, y hemos visto mejores prácticas, en particular, utilizando a StringBuilder. Nos falta un tema que es importante, aunque sale un poquito del alcance de esta entrada: los flujos de datos.

¿Qué tienen que ver los flujos de datos?

Primero, un flujo de dato es una secuencia ordenada de bytes. En .NET, la clase que representa a los flujos se llama Stream, ubicada en el espacio de nombres System.IO. De momento no ahondaremos más en el tema de los flujos (puedes consultar la entrada Todo sobre flujo de datos). Pero retomaremos el concepto de leer y escribir datos en los flujos.

¿Qué la clase Stream no tiene métodos para leer y escribir?

Sí, pero de hecho esta clase no escribe sobre un Stream, sino que…

¿Y entonces?

Calma. Estas clases heredan de un par: TextReader y TextWriter. Estas clases base son las usadas para leer y escribir en flujos, y las clases derivadas: StreamReader y StreamWriter nos permiten hacerlo en un flujo. Sin embargo, hay veces en la que nos interesará reutilizar los algoritmos escritos para TextReader y TextWriter, pero para crear cadenas de texto. Ese es el propósito de estas dos clases: StringReader y StringWriter.

Ambas clases guardan todo el flujo dentro de un… ajam… StringBuilder. Así, podemos usarlas de esta forma.

StringWriter w = new StringWriter(); 
w.WriteLine("Todos los pollos van al cielo."); 
w.Write("Un número: "); 
w.WriteLine(42); 
w.Write("Un booleano: "); 
w.WriteLine(false); 
str = w.ToString(); // construye la cadena de texto 
w.Close(); 

StringReader r = new StringReader(str); 

do 

{ 
    str = r.ReadLine(); 
    Console.WriteLine("Ln: {0}", str ?? "EOF"); 
} 
while (str != null); 

r.Close(); 

/* imprime: 
Ln: Todos los pollos van al cielo. 
Ln: Un número: 42 
Ln: Un booleano: false 
*/ 

Realmente usarlas así no aporta mucho, pues podemos usar StringBuilder directamente. Sin embargo, puede servirnos para reutilizar algoritmos de lectura / escritura de datos, sin importar la fuente. Y ahí StringBuilder no nos puede ayudar, me temo.

void CreateHtml(TextWriter w) 

{ 
    w.WriteLine("<html>"); 
    w.WriteLine("<body>"); 
    w.WriteLine("<h1>Todos los pollos van al cielo.</h1>"); 
    w.WriteLine("<p>Lorem ipsum dolor sit amet, qua micas la pater "
        + "santis sempire damore denarii.</p>"); 
    w.WriteLine("</body>"); 
    w.WriteLine("</html>"); 
} 

StreamWriter file = new StreamWriter("C:\\miarchivo.txt"); 
CreateHtml(file); 
file.Close(); 
StringWriter text = new StringWriter(); 
CreateHtml(text); 
Console.WriteLine(text.ToString()); 
text.Close();

Aquí vemos cómo podemos escribir el texto a un archivo o a la consola, reutilizando el mismo método CreateHtml.

¿Y ahora sí ya estuvo?

Ya estuvo. Las cadenas de texto son el alma de todos los programas, pues es como mejor representamos la información. Cuídalas para que no sufras por memoria. Siempre prefiere usar StringReader y StringWriter sobre string, y StringBuilder sobre todas. También revisa las expresiones regulares, otra forma de buscar cadenas de texto, más avanzada.

Nos vemos pronto con más de esta serie: C# 101.

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

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: 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: , ,

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!

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: , ,

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: , ,