В чём разница между условной компиляцией и атрибутом 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».

Пользователь: Обалдеть.