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


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

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 флаг), мы видим только GetB печатается на консоли, как вызов Hello() будет пропущен компилятором. В отладочной сборке появятся обе печати.

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

a.GetB().Hello();

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

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

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

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

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

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

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

3 69

3 ответа:

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

Марк Gravell это!--22--> уже показывает, что такое поведение предназначено и что оно означает на практике. Вы также спросили о обоснование за этим, но, похоже, недовольны упоминанием Марка об удалении накладных расходов.

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

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

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

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

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

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

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

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

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

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

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

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

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

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


отказ от ответственности

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

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

в выражении:

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] - в результате чего ваши фактические "важные вещи" не были сделаны.

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

да.

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

ответы Марка и Серена в основном верны. Этот ответ просто четко документировать сроки.

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