Директивы препроцессора 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 , 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 , NET9_0 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), используемых
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
Вы можете использовать операторы ==
(равенство) и !=
(неравенство) для проверки значений 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
директивы , #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
}
}