2010-05-23 13 views
19

Esta pregunta está inspirada en this question, que presenta el siguiente fragmento de código.¿Cuáles son las ventajas de aplastar la asignación y la comprobación de errores en una línea?

int s; 
if((s = foo()) == ERROR) 
    print_error(); 

puedo encontrar este estilo difícil de leer y propenso a errores (como la pregunta original demuestra - que fue motivada por paréntesis en torno a la asignación que falta). En cambio, escribiría lo siguiente, que en realidad es más corto en términos de caracteres.

int s = foo(); 
if(s == ERROR) 
    print_error(); 

Ésta no es la primera vez que he visto este idioma, sin embargo, y supongo que hay razones históricas (tal vez) para que sea tan frecuente. ¿Cuáles son esas razones?

+3

El estilo anterior tiene mucho más sentido cuando se usa con 'while' en lugar de' if', por ejemplo 'while ((c = getchar())!! = EOF) putchar (c);' – fredoverflow

+2

@FredOverflow, isn ¿Para qué sirve "for"? :) – avakar

+1

No, en absoluto. No hay inicialización ni incremento en el ejemplo anterior que pueda utilizarse en un ciclo 'for'. ¿Cómo se propone embellecer este código con 'for'? – fredoverflow

Respuesta

13

Cuando está escribiendo un bucle, a veces es conveniente utilizar la primera forma, como en este ejemplo famoso de K & R:

int c; 

while ((c = getchar()) != EOF) { 
    /* stuff */ 
} 

No hay elegante "segunda forma" manera de escribir esto sin una repetición:

int c = getchar(); 

while (c != EOF) { 
    /* stuff */ 
    c = getchar(); 
} 

O:

int c; 

for (c = getchar(); c != EOF; c = getchar()) { 
    /* stuff */ 
} 

Ahora que t La asignación a c se repite, el código es más propenso a errores, porque uno tiene que mantener ambas declaraciones sincronizadas.

Así que uno tiene que ser capaz de aprender a leer y escribir el primer formulario fácilmente. Y dado que, parece lógico usar la misma forma en las condiciones if también.

Tiendo a utilizar el primer formulario principalmente porque me resulta fácil de leer — como dijo otra persona, une la llamada de función y la prueba de valor de retorno mucho más de cerca.

+1

Gracias por responder. Yo personalmente escribiría el ciclo como 'for (;;) {int c = getchar(); si (c == EOF) se rompe;/* stuff * /} ', que no tiene redundancia y aún mantiene una operación por línea. – avakar

+0

@avakar: Buen punto. Pero, tiendo a evitar bucles "infinitos" con 'break 'en ellos a menos que sea necesario. No estoy seguro de esto sobre el ciclo 'for' que presentaste. Por supuesto, piensas que es más claro, o no escribirías tu ciclo de esa manera :-). Entonces, 'while ((c = getchar)! = EOF)' sigue siendo la mejor forma para mí. –

+0

, básicamente, lo que se necesita es que tenemos que hacer algo (declaraciones previas), verificar el estado del ciclo y luego hacer algo más (declaraciones posteriores), y no tenemos una construcción de bucle correspondiente para eso. Gracias a la naturaleza de la gramática C y C++, puede poner los pre-enunciados en la condición misma, pero le resultará menos legible a medida que aumente su complejidad. El enfoque 'break 'evita esto, pero, por supuesto, hay oponentes de roturas (y otras construcciones goto-like). De hecho, es una cuestión muy subjetiva :) – avakar

15

Creo que es por razones histéricas, que los primeros compiladores no fueron tan inteligentes en la optimización. Al ponerlo en una línea como una sola expresión, le da al compilador una pista de que se puede probar el mismo valor obtenido de foo() en lugar de cargar el valor específicamente desde s.

Prefiero la claridad de su segundo ejemplo, con la tarea y la prueba hechas más tarde. Un compilador moderno no tendrá problemas para optimizar esto en los registros, evitando cargas innecesarias desde el almacén de memoria.

+10

+1 por "razones histéricas" ;-) –

+0

+1 Yo también lo haré. – JeremyP

+5

Mucha gente cree que "cuanto más corto sea mi código, menos código se generará y más rápido se ejecutará". A menudo, una mejor regla empírica es que "cuanto más legible es mi código, mejor será el compilador capaz de entenderlo y más rápido se ejecutará". Pero intenta convencer a la gente de eso. ;) – jalf

5

Hago un intento consciente de combinar los dos siempre que sea posible. La "penalización" de tamaño no es suficiente para superar la ventaja en claridad, IMO.

La ventaja de la claridad proviene de un hecho: para una función como esta, siempre se debe pensar en llamar a la función y comprobar el valor devuelto como un solo acción que no puede ser dividida en dos partes ("atómica", Si tu quieres). Debe llamar al nunca, tal función sin probar inmediatamente su valor de retorno.

Separación de los dos (en absoluto) conduce a una mayor probabilidad de que a veces se saltee la comprobación del valor de retorno por completo. Otras veces, accidentalmente inserta un código entre la llamada y la prueba del valor de retorno que realmente depende de que la función haya tenido éxito. Si siempre combinas todo en una sola declaración, (casi) elimina cualquier posibilidad de caer en estas trampas.

+0

Interesante punto de vista. Estoy de acuerdo en que es ventajoso pensar en la asignación/verificación de errores como una unidad atómica. (Personalmente, hago que las tres líneas sean un párrafo separado). – avakar

+0

Mejor aún, use excepciones. –

+0

@John: las excepciones no siempre son adecuadas. Para un ejemplo obvio, 'while ((ch = getc())! = EOF)'. –

0

A menudo prefiero el primer formulario. No podría decir exactamente por qué, pero tiene algo que ver con la semántica involucrada.

El segundo estilo me parece más como dos operaciones separadas. Llame a la función y luego haga algo con el resultado, 2 cosas diferentes. En el primer estilo, es una unidad lógica. Llame a la función, guarde el resultado temporal y eventualmente maneje el caso de error.

Sé que es bastante vago y está lejos de ser completamente racional, así que usaré uno u otro dependiendo de la importancia de la variable guardada o el caso de prueba.

+0

Son dos operaciones separadas. Una tarea y una comparación. ¿No debería sentirse así? –

+0

Sí, pero la importancia de la operación depende. Si la asignación está solo allí, para mantener un temporal (para limpieza, por ejemplo), no es una parte esencial del algoritmo. Si el propósito de la operación es esta asignación, la escribiré en declaraciones separadas. Es la importancia relativa lo que hace la diferencia. –

4

Siempre iría por el segundo. Es más fácil de leer, no hay peligro de omitir los paréntesis alrededor de la tarea y es más fácil avanzar con un depurador.

+1

+1 para facilitar la depuración en el segundo ejemplo. –

1

A menudo encuentro la separación de la asignación en una línea diferente hace que el reloj del depurador o las ventanas "locales" se comporten mejor frente a la presencia y el valor correcto de "s", al menos en compilaciones no optimizadas.

También permite el uso de step-over por separado en las líneas de asignación y prueba (nuevamente, en compilaciones no optimizadas), lo que puede ser útil si no quiere ir a desordenar en el desmontaje o en la vista mixta.

YMMV por compilador y depurador y para compilaciones optimizadas, por supuesto.

1

Personalmente prefiero que las asignaciones y las pruebas sean en líneas diferentes. Es menos sintácticamente complicado, menos propenso a errores y más fácil de entender. También permite que el compilador le proporcione ubicaciones de error/advertencia más precisas y, a menudo, facilita la depuración.

También me permite hacer más fácilmente cosas como:

int rc = function(); 

DEBUG_PRINT(rc); 

if (rc == ERROR) { 
    recover_from_error(); 
} else { 
    keep_on_going(rc); 
} 

prefiero este estilo tanto que en el caso de bucles yo más bien:

while (1) { 
    int rc = function(); 
    if (rc == ERROR) { 
     break; 
    } 
    keep_on_going(rc); 
} 

de hacer la tarea en el while condicional. Realmente no me gusta que mis pruebas tengan efectos secundarios.

+0

Prefiero que mis dientes se extraigan con un cincel que tener una condición de salida de bucle en el bloque del lazo. Pero eso me deja con un dilema en el sentido de que o bien tengo que ejecutar el código que genera el bucle de salida dos veces (una vez antes del bucle y una vez al final) o poner una tarea en la condición de bucle. Supongo que su ejemplo está bien porque la condición de salida está al menos directamente debajo del tiempo. – JeremyP

0

Creo que la claridad siempre debe primar sobre las optimizaciones o "simplificaciones" basadas únicamente en la cantidad de caracteres escritos. Esta creencia me ha impedido cometer muchos errores tontos.

Separar la asignación y la comparación hace que ambos sean más claros y menos propensos a errores, incluso si la duplicación de la comparación puede introducir un error de vez en cuando. Entre otras cosas, los paréntesis se vuelven rápidamente difíciles de distinguir y mantener todo en una línea introduce más paréntesis. Además, al dividirlo, se limitan las declaraciones a hacer solo una, ya sea de obtener un valor o asignar uno.

Sin embargo, si espera que las personas que lean su código se sientan más cómodas con el idioma de una línea, entonces es lo suficientemente amplio como para no causar ningún problema a la mayoría de los programadores.Los programadores de C sin duda lo sabrán, incluso aquellos que puedan encontrarlo incómodo.

Cuestiones relacionadas