Directivas de preprocesador de C#

Aunque el compilador no tiene un preprocesador independiente, las directivas descritas en esta sección se procesan como si hubiera uno. Se usan para facilitar la compilación condicional. A diferencia de las directivas de C y C++, estas no se pueden usar para crear macros. Una directiva de preprocesador debe ser la única instrucción en una línea.

Contexto que admite un valor NULL

La directiva de preprocesador #nullable establece el contexto de anotación que admite un valor NULL y el contexto de advertencia que admite un valor NULL. Esta directiva controla si las anotaciones que admiten un valor NULL surten algún efecto y si se proporcionan advertencias de nulabilidad. Cada contexto está deshabilitado o habilitado.

Ambos contextos se pueden especificar en el nivel de proyecto (fuera del código fuente de C#) agregando el elemento Nullable al elemento PropertyGroup. La directiva #nullable controla los contextos de anotación y advertencia y tiene prioridad sobre la configuración de nivel de proyecto. Una directiva establece los contextos que controla hasta que otra directiva la invalida o hasta el final del archivo de código fuente.

El efecto de las directivas es el siguiente:

  • #nullable disable: establece los contextos de advertencia y anotación que admiten un valor NULL en deshabilitado.
  • #nullable enable: establece los contextos de advertencia y anotación que admiten un valor NULL en habilitado.
  • #nullable restore: restaura los contextos de advertencia y anotación que admiten un valor NULL a la configuración del proyecto.
  • #nullable disable annotations: establece el contexto de anotación que admite un valor NULL en deshabilitado.
  • #nullable enable annotations: establece el contexto de anotación que admite un valor NULL en habilitado.
  • #nullable restore annotations: restaura el contexto de anotación que admite un valor NULL a la configuración del proyecto.
  • #nullable disable warnings: establece el contexto de advertencia que admite un valor NULL en deshabilitado.
  • #nullable enable warnings: establece el contexto de advertencia que admite un valor NULL en habilitado.
  • #nullable restore warnings: restaura el contexto de advertencia que admite un valor NULL a la configuración del proyecto.

Compilación condicional

Para controlar la compilación condicional se usan cuatro directivas de preprocesador:

  • #if: abre una compilación condicional, donde el código solo se compila si se define el símbolo especificado.
  • #elif: cierra la compilación condicional anterior y abre una nueva en función de si se define el símbolo especificado.
  • #else: cierra la compilación condicional anterior y abre una nueva si no se ha definido el símbolo especificado anterior.
  • #endif: cierra la compilación condicional anterior.

El compilador de C# compila el código entre la directiva #if y la directiva #endif solo si se define el símbolo especificado o no se define cuando se usa el operador not !. A diferencia de C y C++, no se puede asignar un valor numérico a un símbolo. La instrucción #if en C# es booleana y solo comprueba si el símbolo se ha definido o no. Por ejemplo, el código siguiente se compila cuando DEBUG se define:

#if DEBUG
    Console.WriteLine("Debug version");
#endif

El código siguiente se compila cuando MYTESTno se define:

#if !MYTEST
    Console.WriteLine("MYTEST is not defined");
#endif

Puede usar los operadores == (igualdad) y != (desigualdad) para comprobar los valores booltrue o false. true significa que el símbolo está definido. La instrucción #if DEBUG tiene el mismo significado que #if (DEBUG == true). Puede usar los operadores && (y), || (o) y ! (no) para evaluar si se han definido varios símbolos. Es posible agrupar símbolos y operadores mediante paréntesis.

A continuación se muestra una directiva compleja que permite que el código aproveche las características más recientes de .NET mientras permanece compatible con versiones anteriores. Por ejemplo, imagine que usa un paquete NuGet en el código, pero el paquete solo admite .NET 6 y versiones posteriores, así como .NET Standard 2.0 y versiones posteriores:

#if (NET6_0_OR_GREATER || NETSTANDARD2_0_OR_GREATER)
    Console.WriteLine("Using .NET 6+ or .NET Standard 2+ code.");
#elif
    Console.WriteLine("Using older code that doesn't support the above .NET versions.");
#endif

#if, junto con las directivas #else, #elif, #endif, #define y #undef, permite incluir o excluir código basado en la existencia de uno o varios símbolos. La compilación condicional puede resultar útil al compilar código para una compilación de depuración o para una configuración concreta.

Una directiva condicional que empieza con una directiva #if debe terminar de forma explícita con una directiva #endif. #define permite definir un símbolo. Al usar el símbolo como la expresión que se pasa a la directiva #if, la expresión se evalúa como true. También se puede definir un símbolo con la opción del compilador DefineConstants. La definición de un símbolo se puede anular mediante #undef. El ámbito de un símbolo creado con #define es el archivo en que se ha definido. Un símbolo definido con DefineConstants o #define no debe entrar en conflicto con una variable del mismo nombre. Es decir, un nombre de variable no se debe pasar a una directiva de preprocesador y un símbolo solo puede ser evaluado por una directiva de preprocesador.

#elif permite crear una directiva condicional compuesta. La expresión #elif se evaluará si ninguna de las expresiones de directiva #if o #elif (opcional) precedentes se evalúan como true. Si una expresión #elif se evalúa como true, el compilador evalúa todo el código comprendido entre #elif y la siguiente directiva condicional. Por ejemplo:

#define VC7
//...
#if DEBUG
    Console.WriteLine("Debug build");
#elif VC7
    Console.WriteLine("Visual Studio 7");
#endif

#else permite crear una directiva condicional compuesta, de modo que, si ninguna de las expresiones de las directivas #if o #elif (opcional) anteriores se evalúan como true, el compilador evaluará todo el código entre #else y la directiva #endif siguiente. #endif(#endif) debe ser la siguiente directiva de preprocesador después de #else.

#endif especifica el final de una directiva condicional, que comienza con la directiva #if.

El sistema de compilación también tiene en cuenta los símbolos de preprocesador predefinidos que representan distintos marcos de destino en proyectos de estilo SDK. Resultan útiles al crear aplicaciones que pueden tener como destino más de una versión de .NET.

Versiones de .NET Framework de destino Símbolos Símbolos adicionales
(disponible en SDK de .NET 5+)
Símbolos de plataforma (solo disponibles
cuando se especifica un TFM específico del sistema operativo)
.NET Framework NETFRAMEWORK, NET48, NET472, NET471, NET47, NET462, NET461, NET46, NET452, NET451, NET45, NET40, NET35, NET20 NET48_OR_GREATER, NET472_OR_GREATER, NET471_OR_GREATER, NET47_OR_GREATER, NET462_OR_GREATER, NET461_OR_GREATER, NET46_OR_GREATER, NET452_OR_GREATER, NET451_OR_GREATER, NET45_OR_GREATER, NET40_OR_GREATER, NET35_OR_GREATER, NET20_OR_GREATER
.NET Standard NETSTANDARD, NETSTANDARD2_1, NETSTANDARD2_0, NETSTANDARD1_6, NETSTANDARD1_5, NETSTANDARD1_4, NETSTANDARD1_3, NETSTANDARD1_2, NETSTANDARD1_1, NETSTANDARD1_0 NETSTANDARD2_1_OR_GREATER, NETSTANDARD2_0_OR_GREATER, NETSTANDARD1_6_OR_GREATER, NETSTANDARD1_5_OR_GREATER, NETSTANDARD1_4_OR_GREATER, NETSTANDARD1_3_OR_GREATER, NETSTANDARD1_2_OR_GREATER, NETSTANDARD1_1_OR_GREATER, NETSTANDARD1_0_OR_GREATER
.NET 5+ (y .NET Core) NET, NET8_0, NET7_0, NET6_0, NET5_0, NETCOREAPP, NETCOREAPP3_1, NETCOREAPP3_0, NETCOREAPP2_2, NETCOREAPP2_1, NETCOREAPP2_0, NETCOREAPP1_1, NETCOREAPP1_0 NET8_0_OR_GREATER, NET7_0_OR_GREATER, NET6_0_OR_GREATER, NET5_0_OR_GREATER, NETCOREAPP3_1_OR_GREATER, NETCOREAPP3_0_OR_GREATER, NETCOREAPP2_2_OR_GREATER, NETCOREAPP2_1_OR_GREATER, NETCOREAPP2_0_OR_GREATER, NETCOREAPP1_1_OR_GREATER, NETCOREAPP1_0_OR_GREATER ANDROID, BROWSER, IOS, MACCATALYST, MACOS, TVOS, WINDOWS,
[OS][version] (por ejemplo IOS15_1),
[OS][version]_OR_GREATER (por ejemplo, IOS15_1_OR_GREATER)

Nota:

  • Los símbolos sin versión se definen independientemente de la versión de destino.
  • Los símbolos específicos de la versión solo se definen para la versión de destino.
  • Los símbolos <framework>_OR_GREATER se definen para la versión de destino y todas las versiones anteriores. Por ejemplo, si tiene como destino .NET Framework 2.0, se definen los símbolos siguientes: NET20, NET20_OR_GREATER, NET11_OR_GREATER y NET10_OR_GREATER.
  • Los símbolos NETSTANDARD<x>_<y>_OR_GREATER solo se definen para destinos de .NET Standard y no para destinos que implementan .NET Standard, como .NET Core y .NET Framework.
  • Son diferentes de los monikers de la plataforma de destino (TFM) que usa la propiedad MSBuildTargetFramework y NuGet.

Nota

En el caso de los proyectos que no son de estilo SDK, tendrá que configurar manualmente los símbolos de compilación condicional para las diferentes plataformas de destino en Visual Studio a través de las páginas de propiedades del proyecto.

Otros símbolos predefinidos incluyen las constantes DEBUG y TRACE. Puede invalidar los valores establecidos para el proyecto con #define. Por ejemplo, el símbolo DEBUG se establece automáticamente según las propiedades de configuración de compilación (modo de "depuración" o de "versión").

En el ejemplo siguiente se muestra cómo definir un símbolo MYTEST en un archivo y luego probar los valores de los símbolos MYTEST y DEBUG. La salida de este ejemplo depende de si el proyecto se ha compilado en modo de configuración Depuración o Versión.

#define MYTEST
using System;
public class MyClass
{
    static void Main()
    {
#if (DEBUG && !MYTEST)
        Console.WriteLine("DEBUG is defined");
#elif (!DEBUG && MYTEST)
        Console.WriteLine("MYTEST is defined");
#elif (DEBUG && MYTEST)
        Console.WriteLine("DEBUG and MYTEST are defined");
#else
        Console.WriteLine("DEBUG and MYTEST are not defined");
#endif
    }
}

En el ejemplo siguiente se muestra cómo probar distintos marcos de destino para que se puedan usar las API más recientes cuando sea posible:

public class MyClass
{
    static void Main()
    {
#if NET40
        WebClient _client = new WebClient();
#else
        HttpClient _client = new HttpClient();
#endif
    }
    //...
}

Definición de símbolos

Use las dos directivas de preprocesador siguientes para definir o anular la definición de símbolos para la compilación condicional:

  • #define: se define un símbolo.
  • #undef: se anula la definición de un símbolo.

Usa #define para definir un símbolo. Si usa el símbolo como expresión que se pasa a la directiva #if, la expresión se evaluará como true, como se muestra en el siguiente ejemplo:

#define VERBOSE

#if VERBOSE
   Console.WriteLine("Verbose output version");
#endif

Nota

La directiva #define no puede usarse para declarar valores constantes como suele hacerse en C y C++. En C#, las constantes se definen mejor como miembros estáticos de una clase o struct. Si tiene varias constantes de este tipo, puede considerar la posibilidad de crear una clase "Constants" independiente donde incluirlas.

Los símbolos se pueden usar para especificar condiciones de compilación. Puede comprobar el símbolo tanto con #if como con #elif. También se puede usar ConditionalAttribute para realizar una compilación condicional. Puede definir un símbolo, pero no asignar un valor a un símbolo. La directiva #define debe aparecer en el archivo antes de que use cualquier instrucción que tampoco sea una directiva del preprocesador. También se puede definir un símbolo con la opción del compilador DefineConstants. La definición de un símbolo se puede anular mediante #undef.

Definición de regiones

Puede definir regiones de código que se pueden contraer en un esquema mediante las dos directivas de preprocesador siguientes:

  • #region: se inicia una región.
  • #endregion: se finaliza una región.

#region permite especificar un bloque de código que se puede expandir o contraer cuando se usa la característica de esquematización del editor de código. En archivos de código más largos, es conveniente contraer u ocultar una o varias regiones para poder centrarse en la parte del archivo en la que se trabaja actualmente. En el ejemplo siguiente se muestra cómo definir una región:

#region MyClass definition
public class MyClass
{
    static void Main()
    {
    }
}
#endregion

Un bloque #region se debe terminar con una directiva #endregion. Un bloque #region no se puede superponer con un bloque #if. Pero, un bloque #region se puede anidar en un bloque #if y un bloque #if se puede anidar en un bloque #region.

Información de errores y advertencias

Indique al compilador que genere errores y advertencias del compilador definidos por el usuario, y controle la información de línea mediante las directivas siguientes:

  • #error: se genera un error del compilador con un mensaje especificado.
  • #warning: se genera una advertencia del compilador con un mensaje especificado.
  • #line: se cambia el número de línea impreso con mensajes del compilador.

#error permite generar un error CS1029 definido por el usuario desde una ubicación específica en el código. Por ejemplo:

#error Deprecated code in this method.

Nota

El compilador trata #error version de forma especial e informa de un error del compilador, CS8304, con un mensaje que contiene las versiones que se usan del compilador y del lenguaje.

#warning permite generar una advertencia del compilador CS1030 de nivel uno desde una ubicación específica en el código. Por ejemplo:

#warning Deprecated code in this method.

#line le permite modificar el número de línea del compilador y (opcionalmente) la salida del nombre de archivo de errores y advertencias.

En el siguiente ejemplo, se muestra cómo notificar dos advertencias asociadas con números de línea. La directiva #line 200 fuerza el número de línea siguiente para que sea 200 (aunque el valor predeterminado es 6) y hasta la siguiente directiva #line, el nombre de archivo se notificará como "Especial". La directiva #line default devuelve la numeración de líneas a su numeración predeterminada, que cuenta las líneas a las que la directiva anterior ha cambiado el número.

class MainClass
{
    static void Main()
    {
#line 200 "Special"
        int i;
        int j;
#line default
        char c;
        float f;
#line hidden // numbering not affected
        string s;
        double d;
    }
}

La compilación genera el siguiente resultado:

Special(200,13): warning CS0168: The variable 'i' is declared but never used
Special(201,13): warning CS0168: The variable 'j' is declared but never used
MainClass.cs(9,14): warning CS0168: The variable 'c' is declared but never used
MainClass.cs(10,15): warning CS0168: The variable 'f' is declared but never used
MainClass.cs(12,16): warning CS0168: The variable 's' is declared but never used
MainClass.cs(13,16): warning CS0168: The variable 'd' is declared but never used

La directiva #line podría usarse en un paso intermedio automatizado en el proceso de compilación. Por ejemplo, si se han eliminado las líneas del archivo de código fuente original, pero aún quiere que el compilador genere unos resultados en función de la numeración de líneas original en el archivo, puede quitar las líneas y, después, simular la numeración de líneas original con #line.

La directiva #line hidden oculta las líneas sucesivas del depurador, de forma que, cuando el desarrollador ejecuta paso a paso el código, cualquier línea entre #line hidden y la siguiente directiva #line (siempre que no sea otra directiva #line hidden) se depurará paso a paso por procedimientos. Esta opción también se puede usar para permitir que ASP.NET diferencie entre el código generado por el equipo y el definido por el usuario. Aunque ASP.NET es el consumidor principal de esta característica, es probable que la usen más generadores de código fuente.

Una directiva #line hidden no afecta a los nombres de archivo ni a los números de línea en el informe de errores. Es decir, si el compilador detecta un error en un bloque oculto, notificará el nombre de archivo y número de línea actuales del error.

La directiva #line filename especifica el nombre de archivo que quiere que aparezca en la salida del compilador. De forma predeterminada, se usa el nombre real del archivo de código fuente. El nombre de archivo debe estar entre comillas dobles ("") y debe ir precedido de un número de línea.

A partir de C# 10, se puede usar un formulario nuevo de la directiva #line:

#line (1, 1) - (5, 60) 10 "partial-class.cs"
/*34567*/int b = 0;

Los componentes de este formulario son los siguientes:

  • (1, 1): la línea de inicio y la columna del primer carácter de la línea que sigue a la directiva. En este ejemplo, la línea siguiente se notifica como línea 1, columna 1.
  • (5, 60): la línea final y la columna de la región marcada.
  • 10: desplazamiento de columna para que la directiva #line surta efecto. En este ejemplo, la décima columna se notifica como columna 1. Aquí es donde comienza la declaración int b = 0;. Este campo es opcional. Si se omite, la directiva surte efecto en la primera columna.
  • "partial-class.cs": el nombre del archivo de salida.

En el ejemplo anterior se generaría la advertencia siguiente:

partial-class.cs(1,5,1,6): warning CS0219: The variable 'b' is assigned but its value is never used

Después de la reasignación la variable, b, está en la primera línea, en el carácter seis, del archivo partial-class.csñ.

Los lenguajes específicos de dominio (DSL) suelen usar este formato para proporcionar una mejor asignación desde el archivo de origen hasta la salida de C# generada. El uso más común de esta directiva extendida #line es volver a asignar advertencias o errores que aparecen en un archivo generado al origen primario. Por ejemplo, considere esta página de Razor:

@page "/"
Time: @DateTime.NowAndThen

La propiedad DateTime.Now se ha escrito incorrectamente como DateTime.NowAndThen. El C# generado para este fragmento de código de Razor tiene el siguiente aspecto, en page.g.cs:

  _builder.Add("Time: ");
#line (2, 6) - (2, 27) 15 "page.razor"
  _builder.Add(DateTime.NowAndThen);

La salida del compilador para el fragmento de código anterior es:

page.razor(2, 2, 2, 27)error CS0117: 'DateTime' does not contain a definition for 'NowAndThen'

La línea 2, columna 6 de page.razor es donde comienza el texto @DateTime.NowAndThen. Esto se indica por (2, 6) en la directiva. Ese intervalo de @DateTime.NowAndThen termina en la línea 2, columna 27. Esto se indica por (2, 27) en la directiva. El texto para DateTime.NowAndThen comienza en la columna 15 de page.g.cs. Esto se indica por 15 en la directiva. Juntando todos los argumentos, el compilador informa del error en su ubicación en page.razor. El desarrollador puede navegar directamente al error en su código fuente, no en el código fuente generado.

Para ver más ejemplos de este formato, vea la especificación de la característica en la sección sobre ejemplos.

Pragmas

#pragma proporciona al compilador instrucciones especiales para la compilación del archivo en el que aparece. Las instrucciones deben ser compatibles con el compilador. Es decir, no puede usar #pragma para crear instrucciones de preprocesamiento personalizadas.

#pragma pragma-name pragma-arguments

Donde pragma-name es el nombre de una pragma reconocida y pragma-arguments es el argumento específico de la pragma.

#pragma warning

#pragma warning puede habilitar o deshabilitar determinadas advertencias.

#pragma warning disable warning-list
#pragma warning restore warning-list

Donde warning-list es una lista de números de advertencia separados por comas. El prefijo "CS" es opcional. Cuando no se especifica ningún número de advertencia, disable deshabilita todas las advertencias y restore habilita todas las advertencias.

Nota:

Para buscar los números de advertencia en Visual Studio, compile el proyecto y después busque los números de advertencia en la ventana Salida.

disable surte efecto a partir de la siguiente línea del archivo de código fuente. La advertencia se restaura en la línea que sigue a restore. Si el archivo no incluye restore, las advertencias se restauran a su estado predeterminado en la primera línea de los archivos posteriores de la misma compilación.

// pragma_warning.cs
using System;

#pragma warning disable 414, CS3021
[CLSCompliant(false)]
public class C
{
    int i = 1;
    static void Main()
    {
    }
}
#pragma warning restore CS3021
[CLSCompliant(false)]  // CS3021
public class D
{
    int i = 1;
    public static void F()
    {
    }
}

Suma de comprobación #pragma

Genera sumas de comprobación de archivos de código fuente para ayudar con la depuración de páginas ASP.NET.

#pragma checksum "filename" "{guid}" "checksum bytes"

Donde "filename" es el nombre del archivo en el que es necesario supervisar los cambios o las actualizaciones, "{guid}" es el identificador único global (GUID) para el algoritmo hash y "checksum_bytes" es la cadena de dígitos hexadecimales que representa los bytes de la suma de comprobación. Debe ser un número par de dígitos hexadecimales. Un número impar de dígitos genera una advertencia de tiempo de compilación y la directiva se ignora.

El depurador de Visual Studio usa una suma de comprobación para asegurarse de que siempre encuentra el código fuente correcto. El compilador calcula la suma de comprobación para un archivo de origen y, después, emite el resultado en el archivo de base de datos del programa (PDB). Después, el depurador usa el archivo PDB para comparar la suma de comprobación que calcula para el archivo de origen.

Esta solución no funciona con proyectos de ASP.NET, ya que la suma de comprobación calculada es para el archivo de código fuente generado, no para el archivo .aspx. Para solucionar este problema, #pragma checksum proporciona compatibilidad de la suma de comprobación con páginas ASP.NET.

Cuando se crea un proyecto de ASP.NET en Visual C#, el archivo de código fuente generado contiene una suma de comprobación del archivo .aspx desde el que se genera el código fuente. Después, el compilador escribe esta información en el archivo PDB.

Si el compilador no encuentra ninguna directiva #pragma checksum en el archivo, calcula la suma de comprobación y escribe el valor en el archivo PDB.

class TestClass
{
    static int Main()
    {
        #pragma checksum "file.cs" "{406EA660-64CF-4C82-B6F0-42D48172A799}" "ab007f1d23d9" // New checksum
    }
}