Этот вопрос вдохновлен этим вопросом, который показывает следующий фрагмент кода.
int s;
if((s = foo()) == ERROR)
print_error();
Я нахожу, что этот стиль трудно читает и подверженный ошибке (как исходный вопрос демонстрирует - он был запрошен недостающими круглыми скобками вокруг присвоения). Я вместо этого записал бы следующее, которое на самом деле короче с точки зрения символов.
int s = foo();
if(s == ERROR)
print_error();
Это не первый раз, когда я видел эту идиому, хотя, и я предполагаю, что существуют причины (возможно, исторические) для нее так часто используемый. Каковы те причины?
Когда вы пишете цикл, иногда желательно использовать первую форму, как в этом известном примере от K&R:
int c;
while ((c = getchar()) != EOF) {
/* stuff */
}
Не существует элегантного «второго варианта» способа написать это без повторения:
int c = getchar();
while (c != EOF) {
/* stuff */
c = getchar();
}
Или:
int c;
for (c = getchar(); c != EOF; c = getchar()) {
/* stuff */
}
Теперь, когда присвоение c
повторяется, код более подвержен ошибкам, потому что необходимо синхронизировать оба оператора.
Итак, нужно уметь легко научиться читать и писать первую форму. Учитывая это, кажется логичным использовать ту же форму в условиях if
.
Я предпочитаю использовать первую форму в основном потому, что она мне удобна для чтения - как кто-то сказал, она гораздо более тесно связывает вызов функции и проверку возвращаемого значения.
Я всегда выбираю второй. Его легче читать, нет опасности опустить круглые скобки вокруг присваивания, и его легче выполнить с помощью отладчика.
Я думаю, что ранние компиляторы не были настолько умны в оптимизации по истерическим причинам. Помещая его в одну строку как одно выражение, он дает компилятору подсказку, что то же значение, полученное из foo (), можно протестировать, а не загружать значение из s
.
Я предпочитаю ясность вашего второго примера с заданием и проверкой, выполненными позже. У современного компилятора не возникнет проблем с оптимизацией этого в регистры, избегая ненужных загрузок из хранилища памяти.
Я часто нахожу, что разделение присвоения на другую строку заставляет отладчик наблюдать или "локальные" окна ведут себя лучше в отношении наличия и правильного значения "s", по крайней мере в неоптимизированных сборках.
Это также позволяет использовать пошаговое выполнение отдельно в строках назначения и тестирования (опять же, в неоптимизированных сборках), что может быть полезно, если вы не хотите возиться с разборкой или смешанным представлением.
YMMV для каждого компилятора и отладчика и, конечно же, для оптимизированных сборок.
Я часто предпочитаю первую форму. Я не могу точно сказать почему, но это как-то связано с семантикой.
Второй стиль кажется мне более похожим на две отдельные операции. Вызвать функцию и затем сделать что-то с результатом - две разные вещи. В первом стиле это одна логическая единица. Вызвать функцию, сохранить первичный результат и, в конце концов, обработать случай ошибки.
Я знаю, что это довольно расплывчато и далеко не полностью рационально, поэтому я буду использовать то или другое в зависимости от важности сохраненной переменной или тестового случая.
Я делаю сознательную попытку комбинировать и то, и другое, когда это возможно. ИМО, «штрафа» в размере недостаточно, чтобы преодолеть преимущество в ясности.
Преимущество ясности заключается в одном факте: для такой функции вы всегда должны думать о вызове функции и проверке возвращаемого значения как о одиночном действии, которое нельзя разбить на две части (" атомный ", если хотите). Вы не должны никогда вызывать такую функцию без немедленной проверки ее возвращаемого значения.
Разделение двух (вообще) приводит к гораздо большей вероятности того, что вы иногда полностью пропускаете проверку возвращаемого значения. В других случаях вы случайно вставляете код между вызовом и проверкой возвращаемого значения, который фактически зависит от успешного выполнения этой функции. Если вы всегда объединяете все это в один оператор, это (почти) исключает любую возможность попасть в эти ловушки.
Лично я предпочитаю, чтобы задания и тесты проводились на разных линиях. Он менее сложен синтаксически, менее подвержен ошибкам и более понятен. Это также позволяет компилятору указывать более точные местоположения ошибок / предупреждений и часто упрощает отладку.
Это также позволяет мне более легко делать такие вещи, как:
int rc = function();
DEBUG_PRINT(rc);
if (rc == ERROR) {
recover_from_error();
} else {
keep_on_going(rc);
}
Я настолько предпочитаю этот стиль, что в случае циклов я бы предпочел:
while (1) {
int rc = function();
if (rc == ERROR) {
break;
}
keep_on_going(rc);
}
, чем выполнять присваивание в , в то время как
условное . Мне очень не нравится, что у моих тестов есть побочные эффекты.