Поделиться через


Директивы препроцессора C#

Хотя у компилятора нет отдельного препроцессора, директивы, описанные в этом разделе, обрабатываются так, как если бы он был. Они используются в условной компиляции. В отличие от директив C и C++ вы не можете использовать их для создания макросов. Директива препроцессора должна быть единственной инструкцией в строке.

Контекст, допускающий значение NULL

Директива препроцессора #nullable задает заметки и флаги предупреждения в контексте, допускающего значение NULL,. Эта директива определяет, действуют ли заметки, допускающие значение NULL, и могут ли быть заданы предупреждения о допустимости значений NULL. Каждый из флагов отключен или включен.

Оба контекста можно указать на уровне проекта (за пределами исходного кода C#), добавив Nullable элемент в PropertyGroup элемент. Директива #nullable управляет флагами заметки и предупреждениями и имеет приоритет над параметрами уровня проекта. Директива задает флаг, которым она управляет, пока другая директива не переопределит его или до конца файла источника.

Ниже приведены результаты использования директив:

  • #nullable disable. Задает контекст, допускающий значение NULL, отключен.
  • #nullable enable. Устанавливает допускающий значение NULL контекст включенным.
  • #nullable restore: восстанавливает контекст, допускающий значение NULL, в параметры проекта.
  • #nullable disable annotations. Устанавливает флаг аннотаций в контексте, допускающем значение NULL, в состояние отключено.
  • #nullable enable annotations. Задает флаг заметок в контексте, допускаемом значение NULL, включено.
  • #nullable restore annotations: восстанавливает флаг заметок в контексте, допускаемом значение NULL, в параметры проекта.
  • #nullable disable warnings: задает флаг предупреждения в контексте, допускающего значение NULL, отключенным.
  • #nullable enable warnings: Задает флаг предупреждения в контексте, допускающем значение NULL, включено.
  • #nullable restore warnings: восстанавливает флаг предупреждения в nullable контексте в настройки проекта.

Условная компиляция

Для управления условной компиляцией используются четыре директивы препроцессора.

  • #if: открывает условную компиляцию, где код компилируется, только если определен указанный символ.
  • #elif: закрывает предыдущую условную компиляцию и открывает новую на основе того, определен ли указанный символ.
  • #else: закрывает предыдущую условную компиляцию и открывает новую, если указанный символ не определен.
  • #endif: закрывает предыдущую условную компиляцию.

Система сборки также учитывает символы препроцессора, представляющие целевые платформы в проектах в стиле SDK. Они полезны при создании приложений, предназначенных для нескольких версий .NET.

Требуемые версии .NET Framework Символы Дополнительные символы
(доступно в пакетах SDK для .NET 5 и более поздних версий)
Символы платформы (доступны только
при указании TFM для конкретной ОС)
.NET Framework NETFRAMEWORK NET481, NET48NET472NET471NET47NET462NET461NET46NET452NET451NET45NET40NET35NET20 NET48_OR_GREATER, NET472_OR_GREATER, NET471_OR_GREATERNET47_OR_GREATERNET462_OR_GREATERNET461_OR_GREATERNET46_OR_GREATERNET452_OR_GREATERNET451_OR_GREATERNET45_OR_GREATERNET40_OR_GREATERNET35_OR_GREATERNET20_OR_GREATER
.NET Standard NETSTANDARD, NETSTANDARD2_1, NETSTANDARD2_0NETSTANDARD1_6NETSTANDARD1_5NETSTANDARD1_4NETSTANDARD1_3NETSTANDARD1_2NETSTANDARD1_1NETSTANDARD1_0 NETSTANDARD2_1_OR_GREATER, NETSTANDARD2_0_OR_GREATERNETSTANDARD1_6_OR_GREATERNETSTANDARD1_5_OR_GREATERNETSTANDARD1_4_OR_GREATERNETSTANDARD1_3_OR_GREATERNETSTANDARD1_2_OR_GREATERNETSTANDARD1_1_OR_GREATERNETSTANDARD1_0_OR_GREATER
.NET 5+ (и .NET Core) NET, NET9_0NET8_0NET7_0NET6_0NET5_0NETCOREAPPNETCOREAPP3_1NETCOREAPP3_0NETCOREAPP2_2NETCOREAPP2_1NETCOREAPP2_0NETCOREAPP1_1NETCOREAPP1_0 NET8_0_OR_GREATER, NET7_0_OR_GREATER, NET6_0_OR_GREATERNET5_0_OR_GREATERNETCOREAPP3_1_OR_GREATERNETCOREAPP3_0_OR_GREATERNETCOREAPP2_2_OR_GREATERNETCOREAPP2_1_OR_GREATERNETCOREAPP2_0_OR_GREATERNETCOREAPP1_1_OR_GREATERNETCOREAPP1_0_OR_GREATER ANDROID, BROWSER, IOSMACCATALYSTMACOSTVOSWINDOWS
[OS][version] (например IOS15_1),
[OS][version]_OR_GREATER (например IOS15_1_OR_GREATER)

Примечание.

  • Символы без привязки к версии определены независимо от версии, которую вы хотите использовать в качестве целевой.
  • Символы для определенных версий определены только для тех версий, которые вы хотите использовать в качестве целевых.
  • Символы <framework>_OR_GREATER определены для версии, которую вы хотите использовать в качестве целевой, и всех более ранних версий. Например, если вы выбрали .NET Framework 2.0, определяются следующие символы: NET20, NET20_OR_GREATER, NET11_OR_GREATER и NET10_OR_GREATER.
  • NETSTANDARD<x>_<y>_OR_GREATER Символы определяются только для целевых объектов .NET Standard, а не для целевых объектов, реализующих .NET Standard, таких как .NET Core и платформа .NET Framework.
  • Они отличаются от моникеров целевой платформы (TFM), используемых TargetFramework MSBuild и NuGet.

Примечание.

Для традиционных проектов, в которых не используется пакет SDK, необходимо вручную настроить символы условной компиляции для различных целевых платформ в Visual Studio с помощью страниц свойств проекта.

Другие предопределенные символы включают константы DEBUG и TRACE. Вы можете переопределить значения для проектов с помощью #define. Например, символ DEBUG автоматически устанавливается в зависимости от свойств конфигурации сборки (в режиме отладки или выпуска).

Компилятор C# компилирует код между #if директивой и #endif директивой, только если определен указанный символ или не определен, если ! оператор не используется. В отличие от C и C++, числовое значение символу нельзя назначить. Оператор #if в C# является логическим и только проверяет, определён ли символ. Например, следующий код компилируется при DEBUG определении:

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

Следующий код компилируется, если MYTESTне определен:

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

Вы можете использовать операторы == (равенство) и != (неравенство) для проверки значений booltrue или false. Значение true означает, что символ определен. Инструкция #if DEBUG имеет то же значение, что и #if (DEBUG == true). Вы можете использовать && (и), || (или)и операторы ! (не) для оценки того, определены ли несколько символов. Можно также группировать символы и операторы при помощи скобок.

В следующем примере показана сложная директива, которая позволяет коду использовать новые функции .NET при сохранении обратной совместимости. Например, представьте, что в коде используется пакет NuGet, но пакет поддерживает только .NET 6 и выше, а также .NET Standard 2.0 и выше:

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

#if, а также #elseдирективы , #elif#endif#defineи #undef директивы, позволяют включать или исключать код на основе существования одного или нескольких символов. Условная компиляция может быть полезной при компиляции кода для отладочной сборки или для определенной конфигурации.

Директива #elif позволяет создать составную условную директиву. Выражение #elif вычисляется, если ни одно из предыдущих выражений директив #if, ни какое-либо из предыдущих, но необязательных выражений директив #elif не оцениваются как true. Если после вычисления выражения #elif возвращается значение true, компилятор вычисляет весь код между директивой #elif и следующей условной директивой. Например:

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

С помощью директивы #else можно создать составную условную директиву со следующим поведением: если ни одно из выражений в предшествующих директивах #if или (необязательно) #elif не принимает значение true, компилятор вычисляет код между директивой #else и последующей директивой #endif. Директива #endif обязательно указывается в качестве следующей директивы препроцессора после #else.

#endif указывает на конец условной директивы, начало которой было задано с помощью директивы #if.

В следующем примере показано, как определить символ MYTEST в файле и затем протестировать значения символов MYTEST и DEBUG. Выходные данные этого примера зависят от режима конфигурации, в котором создан проект (Отладка или Выпуск).

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

В следующем примере показано, как тестировать разные целевые платформы для использования более новых интерфейсов API, когда это возможно:

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

Определение символов

Используйте следующие две директивы препроцессора, чтобы определить или отменить определение символов для условной компиляции.

  • #define: определение символа.
  • #undef: подчеркивание символа.

#define позволяет определить символ. При использовании символа в качестве выражения, переданного директиве #if, выражение оценивается как true, как показано в следующем примере:

#define VERBOSE

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

Примечание.

В C# примитивные константы должны быть определены с помощью ключевого const слова. Объявление const создает static элемент, который нельзя изменить во время выполнения. Директива #define не может использоваться для объявления константных значений, как правило, в C и C++. При наличии нескольких констант имеет смысл создать для них отдельный класс "Constants".

Символы можно использовать для указания условий компиляции. Вы можете протестировать символ с помощью одного #if или.#elif Для условной компиляции также можно использовать ConditionalAttribute. Вы можете определить символ, но не можете присвоить символу значение. Директива #define должна находиться в файле перед использованием любых инструкций, которые также не являются директивами препроцессора. Символ также можно определить с помощью параметра компилятора DefineConstants. Символ можно отменить с #undefпомощью.

Определение областей

Вы можете определить области кода, которые можно свернуть в структуру, используя следующие две директивы препроцессора.

  • #region: начало области.
  • #endregion: конец области.

Директива #region позволяет указать блок кода, который можно разворачивать и сворачивать с помощью функции структурирования в редакторе кода. В больших файлах кода удобно сворачивать или скрывать одну область или несколько, чтобы не отвлекаться от той части файла, над которой в настоящее время идет работа. В следующем примере показано, как определить область:

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

В конце блока #region должна присутствовать директива #endregion. Блок #region не может накладываться на блок #if. Однако блок #region можно вложить в блок #if, а блок #if — в блок #region.

Сведения об ошибках и предупреждениях

Вы указываете компилятору создавать определенные пользователем ошибки и предупреждения компилятора, а также управлять сведениями о строках с помощью следующих директив.

  • #error: создание ошибки компилятора с указанным сообщением.
  • #warning: создание предупреждения компилятора с конкретным сообщением.
  • #line: изменение номера строки, выводимого с сообщениями компилятора.

#error позволяет создать определяемую пользователем ошибку CS1029 из определенного места в коде. Например:

#error Deprecated code in this method.

Примечание.

Компилятор обрабатывает #error version особым образом и сообщает об ошибке компилятора CS8304 с сообщением, содержащим используемые версии компилятора и языка.

#warning позволяет создать предупреждение компилятора CS1030 первого уровня из определенного места в коде. Например:

#warning Deprecated code in this method.

Директива #line позволяет изменять номер строки компилятора и при необходимости имя файла, в который будут выводиться ошибки и предупреждения.

В следующем примере показано, как включить в отчет два предупреждения, связанные с номерами строк. Директива #line 200 заставляет номер следующей строки иметь значение 200 (хотя значение по умолчанию — #6), а до следующей директивы #line имя файла будет указано как "Специальный". Директива #line default возвращает нумерацию строк в нумерацию по умолчанию, которая подсчитывает строки, переумерованные предыдущей директивой.

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

В результате компиляции формируются следующие результаты:

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

Директива #line может использоваться на автоматизированном промежуточном этапе процесса построения. Например, если строки были удалены из первоначального файла с исходным кодом, но вам по-прежнему требуется создавать выходные файлы компилятора на основе изначальной нумерации строк в файле, можно удалить строки и затем смоделировать их первичную нумерацию с помощью директивы #line.

Директива #line hidden скрывает последующие строки для отладчика. В этом случае при пошаговой проверке кода разработчиком все строки между #line hidden и следующей директивой #line (кроме случаев, когда это также директива #line hidden) будут пропущены. Этот параметр также можно использовать для того, чтобы дать ASP.NET возможность различать определяемый пользователем и создаваемый компьютером код. Хотя ASP.NET является основным потребителем этой функции, скорее всего, более исходные генераторы используют его.

Директива #line hidden не влияет на имена файлов и номера строк в отчетах об ошибках. То есть, если компилятор находит ошибку в скрытом блоке, компилятор сообщает текущее имя файла и номер строки ошибки.

Директива #line filename задает имя файла, которое будет отображаться в выходных данных компилятора. По умолчанию используется фактическое имя файла с исходным кодом. Имя файла должно быть в двойных кавычках ("") и должно соответствовать номеру строки.

Вы можете использовать новую форму директивы #line:

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

Компоненты этой формы:

  • (1, 1): начальная строка и столбец первого символа в строке, следующей за директивой. В этом примере следующая строка будет отображаться как строка 1, столбец 1.
  • (5, 60): конечная строка и столбец для помеченной области.
  • 10: смещение столбца, чтобы директива #line вступила в силу. В этом примере в качестве столбца 1 будет отображаться десятый столбец. Объявление int b = 0; начинается с этого столбца. Это поле необязательно. Если этот параметр опущен, директива вступает в силу в первом столбце.
  • "partial-class.cs": имя выходного файла.

В предыдущем примере будет создано следующее предупреждение:

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

После повторного сопоставления переменная b, находится в первой строке в символе шесть из файла partial-class.cs.

Предметно-ориентированные языки (DSL) обычно используют этот формат, чтобы обеспечить более эффективное сопоставление исходного файла с созданными выходными данными C#. Наиболее распространенное использование этой расширенной директивы #line заключается в ремаппинге предупреждений или ошибок, которые появляются в созданном файле, в исходный код. Например, рассмотрим эту страницу razor:

@page "/"
Time: @DateTime.NowAndThen

Свойство DateTime.Now было введено неправильно DateTime.NowAndThen. Созданный C# для этого фрагмента razor выглядит следующим образом:page.g.cs

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

Выходные данные компилятора для предыдущего фрагмента кода:

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

Строка 2, столбец 6 в page.razor — это место, где начинается текст @DateTime.NowAndThen, отмеченный как (2, 6) в директиве. Этот диапазон @DateTime.NowAndThen заканчивается в строке 2, столбце 27, отмеченном (2, 27) в директиве. Текст для DateTime.NowAndThen начинается в столбце 15 таблицы page.g.cs, отмеченном как 15 в директиве. Компилятор сообщает об ошибке в его расположении в page.razor. Разработчик может перейти непосредственно к ошибке в исходном коде, а не к созданному источнику.

Дополнительные примеры этого формата см. в разделе примеров в спецификации функции.

Директивы pragma

Директива #pragma предоставляет компилятору специальные инструкции для компиляции файла, в котором она появляется. Компилятор должен поддерживать используемые прагмы. Другими словами, директиву #pragma невозможно использовать для создания настраиваемых инструкций предварительной обработки.

  • #pragma warning: включение или отключение предупреждений.
  • #pragma checksum: создание контрольной суммы.
#pragma pragma-name pragma-arguments

pragma-name — имя распознанной прагмы, а pragma-arguments — аргументы, относящиеся к прагме.

#pragma warning

#pragma warning может включать или отключать определенные предупреждения. #pragma warning disable format и #pragma warning enable format управляют форматированием блоков кода в Visual Studio.

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

Где warning-list — это разделенный запятыми список номеров предупреждений, например 414, CS3021. Префикс CS является необязательным. Если номера предупреждений не указаны, disable отключает все предупреждения, а restore включает все предупреждения.

Примечание.

Чтобы найти номера предупреждений в Visual Studio, выполните сборку проекта, а затем поиск номеров предупреждений в окне Вывод.

Параметр disable вступает в силу, начиная со следующей строки исходного файла. Предупреждение восстанавливается в строке после restore. Если в файле нет restore, предупреждения восстанавливаются до их состояния по умолчанию в первой строке всех последующих файлов в той же компиляции.

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

Другая форма warning pragma отключает или восстанавливает команды форматирования Visual Studio в блоках кода:

#pragma warning disable format
#pragma warning restore format

Команды форматирования Visual Studio не изменяют текст в блоках кода, где disable format действует. Команды форматирования, такие как CTRL+K, CTRL+D, не изменяют эти области кода. Эта pragma позволяет точно управлять визуальным представлением кода.

#pragma checksum

Создает контрольные суммы для исходных файлов, чтобы помочь с отладкой страниц ASP.NET.

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

"filename" — это имя файла, для которого требуется наблюдение за изменениями или обновлениями, "{guid}" — глобальный уникальный идентификатор (GUID) для хэш-алгоритма, а "checksum_bytes" — строка шестнадцатеричных цифр, представляющих байты контрольной суммы. Должно быть четным числом шестнадцатеричных цифр. Нечетное число цифр приведет к выводу предупреждения во время компиляции, и директива будет пропущена.

Отладчик Visual Studio использует контрольную сумму, чтобы подтвердить нахождение правильного источника. Компилятор вычисляет контрольную сумму для исходного файла, а затем передает результат в файл базы данных (PDB) программы. Отладчик затем использует PDB-файл для сравнения с контрольной суммой, вычисленной им для исходного файла.

Это решение не работает для проектов ASP.NET, так как рассчитанная контрольная сумма относится к созданному исходному файлу, а не файлу ASPX. Чтобы решить эту проблему, #pragma checksum предоставляет поддержку контрольных сумм для страниц ASP.NET.

При создании проекта ASP.NET в Visual C# созданный исходный файл содержит контрольную сумму для ASPX-файла, из которого создается источник. Затем компилятор записывает эти данные в PDB-файл.

Если компилятор не обнаруживает директиву #pragma checksum в файле, он вычисляет контрольную сумму и записывает значение в PDB-файл.

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