gravatar

El lenguaje de programación C#. El preprocesador.

Autor: José Antonio González Seco

Curso completo disponible en formato DOC y PDF en la web del autor.

Concepto de preprocesador

El preprocesado es un paso previo a la compilación mediante el que es posible controlar la forma en que se realizará ésta. El preprocesador es el módulo auxiliar que utiliza el compilador para realizar estas tareas, y lo que finalmente el compilador compila es el resultado de aplicar el preprocesador al fichero de texto fuente, resultado que también es un fichero de texto. Nótese pues, que mientras que el compilador hace una traducción de texto a binario, lo que el preprocesador hace es una traducción de texto a texto.

Aquellos que tengan experiencia en el uso del preprocesador en lenguajes como C++ y conozcan los problemas que implica el uso del mismo pueden respirar tranquilos, ya que en C# se han eliminado la mayoría de características de éste que provocaban errores difíciles de detectar (macros, directivas de inclusión, etc.) y prácticamente sólo se usa para permitir realizar compilaciones condicionales de código.

Seguir Leyendo...

Directivas de preprocesado

Concepto de directiva. Sintaxis

El preprocesador no interpreta de ninguna manera el código fuente del fichero, sino que sólo interpreta de dicho fichero lo que se denominan directivas de preprocesado. Estas directivas son líneas de texto del fichero fuente que se caracterizan porque en ellas el primer carácter no blanco que aparece es una almohadilla (carácter #) Por ejemplo:

#define TEST
Ha habido un error fatal
No se preocupe ahora si no entiendo el significado de estas directivas, ya que se explicarán más adelante. Lo único debe saber es que el nombre que se indica tras el símbolo # es el nombre de la directiva, y el texto que se incluye tras él (no todas las directivas tienen porqué incluirlo) es el valor que se le da. Por tanto, la sintaxis de una directiva es:
#<nombreDirectiva> <valorDirectiva>
Es posible incluir comentarios en la misma línea en que se declara una directiva, aunque estos sólo pueden ser comentarios de una línea que empiecen con //. Por ejemplo, el siguiente comentario es válido:
#define TEST  // Ha habido algún error durante el  preprocesado
Pero este otro no, pues aunque ocupa una línea tiene la sintaxis de los comentarios que pueden ocupar varias líneas:
#define TEST /* Ha habido algún  error durante el preprocesado */
Definición de identificadores de preprocesado

Como ya se ha comentado, la principal utilidad del preprocesador en C# es la de permitir determinar cuáles regiones de código de un fichero fuente se han de compilar. Para ello, lo que se hace es encerrar las secciones de código opcionales dentro de directivas de compilación condicional, de modo que sólo se compilarán si determinados identificadores de preprocesado están definidos. Para definir un identificador de este tipo la directiva que se usa sigue esta sintaxis:
#define <nombreIdentificador>
Esta directiva define un identificador de preprocesado <nombreIdentificador>. Aunque más adelante estudiaremos detalladamente cuáles son los nombres válidos como identificadores en C#, por ahora podemos considerar que son válidos aquellos formados por uno o más caracteres alfanuméricos tales que no sean ni true ni false y no empiecen con un numero. Por ejemplo, para definir un identificador de preprocesado de nombre PRUEBA se haría:
#define PRUEBA
Por convenio se da a estos identificadores nombres en los que todas las letras se escriben en mayúsculas, como en el ejemplo anterior. Aunque es sólo un convenio y nada obliga a usarlo, ésta será la nomenclatura que usaremos en el presente documento ya que es la que sigue Microsoft en sus códigos de ejemplo. Conviene familiarizarse con ella porque hay mucho código escrito que la usa y porque emplearla facilitará a los demás la lectura de nuestro código ya que es la notación que esperarán encontrar.

Es importante señalar que cualquier definición de identificador ha de preceder a cualquier aparición de código en el fichero fuente. Por ejemplo, el siguiente código no es válido puesto que en él antes del #define se ha incluido código fuente (el class A):
class A
#define PRUEBA
{}
Sin embargo, aunque no pueda haber código antes de un #define sí que existe total libertad para precederlo de otras directivas de preprocesado.

Existe una forma alternativa de definir un identificador de preprocesado y que además permite que dicha definición sólo sea válida en una compilación en concreto. Esta forma consiste en pasarle al compilador en su llamada la opción /d:<nombreIdentificador> (forma abreviada de /define:<nombreIdentificador>), caso en que durante la compilación se considerará que al principio de todos los ficheros fuente a compilar se encuentra definido el identificador indicado. Las siguientes tres formas de llamar al compilador son equivalentes y definen identificadores de preprocesado de nombres PRUEBA y TRAZA durante la compilación de un fichero fuente de nombre ejemplo.cs
csc /d:PRUEBA /d:TRAZA  ejemplo.cs
csc /d:PRUEBA,TRAZA ejemplo.cs
csc /d:PRUEBA;TRAZA ejemplo.cs
Nótese en el ejemplo que si queremos definir más de un identificador usando esta técnica tenemos dos alternativas: incluir varias opciones /d en la llamada al compilador o definir varios de estos identificadores en una misma opción /d separándolos mediante caracteres de coma (,) o punto y coma (;)

Si se trabaja con Visual Studio.NET en lugar de directamente con el compilador en línea de comandos, entonces puede conseguir mismo efecto a través de View à Property Pages à Configuration Options à Build à Conditional Compilation Constants, donde nuevamente usado el punto y coma (;) o la coma (,) como separadores, puede definir varias constantes. Para que todo funcione bien, antes de seleccionar  View ha de seleccionar en el Solution Explorer (se abre con View à Solution Explorer) el proyecto al que aplicar la definición de las constantes.

Finalmente, respecto al uso de #define sólo queda comentar que es posible definir varias veces una misma directiva sin que ello provoque ningún tipo de error en el compilador, lo que permite que podamos pasar tantos valores a la opción /d del compilador como queramos sin temor a que entren en conflicto con identificadores de preprocesado ya incluidos en los fuentes a compilar.

Eliminación de identificadores de preprocesado

Del mismo modo que es posible definir identificadores de preprocesado, también es posible eliminar definiciones de este tipo de identificadores previamente realizadas. Para ello la directiva que se usa tiene la siguiente sintaxis:

#undef <nombreIdentificador>
En caso de que se intente eliminar con esta directiva un identificador que no haya sido definido o cuya definición ya haya sido eliminada no se producirá error alguno, sino que simplemente la directiva de eliminación será ignorada. El siguiente ejemplo muestra un ejemplo de esto en el que el segundo #undef es ignorado:
#define  VERSION1
#undef  VERSION1
#undef  VERSION1
Al igual que ocurría con las directivas #define, no se puede incluir código fuente antes de las directivas #undef, sino que, todo lo más, lo único que podrían incluirse antes que ellas serían directivas de preprocesado.

Compilación condicional

Como se ha repetido varias veces a lo largo del tema, la principal utilidad del preprocesador en C# es la de permitir la compilación de código condicional, lo que consiste en sólo permitir que se compile determinadas regiones de código fuente si las variables de preprocesado definidas cumplen alguna condición determinada. Para conseguir esto se utiliza el siguiente juego de directivas:
#if <condición1>
    <código1>
#elif <condición2>
    <código2>
...
#else
    <códigoElse>
#endif
El significado de una estructura como esta es que si se cumple <condición1> >entonces se pasa al compilador el <código1>, si no ocurre esto pero se cumple <condición2> entonces lo que se pasaría al compilador sería <código2>, y así continuamente hasta que se llegue a una rama #elif cuya condición se cumpla. Si no se cumple ninguna pero hay una rama #else se pasará al compilador el <códigoElse>, pero si dicha rama no existiese entonces no se le pasaría código alguno y se continuaría preprocesando el código siguiente al #endif en el fuente original.

Aunque las ramas #else y #eldif son opcionales, hay que tener cuidado y no mezclarlas, ya que la rama #else sólo puede aparecer como última rama del bloque #if...#endif.

Es posible anidar varias estructuras #if...#endif, como muestra el siguiente código:
#define  PRUEBA

using System;
class A { public static void Main() { #if PRUEBA Console.Write (“Esto es una prueba”); #if TRAZA Console.Write(“ con traza”); #elif !TRAZA Console.Write(“ sin traza”); #endif #endif } }
Como se ve en el ejemplo, las condiciones especificadas son nombres de identificadores de preprocesado, considerándose que cada condición sólo se cumple si el identificador que se indica en ella está definido. O lo que es lo mismo: un identificador de preprocesado vale cierto (true en C#) si está definido y falso (false en C#) si no.

El símbolo ! incluido en junto al valor de la directiva #elif es el símbolo de “no” lógico, y el #elif en el que se usa lo que nos permite es indicar que en caso de que no se encuentre definido el identificador de preprocesado TRAZA se han de pasar al compilador las instrucciones a continuación indicadas (o sea, el Console.Write(“sin traza”);

El código fuente que el preprocesador pasará al compilador en caso de que compilemos sin especificar ninguna opción /d en la llamada al compilador será:
using  System;

class A { static void Main() { Console.Write(“Esto es una prueba”); Console.Write(“ sin traza”); } }
Nótese como en el código que se pasa al compilador ya no aparece ninguna directiva de preprocesado, pues lo que el preprocesador le pasa es el código resultante de aplicar al original las directivas de preprocesado que contuviese.

Asimismo,  si compilásemos el código fuente original llamando al compilador con /d:TRAZA, lo que el preprocesador pasaría al compilador sería:
using  System;

class A { public static void Main() { Console.Write(“Esto es una prueba”); Console.Write(“ sin traza”); } }
Hasta ahora solo hemos visto que la condición de un #if o #elif puede ser un identificador de preprocesado, y que éste valdrá true o false según esté o no definido. Pues bien, éstos no son el único tipo de condiciones válidas en C#, sino que también es posible incluir condiciones que contengan expresiones lógicas formadas por identificadores de preprocesado, operadores lógicos (! para “not”, && para “and” y || para “or”), operadores relacionales de igualdad (==) y desigualdad (!=), paréntesis (( y )) y los identificadores especiales true y false. Por ejemplo:
#if TRAZA // Se cumple si TRAZA esta  definido.
#if  TRAZA==true // Ídem al ejemplo anterior aunque con una sintaxis menos cómoda
#if !TRAZA // Sólo se cumple si  TRAZA no está definido.
#if TRAZA==false // Ídem a al  ejemplo anterior aunque con una sintaxis menos cómoda
#if TRAZA == PRUEBA // Solo se cumple si tanto TRAZA como PRUEBA  están 
                    // definidos o si no ninguno lo  está.
#if TRAZA != PRUEBA // Solo se cumple si TRAZA esta definido y  PRUEBA no o 
                    // viceversa
#if TRAZA && PRUEBA // Solo  se cumple si están definidos TRAZA y PRUEBA.
#if TRAZA || PRUEBA // Solo se  cumple si están definidos TRAZA o PRUEBA.
#if false // Nunca se cumple (por lo  que es absurdo ponerlo)
#if true // Siempre se cumple (por  lo que es absurdo ponerlo)
Es fácil ver que la causa de la restricción antes comentada de que no es válido dar un como nombre true o false a un identificador de preprocesado se debe al significado especial que éstos tienen en las condiciones de los #if y #elif

Generación de avisos y errores

El preprocesador de C# también ofrece directivas que permiten generar avisos y errores durante el proceso de preprocesado en caso de llegar a ser interpretadas por el preprocesador. Estas directivas tienen la siguiente sintaxis:
#warning <mensajeAviso>
#error <mensajeError>
La directiva #warning lo que hace al ser procesada es provocar que el compilador produzca un mensaje de aviso que siga el formato estándar usado por éste para ello y cuyo texto descriptivo tenga el contenido indicado en <mensajeAviso>; y #error hace lo mismo pero provocando un mensaje de error en vez de uno de aviso.

Usando directivas de compilación condicional se puede controlar cuando se han de producir estos mensajes, cuando se han de procesar estas directivas. De hecho la principal utilidad de estas directivas es permitir controlar errores de asignación de valores a los diferentes identificadores de preprocesado de un código, y un ejemplo de ello es el siguiente:
#warning Código aun no revisado
#define PRUEBA
#if PRUEBA && FINAL
#error Un código no  puede ser simultáneamente de prueba y versión final
#endif
class A
{}
En este código siempre se producirá el mensaje de aviso, pero el #if indica que sólo se producirá el mensaje de error si se han definido simultáneamente los identificadores de preprocesado PRUEBA y FINAL.

Como puede deducirse del ejemplo, el preprocesador de C# considera que los mensajes asociados a directivas #warning o #error son todo el texto que se encuentra tras el nombre de dichas directivas y hasta el final de la línea donde éstas aparecen. Por tanto, todo comentario que se incluya en una línea de este tipo será considerado como parte del mensaje a mostrar, y no como comentario como tal. Por ejemplo, ante la directiva:
#error La compilación ha fallado //  Error
Lo que se mostrará en pantalla es un mensaje de la siguiente forma:
Fichero.cs(3,5): error CS1029: La  compilación ha fallado // Error
Cambios en la numeración de líneas

Por defecto el compilador enumera las líneas de cada fichero fuente según el orden normal en que estas aparecen en el mismo, y este orden es el que sigue a la hora de informar de errores o de avisos durante la compilación. Sin embargo, hay situaciones en las que interesa cambiar esta numeración, y para ello se ofrece una directiva con la siguiente sintaxis:
#line <número> <nombreFichero>
Esta directiva indica al preprocesador que ha de considerar que la siguiente línea del fichero fuente en que aparece es la línea cuyo número se le indica, independientemente del valor que tuviese según la numeración usada en ese momento. El valor indicado en <nombreFichero> es opcional, y en caso de aparecer indica el nombre que se ha de considerar que tiene el fichero a la hora de dar mensajes de error. Un ejemplo:
#line 127 “csmace.cs”
Este uso de #line indica que el compilador ha de considerar que la línea siguiente es la línea 127 del fichero csmace.cs. A partir de ella se seguirá usando el sistema de numeración normal (la siguiente a esa será la 128 de csmace.cs, la próxima la 129, etc.) salvo que más adelante se vuelva a cambiar la numeración con otra directiva #line.

Aunque en principio puede parecer que esta directiva es de escasa utilidad, lo cierto es que suele venir bastante bien para la escritura de compiladores y otras herramientas que generen código en C# a partir de código escrito en otros lenguajes.

Marcado de regiones de código

Es posible marcar regiones de código y asociarles un nombre usando el juego de directivas #region y #endregion. Estas directivas se usan así:
#region <nombreRegión>
        <código>
#endregion
La utilidad que se dé a estas marcaciones depende de cada herramienta, pero en el momento de escribir estas líneas la única herramienta disponible que hacía uso de ellas era Visual Studio.NET, donde se usa para marcar código de modo que desde la ventana de código podamos expandirlo y contraerlo con una única pulsación de ratón. En concreto, en la ventana de código de Visual Studio aparecerá un símbolo [-] junto a las regiones de código así marcadas de manera que pulsando sobre él todo el código contenido en la región se comprimirá y será sustituido por el nombre dado en <nombreRegión>. Tras ello, el [-] se convertirá en un [+] y si volvemos a pulsarlo el código contraído se expandirá y recuperará su aspecto original. A continuación se muestra un ejemplo de cada caso:

clip_image002

Ilustración 4: Código de región expandido


clip_image002[4]

Ilustración 5: Código de región contraído


Hay que tener cuidado al anidar regiones con directivas de compilación condicional, ya que todo bloque #if...#endif que comience dentro de una región ha de terminar también dentro de ella. Por tanto, el siguiente uso de la directiva #region no es valido ya que RegiónErrónea termina estando el bloque #if...#endif abierto:
#region RegiónErrónea
#if A
#endregion
#endif

(C) 2001 José Antonio González Seco



gravatar

Hola!

Yo estoy aprendiendo a programar en C, y mi pregunta es, ¿Cuál es la utilidad real del preprocesador, y con ello la declaración de macros y demás? La duda me surge debido a que se puede hacer lo mismo sin la necesidad de definirlas ahí y definiendolas como funciones o demás.

Gracias,
Javi :)