Tema 3: El
preprocesador
(C) 2001 José Antonio González Seco
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.
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
#error 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, que es la usada por Microsoft en sus códigos de
ejemplo. Conviene familiarizarse con ella porque por un lado hay
mucho código escrito que la usa y por otro usarla facilita
la lectura de nuestro código a los demás al ser 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, pues 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 es posible incluir antes de él otras
directivas de preprocesado con total libertad.
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 #definesó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 alguna, 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 #endif 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
{
public 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 este valdrá true o false según
esté o no definido. Pues bien, estos 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 truey false.
Por ejemplo:
#if TRAZA // Se cumple si TRAZA esta definido.
#if TRAZA==true // Idem al ejemplo anterior aunque con una sintaxis menos cómoda
#if !TRAZA // Sólo se cumple si TRAZA no está definido.
#if TRAZA==false // Idema 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 estos 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 que 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
#errorson 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 #lineindica 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.
Marcación de
regiones de código
Es posible marcar regiones de código y asociarles un nombre
usando el juego de directivas #regiony
#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:


Hay que tener cuidado al anidar regiones con directivas de
compilación condicional, ya que todo bloque
#if...#endifque 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