Директивы препроцессора C#
Хотя у компилятора нет отдельного препроцессора, директивы, описанные в этом разделе, обрабатываются так, как если бы он был. Они используются в условной компиляции. В отличие от директив C и C++ вы не можете использовать их для создания макросов. Директива препроцессора должна быть единственной инструкцией в строке.
Контекст, допускающий значение NULL
Директива препроцессора #nullable
устанавливает контекст с заметками о допустимости значений NULL и контекст с предупреждениями о допустимости значений 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
: восстанавливает контекст предупреждения, допускающего значение NULL, в параметры проекта.
Условная компиляция
Для управления условной компиляцией используются четыре директивы препроцессора.
#if
: открывает условную компиляцию, где код компилируется, только если определен указанный символ.#elif
: закрывает предыдущую условную компиляцию и открывает новую на основе того, определен ли указанный символ.#else
: закрывает предыдущую условную компиляцию и открывает новую, если указанный символ не определен.#endif
: закрывает предыдущую условную компиляцию.
Компилятор 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
Вы можете использовать операторы ==
(равенство) и !=
(неравенство) для проверки значений bool
true
или 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
директивы , #endif
#elif
#define
и #undef
директивы, позволяют включать или исключать код на основе существования одного или нескольких символов. Условная компиляция может быть полезной при компиляции кода для отладочной сборки или для определенной конфигурации.
Условные директивы, начинающиеся с директивы #if
, должны явным образом завершаться директивой #endif
. #define
позволяет определить символ, чтобы выражение, в качестве которого этот символ передается в директиву #if
, при вычислении давало значение true
. Символ также можно определить с помощью параметра компилятора DefineConstants. Символ можно отменить с #undef
помощью. Символ, создаваемый с помощью #define
, будет определен в пределах того файл, в котором он определен. Символ, определенный с помощью DefineConstants или #define
, не конфликтует с одноименной переменной. Соответственно, имя переменной не должно передаваться директиве препроцессора, а символ может использоваться только в директиве препроцессора.
Директива #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
.
Система сборки также учитывает символы препроцессора, представляющие целевые платформы в проектах в стиле SDK. Они полезны при создании приложений, предназначенных для нескольких версий .NET.
Требуемые версии .NET Framework | Символы | Дополнительные символы (доступно в пакетах SDK для .NET 5 и более поздних версий) |
Символы платформы (доступны только при указании TFM для конкретной ОС) |
---|---|---|---|
.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+ (и .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] (например 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), используемых свойством MSBuild
TargetFramework
и NuGet.
Примечание.
Для традиционных проектов, в которых не используется пакет SDK, необходимо вручную настроить символы условной компиляции для различных целевых платформ в Visual Studio с помощью страниц свойств проекта.
Другие предопределенные символы включают константы DEBUG
и TRACE
. Вы можете переопределить значения для проектов с помощью #define
. Например, символ DEBUG автоматически устанавливается в зависимости от свойств конфигурации сборки (в режиме отладки или выпуска).
В следующем примере показано, как определить символ 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
в отчете будет указываться имя файла Special. Директива #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
задает имя файла, которое будет отображаться в выходных данных компилятора. По умолчанию используется фактическое имя файла с исходным кодом. Имя файла должно заключаться в двойные кавычки (" "). Перед ним должен указываться номер строки.
Начиная с C# 10 можно использовать новую форму директивы #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 warning-list
#pragma warning restore warning-list
warning-list
— список номеров предупреждений с разделителем-запятой. Префикс 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()
{
}
}
#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
}
}