Diretivas de pré-processador do C#

Embora o compilador não tenha um pré-processador separado, as diretivas descritas nesta seção são processadas como se houvesse um. Você os usa para ajudar na compilação condicional. Ao contrário das diretivas de C e C++, não é possível usar essas diretivas para criar macros. Uma diretiva de pré-processador deve ser a única instrução em uma linha.

Contexto que permite valor nulo

A diretiva de pré-processador #nullable define o contexto de anotação anulável e o contexto de aviso anulável. Essa diretiva controla se as anotações anuláveis têm efeito e se os avisos de nulidade são dados. Cada contexto está desabilitado ou habilitado.

Ambos os contextos podem ser especificados no nível do projeto (fora do código-fonte C#). A diretiva #nullable controla os contextos de anotação e aviso e tem precedência sobre as configurações no nível do projeto. Uma diretiva define os contextos que controla até que outra diretiva a substitua ou até o final do arquivo de origem.

O efeito das diretivas é o seguinte:

  • #nullable disable: define a anotação anulável e os contextos de aviso como desabilitado.
  • #nullable enable: define os contextos de aviso e anotação anuláveis como habilitado.
  • #nullable restore: restaura a anotação anulável e os contextos de aviso para as configurações do projeto.
  • #nullable disable annotations: define o contexto de anotação anulada como desabilitado.
  • #nullable enable annotations: define o contexto de anotação anulável como habilitado.
  • #nullable restore annotations: restaura o contexto de anotação anulável para as configurações do projeto.
  • #nullable disable warnings: define o contexto de aviso anulável como desabilitado.
  • #nullable enable warnings: define o contexto de aviso anulável como habilitado.
  • #nullable restore warnings: restaura o contexto de aviso anulável nas configurações do projeto.

Compilação condicional

Você usa quatro diretivas de pré-processador para controlar a compilação condicional:

  • #if: abre uma compilação condicional, em que o código é compilado somente se o símbolo especificado for definido.
  • #elif: fecha a compilação condicional anterior e abre uma nova compilação condicional com base no caso do símbolo especificado ser definido.
  • #else: fecha a compilação condicional anterior e abre uma nova compilação condicional se o símbolo especificado anteriormente não estiver definido.
  • #endif: fecha a compilação condicional anterior.

O compilador C# compila o código entre a diretiva #if e a diretiva #endif somente se o símbolo especificado está definido ou não definido quando o operador ! não é usado. Ao contrário do C e do C++, não é possível atribuir um valor numérico a um símbolo. A instrução #if em C# é booliana e testa apenas quando o símbolo foi definido ou não. Por exemplo, o seguinte código é compilado quando DEBUG é definido:

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

O seguinte código é compilado quando MYTESTnão é definido:

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

Você pode usar os operadores == (igualdade) e != (desigualdade) para testar os bool valores true ou false. true significa que o símbolo foi definido. A instrução #if DEBUG tem o mesmo significado que #if (DEBUG == true). Você pode usar os && operadores (e), || (ou) e ! (não) para avaliar se vários símbolos foram definidos. Também é possível agrupar os símbolos e operadores com parênteses.

#if, juntamente com as diretivas #else, #elif, #endif, #define e #undef permite incluir ou excluir código com base na existência de um ou mais símbolos. A compilação condicional pode ser útil ao compilar código para uma compilação de depuração ou ao compilar para uma configuração específica.

Uma diretiva condicional que começa com #if deve ser terminada explicitamente com uma diretiva #endif. A diretiva #define permite definir um símbolo. Ao usar o símbolo como a expressão passada para a diretiva #if, a expressão será avaliada como true. Você também pode definir um símbolo com a opção do compilador DefineConstants. Você pode anular a definição um símbolo com #undef. O escopo de um símbolo criado com #define é o arquivo no qual ele foi definido. Um símbolo que você define com DefineConstants ou com #define não entra em conflito com uma variável de mesmo nome. Ou seja, um nome de variável não deve ser passado para uma diretiva de pré-processador, e um símbolo só pode ser avaliado por uma diretiva de pré-processador.

O #elif permite criar uma diretiva condicional composta. A expressão #elif será avaliada se nem as expressões de diretiva anterior #if nem outras anteriores, opcionais #elif são avaliadas como true. Se uma expressão #elif for avaliada como true, o compilador avaliará todo o código entre #elif e a próxima diretiva condicional. Por exemplo:

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

#else permite que você crie uma diretiva condicional composta, para que, caso nenhuma das expressões nas diretivas anteriores #if ou (opcional) #elifseja avaliada como true, o compilador avalie todo o código entre #else e a próxima #endif. #endif(#endif) deve ser a próxima diretiva de pré-processador após #else.

#endif especifica o final de uma diretiva condicional, que começou com a diretiva #if.

O sistema de compilação também está ciente dos símbolos de pré-processamento predefinidos que representam várias estruturas de destino em projetos no estilo SDK. Eles são úteis ao criar aplicativos que podem ter como destino mais de uma versão do .NET.

Frameworks de destino Símbolos Símbolos adicionais disponíveis no SDK do .NET 5+
.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+ (e .NET Core) NET, NET7_0, NET6_0, NET5_0, NETCOREAPP, NETCOREAPP3_1, NETCOREAPP3_0, NETCOREAPP2_2, NETCOREAPP2_1, NETCOREAPP2_0, NETCOREAPP1_1, NETCOREAPP1_0 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

Observação

  • Os símbolos sem versão são definidos independentemente da versão para a qual você está direcionando.
  • Os símbolos específicos à versão são definidos apenas para a versão que você está direcionando.
  • Os símbolos <framework>_OR_GREATER são definidos para a versão que você está direcionando e todas as versões anteriores. Por exemplo, se você estiver direcionando .NET Framework 2.0, os seguintes símbolos serão definidos: NET20, NET20_OR_GREATER, NET11_OR_GREATER e NET10_OR_GREATER.
  • Eles são diferentes dos TFMs (Moniker da Estrutura de Destino) usados pela propriedade MSBuild TargetFramework e pelo NuGet.

Observação

Para projetos tradicionais, não estilo SDK, você precisa configurar manualmente os símbolos de compilação condicional para as diferentes estruturas de destino no Visual Studio por meio das páginas de propriedades do projeto.

Outros símbolos predefinidos incluem as constantes DEBUG e TRACE. Para substituir os valores definidos no projeto, use a diretiva #define. Por exemplo, o símbolo DEBUG é definido automaticamente, de acordo com as propriedades de configuração do build (Modo de Depuração ou Modo de Versão).

O exemplo a seguir mostra como definir um símbolo MYTEST em um arquivo e testar os valores dos símbolos MYTEST e DEBUG. A saída deste exemplo depende da sua escolha ao compilar o projeto: se você optou pelo modo de configuração Debug ou Release.

#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
    }
}

O exemplo a seguir mostra como testar várias estruturas de destino para que você possa usar APIs mais recentes, quando possível:

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

Definindo símbolos

Use as duas diretivas de pré-processador seguintes para definir ou anular a definição de símbolos para compilação condicional:

  • #define: define um símbolo.
  • #undef: anula a definição de um símbolo.

Use #define para definir um símbolo. Quando você usa o símbolo como a expressão passada para a diretiva #if, a expressão será avaliada como true, conforme mostra o seguinte exemplo:

#define VERBOSE

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

Observação

A diretiva #define não pode ser usada para declarar valores constantes como normalmente é feito em C e C++. As constantes em C# são mais bem definidas como membros estáticos de uma classe ou struct. Se você tiver várias dessas constantes, considere criar uma classe "Constantes" separada para guardá-las.

Os símbolos podem ser usados para especificar condições para compilação. É possível testar o símbolo com #if ou #elif. Você também pode usar o ConditionalAttribute para executar uma compilação condicional. É possível definir um símbolo, mas não é possível atribuir um valor a um símbolo. A diretiva #define deve ser exibida no arquivo antes de usar as instruções que também não são diretivas de pré-processador. Você também pode definir um símbolo com a opção do compilador DefineConstants. Você pode anular a definição um símbolo com #undef.

Definindo regiões

Você pode definir regiões de código que podem ser recolhidas em uma estrutura de tópicos usando as duas diretivas de pré-processador seguintes:

  • #region: inicia uma região.
  • #endregion: encerra uma região.

#region permite que você especifique um bloco de código que pode ser expandido ou recolhido ao usar o recurso de estrutura de tópicos do editor de código. Em arquivos de código mais longos, é conveniente recolher ou ocultar uma ou mais regiões para que você possa se concentrar na parte do arquivo que está trabalhando no momento. O exemplo a seguir mostra como definir uma região:

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

Um bloco #region deve ser encerrado com a diretiva #endregion. Um #region bloco não pode se sobrepor a um bloco #if. No entanto, um bloco #region pode ser aninhado em um bloco #if e um bloco #if pode ser aninhado em um bloco #region.

Informações sobre erros e avisos

Você instrui o compilador a gerar erros e avisos do compilador definidos pelo usuário e controlar informações de linha usando as seguintes diretivas:

  • #error: gere um erro do compilador com uma mensagem especificada.
  • #warning: gere um aviso do compilador com uma mensagem específica.
  • #line: altere o número de linha impresso com mensagens do compilador.

#error permite gerar um erro definido pelo usuário CS1029 de um local específico em seu código. Por exemplo:

#error Deprecated code in this method.

Observação

O compilador trata #error version de maneira especial e relata um erro do compilador, CS8304, com uma mensagem que contém o compilador usado e as versões de idioma.

#warning permite gerar um aviso do compilador CS1030 de nível um de um local específico no código. Por exemplo:

#warning Deprecated code in this method.

O #line permite modificar o número de linha do compilador e (opcionalmente) a saída do nome de arquivo para erros e avisos.

O exemplo a seguir mostra como relatar dois avisos associados aos números de linha. A diretiva #line 200 força o próximo número de linha a ser 200 (embora o padrão seja #6) e, até a próxima diretiva #line, o nome de arquivo será relatado como "Special". A diretiva #line default retorna a numeração de linhas à sua numeração padrão, que conta as linhas que foram renumeradas pela diretiva anterior.

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;
    }
}

A compilação produz a saída a seguir:

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

A diretiva #line pode ser usada em uma etapa intermediária e automatizada no processo de build. Por exemplo, se linhas fossem removidas do arquivo de código-fonte original, mas você ainda deseja que o compilador gere a saída com base na numeração de linha original no arquivo, seria possível remover as linhas e, em seguida, simular a numeração de linha original com #line.

A diretiva #line hidden oculta as linhas sucessivas do depurador, de modo que, quando o desenvolvedor percorrer o código, quaisquer linhas entre um #line hidden e a próxima diretiva #line (supondo que não seja outra diretiva #line hidden) serão puladas. Essa opção também pode ser usada para permitir que o ASP.NET diferencie entre o código gerado pelo computador e definido pelo usuário. Embora o ASP.NET seja o principal consumidor desse recurso, é provável que mais geradores de origem façam uso dele.

A diretiva #line hidden não afeta os nomes de arquivo ou números de linha nos relatórios de erro. Ou seja, se o compilador encontrar um erro em um bloco oculto, o compilador relatará o nome do arquivo atual e o número de linha do erro.

A diretiva #line filename especifica o nome de arquivo que você deseja que seja exibido na saída do compilador. Por padrão, é usado o nome real do arquivo de código-fonte. O nome de arquivo deve estar entre aspas duplas ("") e deve ser precedido por um número de linha.

Do C# 10 em diante, você pode usar uma nova forma da diretiva #line:

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

Os componentes dessa noda forma são:

  • (1, 1): a linha de início e a coluna do primeiro caractere na linha que segue a diretiva. Neste exemplo, a próxima linha seria relatada como linha 1, coluna 1.
  • (5, 60): a linha de extremidade e a coluna da região marcada.
  • 10: o deslocamento da coluna para que a diretiva #line entre em vigor. Neste exemplo, a 10ª coluna seria relatada como coluna um. É aí que começa a declaração int b = 0;. Esse campo é opcional. Se omitida, a diretiva vai entrar em vigor na primeira coluna.
  • "partial-class.g.cs": nome do arquivo de saída.

O exemplo anterior geraria o seguinte aviso:

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

Depois de remapear, a variável b está na primeira linha, no caractere seis.

As DSLs (linguagens específicas do domínio) normalmente usam esse formato para fornecer um mapeamento melhor do arquivo de origem para a saída gerada em C#. Para ver mais exemplos desse formato, confira a especificação de recurso na seção sobre exemplos.

Pragmas

O #pragma fornece ao compilador instruções especiais para a compilação do arquivo no qual ele é exibido. O compilador deve dar suporte às instruções. Em outras palavras, não é possível usar #pragma para criar instruções personalizadas de pré-processamento.

#pragma pragma-name pragma-arguments

Em que pragma-name é o nome de um pragma reconhecido e pragma-arguments são os argumentos específicos do pragma.

#pragma warning

O #pragma warning pode habilitar ou desabilitar determinados avisos.

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

Em que warning-list é uma lista de números de aviso separada por vírgulas. O prefixo "CS" é opcional. Quando não houver números de aviso especificados, o disable desabilita todos os avisos e o restore habilita todos os avisos.

Observação

Para localizar números de aviso no Visual Studio, compile o projeto e, em seguida, procure os números de aviso na janela de Saída.

disable entra em vigor desde a próxima linha do arquivo de origem. O aviso é restaurado na linha que segue restore. Se não houver nenhum restore no arquivo, os avisos serão restaurados para o estado padrão na primeira linha de qualquer arquivo posterior na mesma compilação.

// 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()
    {
    }
}

#pragma checksum

Gera somas de verificação para os arquivos de origem para ajudar na depuração de páginas do ASP.NET.

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

Em que "filename" é o nome do arquivo que requer monitoramento para alterações ou atualizações, "{guid}" é o GUID (Globally Unique Identifier) para o algoritmo hash e "checksum_bytes" é a cadeia de caracteres de dígitos hexadecimal que representam os bytes da soma de verificação. Deve ser um número par de dígitos hexadecimais. Um número ímpar de dígitos resulta em um aviso em tempo de compilação e a diretiva é ignorada.

O depurador do Visual Studio usa uma soma de verificação para garantir sempre a localização da fonte correta. O compilador calcula a soma de verificação para um arquivo de origem e, em seguida, emite a saída no arquivo PDB (banco de dados do programa). Em seguida, o depurador usa o PDB para comparar com a soma de verificação que ele calcula para o arquivo de origem.

Essa solução não funciona para projetos do ASP.NET, porque é a soma de verificação calculada para o arquivo de origem gerado e não para o arquivo .aspx. Para resolver esse problema, a #pragma checksum fornece suporte à soma de verificação para páginas do ASP.NET.

Quando você cria um projeto do ASP.NET em Visual C#, o arquivo de origem gerado contém uma soma de verificação para o arquivo .aspx, do qual a fonte é gerada. Então, o compilador grava essas informações no arquivo PDB.

Se o compilador não encontrar uma diretiva #pragma checksum no arquivo, ele calcula a soma de verificação e grava o valor no arquivo PDB.

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