В чём разница между условной компиляцией и атрибутом Conditional?
Пользователь: почему эта программа отказывается компилироваться в релизном билде?
class Program
{
#if DEBUG
static int testCounter = 0;
#endif
static void Main(string[] args)
{
SomeTestMethod(testCounter++);
}
[Conditional("DEBUG")]
static void SomeTestMethod(int t) { }
}
Эрик: Это не получается скомпилировать при окончательном построении потому, что не удаётся найти testCounter при вызове SomeTestMethod.
Пользователь: Но этот вызов всё равно будет выброшен, так почему это имеет значение? Явно есть какая-то разница между устранением кода при помощи условной компиляции и при помощи условного атрибута, но в чём эта разница?
Эрик: Тебе уже известен ответ на твой вопрос, просто ты об этом еще не знаешь. Давай поиграем в Сократа; я верну вопрос тебе – как это работает? Откуда компилятор узнает, что нужно устранить вызов метода?
Пользователь: Потому, что вызываемый метод помечен атрибутом Conditional.
Эрик: Это знаешь ты. Но откуда компилятор знает, что вызываемый метод помечен атрибутом Conditional?
Пользователь: Потому, что разрешение перегрузок выбрало тот метод. Если бы это был метод из другой сборки, то в метаданных, ассоциированных с методом, был бы этот атрибут. Если это метод в исходном коде, то компилятор знает про атрибут потому, что может проанализировать исходный код и выяснить значение атрибута.
Эрик: Ясно. То есть, фундаментально, основная работа делается разрешением перегрузок. А откуда разрешение перегрузок знает, что нужно выбрать именно тот метод? Предположим гипотетически, что у нас там был и другой метод с тем же именем и другими параметрами.
Пользователь: Разрешение перегрузок работает путём изучения аргументов вызова и сравнения их с типами параметров каждого метода-кандидата, и затем выбора единственного лучшего соответствия среди всех кандидатов.
Эрик: Вот и оно. Таким образом, аргументы должны быть полностью определены в точке вызова, даже если вызов будет впоследствии устранён. Фактически, вызов невозможно устранить, не имея в наличии аргументов! Но в релизном билде, тип аргумента невозможно определить, потому что его объявление было выброшено.
Так что теперь вы видите, что реальная разница между этими двумя техниками устранения нежелательного кода а том, что делает компилятор в момент устранения. На высоком уровне, компилятор обрабатывает текст так. Сначала он «лексит» файл. То есть, разбивает строку на «лексемы» - последовательности букв, цифр и символов, которые имеют смысл для компилятора. Затем эти лексемы «разбираются», чтобы проверить соответствие программы грамматике C#. Затем разобранное состояние анализируется для определения семантической информации о нём; каков тип каждого выражения и всё такое. И, наконец, компилятор выплёвывает код, который эту семантику реализует.
Действие директивы условной компиляции происходит во время лексического разбора; всё, что попало внутрь устранённого блока #if трактуется лексическим анализатором как комментарий. Как будто вы просто удалили всё содержимое блока и заменили пробелом. Но устранение вызовов в зависимости от условных атрибутов происходит во время сематического анализа; всё необходимое для проведения этого семантического анализа должно быть в наличии.
Пользователь: Потрясающе. Какие разделы спецификации C# определяют это поведение?
Эрик: Спецификация начинается с удобного «содержания», которое очень помогает в ответах на такие вопросы. Содержание утверждает, что секция 2.5.1 описывает «Символы условной компиляции», а секция 17.4.1. описывает «атрибут Conditional».
Пользователь: Обалдеть.