Почему компилятор С# удаляет цепочку вызовов методов, когда последний является условным?

Рассмотрим следующие классы:

public class A {
    public B GetB() {
        Console.WriteLine("GetB");
        return new B();
    }
}

public class B {
    [System.Diagnostics.Conditional("DEBUG")]
    public void Hello() {
        Console.WriteLine("Hello");
    }
}

Теперь, если мы будем называть методы таким образом:

var a = new A();
var b = a.GetB();
b.Hello();

В релиз-сборке (т. DEBUG флаг DEBUG), мы увидели бы только GetB напечатанный на консоли, так как вызов Hello() будет отсутствовать компилятором. В отладочной сборке появятся оба отпечатка.

Теперь позвольте цепочке вызов метода:

a.GetB().Hello();

Поведение в сборке отладки не изменилось; однако мы получаем другой результат, если флаг не установлен: оба вызова опускаются и на консоли не отображаются отпечатки. Быстрый взгляд на IL показывает, что вся строка не была скомпилирована.

Согласно последнему стандарту ECMA для С# (ECMA-334, то есть С# 5.0), ожидаемое поведение, когда атрибут Conditional помещается в этот метод, выглядит следующим образом (основное внимание):

Вызов условного метода включается, если один или несколько ассоциированных условных символов компиляции определены в точке вызова, иначе вызов будет опущен. (§22.5.3)

Это, по-видимому, не указывает на то, что всю цепочку следует игнорировать, поэтому мой вопрос. Тем не менее, спецификация С# 6.0 от Microsoft предлагает немного более подробную информацию:

Если символ определен, вызов включается; в противном случае вызов (включая оценку получателя и параметры вызова) опускается.

Тот факт, что параметры вызова не оцениваются, хорошо документирован, поскольку одна из причин, по которым люди используют эту функцию, а не директивы #if в теле функции. Тем не менее, часть о "оценке приемника" является новой - я не могу найти ее в другом месте, и, похоже, это объясняет описанное выше поведение.

В свете этого, мой вопрос заключается в следующем: какое обоснование компилятора С# не оценивает a.GetB() в этой ситуации? Должно ли оно вести себя по-разному на основе того, хранится ли получатель условного вызова во временной переменной или нет?

Ответ 1

Я немного поработал и нашел, что спецификация языка С# 5.0 на самом деле уже содержит вашу вторую цитату в разделе 17.4.2. Условный атрибут на стр. 424.

Ответ Марка Гравелла уже показывает, что это поведение предназначено и что оно означает на практике. Вы также спрашивали об обосновании этого, но, похоже, недовольны упоминанием Марка об устранении накладных расходов.

Может быть, вам интересно, почему это считается накладными, которые можно удалить?

a.GetB().Hello(); не вызванный вообще в вашем сценарии с пропущенным значением Hello(), может показаться нечетным по номиналу.

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

Цепочка метода возможна только в том случае, если каждый предыдущий метод имеет возвращаемое значение. Это имеет смысл, когда вы хотите что-то сделать с этими значениями, то есть a.GetFoos().MakeBars().AnnounceBars();

Если у вас есть функция, которая только что-то делает, не возвращая значения, вы не можете связать что-то позади нее, но можете поместить ее в конец цепочки методов, как в случае с вашим условным методом, поскольку она должна иметь тип возвращаемого типа void.

Также обратите внимание, что результат предыдущих вызовов метода отбрасывается, поэтому в вашем примере a.GetB().Hello(); ваш результат из GetB() не имеет причины жить после выполнения этого оператора. В принципе, вы подразумеваете, что вам нужен результат GetB() только для использования Hello().

Если Hello() опущено, зачем вам нужно GetB()? Если вы опускаете Hello() ваша строка сводится к a.GetB(); без какого-либо задания, и многие инструменты дадут предупреждение о том, что вы не используете возвращаемое значение, потому что это редко вы хотите сделать.

Причина, по которой вы, похоже, не в порядке с этим, заключается в том, что ваш метод не только пытается сделать то, что необходимо для возврата определенного значения, но также имеет побочный эффект, а именно ввода-вывода. Если бы у вас была чистая функция, для GetB() не было бы никакой причины, если бы вы GetB() последующий вызов, то есть, если вы не собираетесь ничего делать с результатом.

Если вы присвойте результат GetB() переменной, это утверждение на свой счет и будет выполнено в любом случае. Поэтому это рассуждение объясняет, почему в

var b = a.GetB();
b.Hello();

только вызов Hello() опущен, когда при использовании цепочки методов целая цепочка опущена.

Вы также можете посмотреть, что-то совсем другое, чтобы получить лучшую перспективу: оператор с нулевым условием или оператор elvis ? введенный в С# 6.0. Хотя это только синтаксический сахар для более сложного выражения с нулевыми проверками, он позволяет построить нечто вроде цепочки методов с возможностью короткого замыкания на основе нулевой проверки.

Например, GetFoos()?.MakeBars()?.AnnounceBars(); будет только доходить до конца, если предыдущие методы не возвращают значение null, иначе последующие вызовы будут опущены.

Это может быть противоречиво, но попробуйте подумать о своем сценарии как об обратном: компилятор опускает ваши вызовы до Hello() в вашем a.GetB().Hello(); так как вы все равно не достигли конца цепи.


отказ

Это все было аргументами для кресла, поэтому, пожалуйста, возьмите это и аналогию с оператором Элвиса с солью.

Ответ 2

Это сводится к фразе:

(включая оценку получателя и параметры вызова) опускается.

В выражении:

a.GetB().Hello();

"оценка получателя": a.GetB(). Итак: это опускается в соответствии со спецификацией, и это полезный трюк, позволяющий [Conditional] избегать накладных расходов на вещи, которые не используются. Когда вы помещаете его в локальный:

var b = a.GetB();
b.Hello();

то "оценка получателя" является только локальной b, но исходный var b = a.GetB(); (даже если локальный b заканчивается удалением).

Это может иметь непреднамеренные последствия, поэтому: используйте [Conditional] с большой осторожностью. Но причины таковы, что такие вещи, как ведение журнала и отладка, могут быть добавлены и удалены тривиально. Обратите внимание, что параметры также могут быть проблематичными при наименее обработанном:

LogStatus("added: " + engine.DoImportantStuff());

а также:

var count = engine.DoImportantStuff();
LogStatus("added: " + count);

может отличаться, если LogStatus отмечен [Conditional] - в результате ваш фактический "важный материал" не будет выполнен.

Ответ 3

Должно ли оно вести себя по-разному на основе того, хранится ли получатель условного вызова во временной переменной или нет?

Да.

Какое обоснование компилятора С# не оценивает a.GetB() в этой ситуации?

Ответы Марка и Сорена в основном правильные. Этот ответ заключается только в том, чтобы четко документировать график.

  • Эта функция была разработана в 1999 году, и целью этой функции всегда было удаление всего заявления.
  • Законопроекты от 2003 года показывают, что команда разработчиков тогда поняла, что спецификация неясна в этом вопросе. До этого момента спецификация только вызывала, что аргументы не будут оцениваться. Я отмечаю, что спецификация делает общую ошибку при вызове аргументов "параметров", хотя, конечно, можно предположить, что они имели в виду "фактические параметры", а не "формальные параметры".
  • Предполагалось создать рабочий элемент, чтобы исправить спецификацию ECMA по этому вопросу; видимо, этого никогда не было.
  • В первый раз, когда исправленный текст появился в любой спецификации С#, была спецификация С# 4.0, которая, мне кажется, была в 2010 году. (Я не помню, была ли это одна из моих исправлений или кто-то другой нашел ее.)
  • Если спецификация ECMA 2017 года не содержит этой поправки, то это ошибка, которая должна быть исправлена в следующей версии. Лучше на 15 лет позже, чем никогда, я думаю.