Suponiendo que tengo este pseudocódigo:
bool conditionA = executeStepA();
if (conditionA){
bool conditionB = executeStepB();
if (conditionB){
bool conditionC = executeStepC();
if (conditionC){
...
}
}
}
executeThisFunctionInAnyCase();
Las funciones executeStepX
deben ejecutarse si y solo si las anteriores tienen éxito. En cualquier caso, la executeThisFunctionInAnyCase
función debe llamarse al final. Soy un novato en programación, lo siento por la pregunta muy básica: ¿hay alguna manera (en C / C ++, por ejemplo) para evitar tanto tiempo?if
cadena produzca ese tipo de "pirámide de código", a expensas de la legibilidad del código? ?
Sé que si pudiéramos omitir la executeThisFunctionInAnyCase
llamada a la función, el código podría simplificarse como:
bool conditionA = executeStepA();
if (!conditionA) return;
bool conditionB = executeStepB();
if (!conditionB) return;
bool conditionC = executeStepC();
if (!conditionC) return;
Pero la restricción es la executeThisFunctionInAnyCase
llamada a la función. ¿Podría la break
declaración ser utilizada de alguna manera?
c++
c
if-statement
control-flow
ABCplus
fuente
fuente
false
es similar a una situación excepcional?false
retorno puede ser bastante normal.Respuestas:
Puede usar un
&&
(lógico Y):Esto satisfará sus dos requisitos:
executeStep<X>()
debe evaluar solo si el anterior tuvo éxito (esto se llama evaluación de cortocircuito )executeThisFunctionInAnyCase()
se ejecutará en cualquier casofuente
&&
y,||
por lo tanto, no hay forma de que desee unirlas en una solaif
declaración sin atornillar la legibilidad. Y no siempre es fácil mover esas condiciones a funciones externas, ya que pueden depender de muchas variables locales calculadas previamente, lo que crearía un desastre terrible si intenta pasar cada una como argumento individual.Simplemente use una función adicional para que su segunda versión funcione:
El uso de ifs profundamente anidados (su primera variante) o el deseo de salir de "parte de una función" generalmente significa que necesita una función adicional.
fuente
foo
funciona a través de una secuencia de condiciones y acciones relacionadas. La funciónbar
está limpiamente separada de las decisiones. Si viéramos los detalles de las condiciones y acciones, podría resultar quefoo
todavía se está haciendo demasiado, pero por ahora esta es una buena solución.bar
usted tendrían que pasarlas manualmentefoo
como parámetros. Si ese es el caso yfoo
solo se llama una vez, me equivocaría al usar la versión goto para evitar definir dos funciones estrechamente acopladas que terminarían no siendo muy reutilizables.if (!executeStepA() || !executeStepB() || !executeStepC()) return
Los programadores de la vieja escuela C usan
goto
en este caso. Es el único usogoto
que en realidad es alentado por la guía de estilo de Linux, se llama la salida de función centralizada:Algunas personas evitan el uso
goto
envolviendo el cuerpo en un bucle y rompiéndolo, pero efectivamente ambos enfoques hacen lo mismo. Elgoto
enfoque es mejor si necesita alguna otra limpieza solo siexecuteStepA()
fue exitosa:Con el enfoque de bucle, terminaría con dos niveles de bucles en ese caso.
fuente
goto
e inmediatamente piensa "Este es un código terrible" pero tiene sus usos válidos.goto
y ni siquiera tengo que pensar para ver que este es un código terrible. Una vez tuve que mantener eso, es muy desagradable. El OP sugiere una alternativa razonable en lenguaje C al final de la pregunta, e incluí eso en mi respuesta.throw
¡es en muchos aspectos peor quegoto
porque con el contexto localthrow
ni siquiera está claro dónde vas a terminar! Use las mismas consideraciones de diseño para el flujo de control de estilo de goto que lo haría para las excepciones.Esta es una situación común y hay muchas formas comunes de tratarla. Aquí está mi intento de una respuesta canónica. Comenta si me perdí algo y mantendré esta publicación actualizada.
Esta es una flecha
Lo que está discutiendo se conoce como el antipatrón de flecha . Se llama flecha porque la cadena de ifs anidados forma bloques de código que se expanden más y más hacia la derecha y luego hacia la izquierda, formando una flecha visual que "apunta" al lado derecho del panel del editor de código.
Aplana la flecha con la guardia
Aquí se discuten algunas formas comunes de evitar la flecha . El método más común es usar un patrón de protección , en el que el código maneja los flujos de excepción primero y luego maneja el flujo básico, por ejemplo, en lugar de
... usarías ...
Cuando hay una larga serie de guardias, esto aplana el código considerablemente ya que todos los guardias aparecen completamente a la izquierda y sus ifs no están anidados. Además, está visualmente emparejando la condición lógica con su error asociado, lo que hace que sea mucho más fácil saber lo que está sucediendo:
Flecha:
Guardia:
Esto es objetiva y cuantificablemente más fácil de leer porque
Cómo agregar código común al final
El problema con el patrón de guardia es que se basa en lo que se llama "retorno oportunista" o "salida oportunista". En otras palabras, rompe el patrón de que todas y cada una de las funciones deben tener exactamente un punto de salida. Este es un problema por dos razones:
A continuación, proporcioné algunas opciones para solucionar esta limitación mediante el uso de funciones de idioma o evitando el problema por completo.
Opción 1. No puede hacer esto: use
finally
Desafortunadamente, como desarrollador de c ++, no puedes hacer esto. Pero esta es la respuesta número uno para los idiomas que contienen finalmente una palabra clave, ya que esto es exactamente para lo que sirve.
Opción 2. Evita el problema: reestructura tus funciones
Puede evitar el problema dividiendo el código en dos funciones. Esta solución tiene la ventaja de funcionar para cualquier idioma y, además, puede reducir la complejidad ciclomática , que es una forma comprobada de reducir la tasa de defectos y mejora la especificidad de cualquier prueba unitaria automatizada.
Aquí hay un ejemplo:
Opción 3. Truco de idioma: usa un bucle falso
Otro truco común que veo es usar while (verdadero) y break, como se muestra en las otras respuestas.
Si bien esto es menos "honesto" que el uso
goto
, es menos propenso a equivocarse al refactorizar, ya que marca claramente los límites del alcance lógico. ¡Un ingenuo programador que corta y pega sus etiquetas o susgoto
declaraciones puede causar grandes problemas! (Y, francamente, el patrón es tan común ahora que creo que comunica claramente la intención y, por lo tanto, no es "deshonesto" en absoluto).Hay otras variantes de estas opciones. Por ejemplo, uno podría usar en
switch
lugar dewhile
. Cualquier construcción de lenguaje con unabreak
palabra clave probablemente funcionaría.Opción 4. Aproveche el ciclo de vida del objeto
Otro enfoque aprovecha el ciclo de vida del objeto. Use un objeto de contexto para transportar sus parámetros (algo de lo que nuestro ingenuo ejemplo carece sospechosamente) y deséchelo cuando haya terminado.
Nota: Asegúrese de comprender el ciclo de vida del objeto de su idioma de elección. Necesitas algún tipo de recolección de basura determinista para que esto funcione, es decir, debes saber cuándo se llamará al destructor. En algunos idiomas deberá usar en
Dispose
lugar de un destructor.Opción 4.1. Aproveche el ciclo de vida del objeto (patrón de envoltura)
Si va a utilizar un enfoque orientado a objetos, puede hacerlo bien. Esta opción utiliza una clase para "ajustar" los recursos que requieren limpieza, así como sus otras operaciones.
Nuevamente, asegúrese de comprender el ciclo de vida de su objeto.
Opción 5. Truco del lenguaje: usar evaluación de cortocircuito
Otra técnica es aprovechar la evaluación de cortocircuito .
Esta solución aprovecha la forma en que funciona el operador &&. Cuando el lado izquierdo de && se evalúa como falso, el lado derecho nunca se evalúa.
Este truco es más útil cuando se requiere un código compacto y cuando no es probable que el código vea mucho mantenimiento, por ejemplo, está implementando un algoritmo bien conocido. Para una codificación más general, la estructura de este código es demasiado frágil; Incluso un cambio menor en la lógica podría desencadenar una reescritura total.
fuente
Solo haz
Es así de simple.
Debido a tres ediciones en las que cada una ha cambiado la pregunta de manera fundamental (cuatro si una cuenta la revisión de nuevo a la versión # 1), incluyo el ejemplo de código al que estoy respondiendo:
fuente
&&
lista mucho más clara. Comúnmente rompo las condiciones en líneas separadas y agrego un final// explanation
a cada una ... Al final, es mucho menos código para mirar, y una vez que entiendes cómo&&
funciona, no hay un esfuerzo mental continuo. Mi impresión es que la mayoría de los programadores profesionales de C ++ estarían familiarizados con esto, pero como usted dice en diferentes industrias / proyectos, el enfoque y la experiencia difieren.En realidad, hay una manera de diferir acciones en C ++: haciendo uso del destructor de un objeto.
Suponiendo que tiene acceso a C ++ 11:
Y luego usando esa utilidad:
fuente
executeThisFunctionInAnyCase();
se ejecutará incluso sifoo();
arroja una excepción. Al escribir código seguro de excepción, es una buena práctica poner todas esas funciones de limpieza en un destructor.foo()
. Y si lo haces, atrápalo. Problema resuelto. Solucione errores solucionándolos, no escribiendo soluciones alternativas.Defer
clase es una pequeña pieza de código reutilizable que le permite realizar cualquier limpieza de final de bloque, de una manera segura y excepcional. se conoce más comúnmente como guardia de alcance . sí, cualquier uso de un protector de alcance puede expresarse de otras maneras más manuales, al igual que cualquierfor
bucle puede expresarse como un bloque y unwhile
bucle, que a su vez puede expresarse conif
ygoto
, que puede expresarse en lenguaje ensamblador si lo desea, o para aquellos que son verdaderos maestros, alterando los bits en la memoria a través de rayos cósmicos dirigidos por el efecto mariposa de gruñidos y cánticos cortos especiales. Pero, ¿por qué hacer eso?Hay una buena técnica que no necesita una función de envoltura adicional con las declaraciones de retorno (el método prescrito por Itjax). Hace uso de un
while(0)
pseudo-loop do . Estowhile (0)
asegura que en realidad no es un bucle sino que se ejecuta solo una vez. Sin embargo, la sintaxis del bucle permite el uso de la instrucción break.fuente
inline
. De todos modos, esta es una buena técnica para saber, porque ayuda en algo más que este problema.También puedes hacer esto:
De esta manera, tiene un tamaño de crecimiento lineal mínimo, +1 línea por llamada, y es fácil de mantener.
EDITAR : (Gracias @Unda) No es un gran fan porque pierdes visibilidad IMO:
fuente
¿Funcionaría esto? Creo que esto es equivalente a tu código.
fuente
ok
cuando uso la misma variable como esta.Asumiendo que el código deseado es como lo veo actualmente:
Diría que el enfoque correcto, ya que es el más simple de leer y el más fácil de mantener, tendría menos niveles de sangría, que es (actualmente) el propósito declarado de la pregunta.
Esto evita cualquier necesidad de
goto
s, excepciones,while
bucles ficticios u otras construcciones difíciles y simplemente continúa con el simple trabajo en cuestión.fuente
return
ybreak
saltar fuera del bucle sin necesidad de introducir variables adicionales de "bandera". En este caso, usar un goto sería igualmente inocuo: recuerde que está cambiando la complejidad de goto adicional por una complejidad variable mutable adicional.newbie
, proporciona una solución más limpia sin inconvenientes. Noto que tampoco depende desteps
tener la misma firma o incluso de ser funciones en lugar de bloques. Puedo ver que esto se usa como un refactor de primer paso, incluso cuando un enfoque más sofisticado es válido.Tal vez no sea la mejor solución, pero puede poner sus declaraciones en un
do .. while (0)
bucle y usarbreak
declaraciones en lugar dereturn
.fuente
do .. while (0)
para definiciones de macro también está abusando de los bucles, pero se considera correcto.Podrías poner todo el
if
condiciones, formateadas como lo desee en una función propia, y al regresar ejecute elexecuteThisFunctionInAnyCase()
función.Desde el ejemplo base en el OP, la prueba de condición y la ejecución se pueden dividir como tal;
Y luego llamado como tal;
Si las lambdas de C ++ 11 están disponibles (no había una etiqueta de C ++ 11 en el OP, pero aún pueden ser una opción), entonces podemos renunciar a la función separada y envolver esto en una lambda.
fuente
Si no le gustan
goto
y no le gustan losdo { } while (0);
bucles y le gusta usar C ++, también puede usar una lambda temporal para tener el mismo efecto.fuente
if
le disgusta ir&&
a no le gusta hacer {} mientras (0)&&
le gusta C ++ ... Lo siento, no pude resistir, pero esa última condición falla porque la pregunta está etiquetada con c y c ++Las cadenas de IF / ELSE en su código no son el problema del idioma, sino el diseño de su programa. Si puede refactorizar o reescribir su programa, me gustaría sugerir que busque en Patrones de diseño ( http://sourcemaking.com/design_patterns ) para encontrar una mejor solución.
Por lo general, cuando ve muchos IF y otros en su código, es una oportunidad para implementar el Patrón de diseño de estrategia ( http://sourcemaking.com/design_patterns/strategy/c-sharp-dot-net ) o tal vez una combinación de otros patrones.
Estoy seguro de que hay alternativas para escribir una larga lista de if / else, pero dudo que cambien algo, excepto que la cadena se verá bonita para ti (Sin embargo, la belleza está en el ojo del espectador aún se aplica al código también:-) ) . Debería preocuparse por cosas como (en 6 meses cuando tengo una nueva condición y no recuerdo nada sobre este código, ¿podré agregarlo fácilmente? ¿O qué pasa si la cadena cambia, qué tan rápido y sin errores? voy a implementarlo)
fuente
Solo haz esto ...
99 veces de 100, esta es la única forma de hacerlo.
Nunca, nunca, intentes hacer algo "complicado" en el código de la computadora.
Por cierto, estoy bastante seguro de que el siguiente es el solución real que tenía en mente ...
La declaración de continuación es crítica en la programación algorítmica. (De manera similar, la declaración goto es crítica en la programación algorítmica).
En muchos lenguajes de programación puedes hacer esto:
(Tenga en cuenta en primer lugar: los bloques desnudos como ese ejemplo son una parte crítica e importante de escribir código hermoso, particularmente si se trata de programación "algorítmica").
De nuevo, eso es exactamente lo que tenías en la cabeza, ¿verdad? Y esa es la hermosa manera de escribirlo, así que tienes buenos instintos.
Sin embargo, trágicamente, en la versión actual de Objective-C (aparte: no sé sobre Swift, lo siento) hay una característica risible en la que se comprueba si el bloque de cierre es un bucle.
Así es como lo evitas ...
Así que no olvides eso ...
hacer {} while (falso);
solo significa "hacer este bloque una vez".
es decir, no hay absolutamente ninguna diferencia entre escribir
do{}while(false);
y simplemente escribir{}
.Esto ahora funciona perfectamente como quería ... aquí está la salida ...
Entonces, es posible que así sea como veas el algoritmo en tu cabeza. Siempre debes tratar de escribir lo que tienes en la cabeza. (Particularmente si no estás sobrio, ¡porque es cuando sale lo bonito! :))
En proyectos "algorítmicos" donde esto sucede mucho, en el objetivo-c, siempre tenemos una macro como ...
... entonces puedes hacer esto ...
Hay dos puntos:
a, aunque es estúpido que el objetivo-c verifique el tipo de bloque en el que se encuentra una declaración de continuación, es preocupante "luchar contra eso". Entonces es una decisión difícil.
b, está la pregunta, ¿debería sangrar, en el ejemplo, ese bloque? Pierdo el sueño por preguntas como esa, así que no puedo aconsejar.
Espero eso ayude.
fuente
if
, también podría usar nombres de funciones más descriptivos y poner los comentarios en las funciones.Haga que sus funciones de ejecución arrojen una excepción si fallan en lugar de devolver false. Entonces su código de llamada podría verse así:
Por supuesto, supongo que en su ejemplo original, el paso de ejecución solo devolvería falso en el caso de que ocurra un error dentro del paso.
fuente
Ya hay muchas buenas respuestas, pero la mayoría de ellas parecen compensar algunas (ciertamente muy pocas) de la flexibilidad. Un enfoque común que no requiere esta compensación es agregar una variable de estado / continuación . El precio es, por supuesto, un valor adicional para realizar un seguimiento de:
fuente
ok &= conditionX;
y no simplementeok = conditionX;
?En C ++ (la pregunta está etiquetada con C y C ++), si no puede cambiar las funciones para usar excepciones, aún puede usar el mecanismo de excepción si escribe una pequeña función auxiliar como
Entonces su código podría leer lo siguiente:
Si te gusta la sintaxis elegante, puedes hacer que funcione a través de un reparto explícito:
Entonces puedes escribir tu código como
fuente
Si su código es tan simple como su ejemplo y su idioma admite evaluaciones de cortocircuito, puede intentar esto:
Si pasa argumentos a sus funciones y obtiene otros resultados para que su código no se pueda escribir de la manera anterior, muchas de las otras respuestas se adaptarían mejor al problema.
fuente
Para C ++ 11 y más allá, un buen enfoque podría ser implementar un sistema de salida de alcance similar al mecanismo de alcance (salida) de D.
Una posible forma de implementarlo es usando C ++ 11 lambdas y algunas macros auxiliares:
Esto le permitirá regresar temprano de la función y garantizar que cualquier código de limpieza que defina siempre se ejecute al salir del ámbito:
Las macros son realmente solo decoración.
MakeScopeExit()
Se puede usar directamente.fuente
[=]
generalmente está mal para una lambda con alcance.[&]
: es seguro y mínimamente sorprendente. Capture por valor solo cuando la lambda (o copias) pueda sobrevivir más tiempo que el alcance en el punto de declaración ...¿Por qué nadie da la solución más simple? :RE
Si todas sus funciones tienen la misma firma, puede hacerlo de esta manera (para lenguaje C):
Para una solución limpia de C ++, debe crear una clase de interfaz que contenga un método de ejecución y envuelva sus pasos en objetos.
Entonces, la solución anterior se verá así:
fuente
Suponiendo que no necesita variables de condición individuales, invirtiendo las pruebas y utilizando el else-falthrough como la ruta "ok" le permitiría obtener un conjunto más vertical de sentencias if / else:
La omisión de la variable fallida hace que el código sea demasiado oscuro para la OMI.
Declarar las variables dentro está bien, no te preocupes por = vs ==.
Esto es oscuro, pero compacto:
fuente
Esto parece una máquina de estado, lo cual es útil porque puede implementarlo fácilmente con un patrón de estado .
En Java se vería así:
Una implementación funcionaría de la siguiente manera:
Y así. Entonces puede sustituir la condición big if con:
fuente
Como Rommik mencionó, podría aplicar un patrón de diseño para esto, pero usaría el patrón Decorador en lugar de la Estrategia, ya que desea encadenar llamadas. Si el código es simple, entonces iría con una de las respuestas bien estructuradas para evitar el anidamiento. Sin embargo, si es complejo o requiere un encadenamiento dinámico, entonces el patrón Decorador es una buena opción. Aquí hay un diagrama de clase yUML :
Aquí hay un ejemplo del programa LinqPad C #:
El mejor libro para leer sobre patrones de diseño, en mi humilde opinión, es Head First Design Patterns .
fuente
Varias respuestas insinuaron un patrón que vi y usé muchas veces, especialmente en la programación de redes. En las pilas de red a menudo hay una larga secuencia de solicitudes, cualquiera de las cuales puede fallar y detendrá el proceso.
El patrón común era usar
do { } while (false);
Usé una macro para
while(false)
hacerlo.do { } once;
El patrón común era:Este patrón era relativamente fácil de leer, y permitía el uso de objetos que destruirían adecuadamente y también evitaba múltiples retornos, lo que facilitaba un poco los pasos y la depuración.
fuente
Para mejorar la respuesta de C ++ 11 de Mathieu y evitar el costo de tiempo de ejecución incurrido por el uso de
std::function
, sugeriría usar lo siguienteEsta clase de plantilla simple aceptará cualquier functor que pueda llamarse sin ningún parámetro, y lo hace sin ninguna asignación de memoria dinámica y, por lo tanto, se ajusta mejor al objetivo de abstracción de C ++ sin sobrecarga innecesaria. La plantilla de función adicional está ahí para simplificar el uso mediante la deducción de parámetros de plantilla (que no está disponible para los parámetros de plantilla de clase)
Ejemplo de uso:
Al igual que la respuesta de Mathieu, esta solución es totalmente segura para excepciones y
executeThisFunctionInAnyCase
se llamará en todos los casos. En caso de queexecuteThisFunctionInAnyCase
se arroje, los destructores se marcan implícitamentenoexcept
y, por lo tantostd::terminate
, se emitirá una llamada a en lugar de provocar una excepción durante el desenrollado de la pila.fuente
functor
endeferred
'd constructor, no es necesario forzar amove
.Parece que quieres hacer todas tus llamadas desde un solo bloque. Como otros lo han propuesto, debe usar un
while
bucle y dejar de usarbreak
o una nueva función que puede dejar conreturn
(puede ser más limpia).Yo personalmente desterro
goto
, incluso para la función de salida. Son más difíciles de detectar al depurar.Una alternativa elegante que debería funcionar para su flujo de trabajo es construir una matriz de funciones e iterar sobre esta.
fuente
Porque también tienes [... bloque de código ...] entre ejecuciones, supongo que tiene asignación de memoria o inicializaciones de objetos. De esta manera, debe preocuparse por limpiar todo lo que ya ha inicializado en la salida, y también limpiarlo si encuentra algún problema y alguna de las funciones devuelve falso.
En este caso, lo mejor que tuve en mi experiencia (cuando trabajé con CryptoAPI) fue crear clases pequeñas, en el constructor inicializas tus datos, en el destructor no los inicializas. Cada clase de función siguiente debe ser hija de la clase de función anterior. Si algo salió mal, lanza una excepción.
Y luego en su código solo necesita llamar:
Supongo que es la mejor solución si cada llamada de ConditionX inicializa algo, asigna memoria, etc. Lo mejor es asegurarse de que todo se limpie.
fuente
Una forma interesante es trabajar con excepciones.
Si escribe dicho código, de alguna manera va en la dirección equivocada. No lo veré como "el problema" tener ese código, sino tener una "arquitectura" tan desordenada.
Consejo: discuta esos casos con un desarrollador experimentado en el que confíe ;-)
fuente
Otro enfoque:
do - while
bucle, a pesar de que se mencionó antes, no había ningún ejemplo que mostrara cómo se ve:(Bueno, ya hay una respuesta con el
while
bucle, pero eldo - while
bucle no verifica de forma redundante si es verdadero (al principio), sino al final xD (sin embargo, esto se puede omitir)).fuente