switch - if not c++
¿Cómo evitar las cadenas “if”? (30)
¿Podría usarse la instrucción break de alguna manera?
Quizás no sea la mejor solución, pero puede poner sus declaraciones en un bucle do .. while (0)
y usar declaraciones de break
lugar de return
.
Suponiendo que tengo este pseudo-código:
bool conditionA = executeStepA();
if (conditionA){
bool conditionB = executeStepB();
if (conditionB){
bool conditionC = executeStepC();
if (conditionC){
...
}
}
}
executeThisFunctionInAnyCase();
Las funciones executeStepX
deben ejecutarse solo si las anteriores son correctas. En cualquier caso, la función executeThisFunctionInAnyCase
debe llamarse al final. Soy un novato en programación, lo siento mucho por la pregunta tan básica: ¿hay alguna forma (en C / C ++ por ejemplo) de evitar ese largo tiempo if
cadena produce ese tipo de "pirámide de código", a expensas del código? ¿legibilidad?
Sé que si pudiéramos omitir la executeThisFunctionInAnyCase
función executeThisFunctionInAnyCase
, 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
función executeThisFunctionInAnyCase
. ¿Podría usarse la instrucción break
de alguna manera?
¿Funcionaría esto? Creo que esto es equivalente con su código.
bool condition = true; // using only one boolean variable
if (condition) condition = executeStepA();
if (condition) condition = executeStepB();
if (condition) condition = executeStepC();
...
executeThisFunctionInAnyCase();
Asumiendo que el código deseado es como lo veo actualmente:
bool conditionA = executeStepA();
if (conditionA){
bool conditionB = executeStepB();
if (conditionB){
bool conditionC = executeStepC();
if (conditionC){
...
}
}
}
executeThisFunctionInAnyCase();
Yo diría que el enfoque correcto, en el sentido de que es el más sencillo 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.
// Pre-declare the variables for the conditions
bool conditionA = false;
bool conditionB = false;
bool conditionC = false;
// Execute each step only if the pre-conditions are met
conditionA = executeStepA();
if (conditionA)
conditionB = executeStepB();
if (conditionB)
conditionC = executeStepC();
if (conditionC) {
...
}
// Unconditionally execute the ''cleanup'' part.
executeThisFunctionInAnyCase();
Esto evita cualquier necesidad de goto
s, excepciones, dummy while
bucles, u otras construcciones difíciles y simplemente continúa con el simple trabajo a la mano.
En realidad, hay una manera de diferir las acciones en C ++: haciendo uso del destructor de un objeto.
Suponiendo que tiene acceso a C ++ 11:
class Defer {
public:
Defer(std::function<void()> f): f_(std::move(f)) {}
~Defer() { if (f_) { f_(); } }
void cancel() { f_ = std::function<void()>(); }
private:
Defer(Defer const&) = delete;
Defer& operator=(Defer const&) = delete;
std::function<void()> f_;
}; // class Defer
Y luego usando esa utilidad:
int foo() {
Defer const defer{&executeThisFunctionInAnyCase}; // or a lambda
// ...
if (!executeA()) { return 1; }
// ...
if (!executeB()) { return 2; }
// ...
if (!executeC()) { return 3; }
// ...
return 4;
} // foo
Esta es una situación común y hay muchas formas comunes de lidiar con ella. Aquí está mi intento de una respuesta canónica. Por favor comente si me perdí algo y mantendré esta publicación actualizada.
Esto es una flecha
Lo que estás discutiendo es conocido como la flecha anti-patrón . Se llama flecha porque la cadena de if anidada 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.
Aplanar la flecha con la guardia
Algunas formas comunes de evitar la flecha se discuten here . El método más común es usar un patrón de guard , en el que el código controla primero los flujos de excepción y luego el flujo básico, por ejemplo, en lugar de
if (ok)
{
DoSomething();
}
else
{
_log.Error("oops");
return;
}
... usarías ...
if (!ok)
{
_log.Error("oops");
return;
}
DoSomething(); //notice how this is already farther to the left than the example above
Cuando hay una larga serie de guardas, esto aplana el código considerablemente, ya que todas las guardas aparecen completamente a la izquierda y sus ifs no están anidados. Además, está emparejando visualmente la condición lógica con su error asociado, lo que hace que sea mucho más fácil saber lo que está pasando:
Flecha:
ok = DoSomething1();
if (ok)
{
ok = DoSomething2();
if (ok)
{
ok = DoSomething3();
if (!ok)
{
_log.Error("oops"); //Tip of the Arrow
return;
}
}
else
{
_log.Error("oops");
return;
}
}
else
{
_log.Error("oops");
return;
}
Guardia:
ok = DoSomething1();
if (!ok)
{
_log.Error("oops");
return;
}
ok = DoSomething2();
if (!ok)
{
_log.Error("oops");
return;
}
ok = DoSomething3();
if (!ok)
{
_log.Error("oops");
return;
}
ok = DoSomething4();
if (!ok)
{
_log.Error("oops");
return;
}
Esto es objetivamente y cuantitativamente más fácil de leer porque
- Los caracteres {y} para un bloque lógico dado están más cerca
- La cantidad de contexto mental necesaria para entender una línea en particular es menor
- Es más probable que la totalidad de la lógica asociada con una condición if esté en una página.
- La necesidad de que el programador desplace la página / pista visual se reduce considerablemente
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" u "salida oportunista". En otras palabras, rompe el patrón de que todas y cada una de las funciones deberían tener exactamente un punto de salida. Este es un problema por dos razones:
- Frota a algunas personas de manera incorrecta, por ejemplo, las personas que aprendieron a codificar en Pascal han aprendido que una función = un punto de salida.
- No proporciona una sección de código que se ejecute al salir sin importar qué , que es el tema en cuestión.
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 puedes hacer esto: usar finally
Desafortunadamente, como desarrollador de c ++, no puedes hacer esto. Pero esta es la respuesta número uno para los idiomas que contienen una palabra clave finally, ya que es exactamente para lo que sirve.
try
{
if (!ok)
{
_log.Error("oops");
return;
}
DoSomething(); //notice how this is already farther to the left than the example above
}
finally
{
DoSomethingNoMatterWhat();
}
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 trabajar 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:
void OuterFunction()
{
DoSomethingIfPossible();
DoSomethingNoMatterWhat();
}
void DoSomethingIfPossible()
{
if (!ok)
{
_log.Error("Oops");
return;
}
DoSomething();
}
Opción 3. Truco de lenguaje: usar un bucle falso
Otro truco común que veo es usar while (verdadero) y romper, como se muestra en las otras respuestas.
while(true)
{
if (!ok) break;
DoSomething();
break; //important
}
DoSomethingNoMatterWhat();
Si bien esto es menos "honesto" que usar goto
, es menos propenso a ser desordenado al refactorizar, ya que claramente marca los límites del alcance lógico. ¡Un codificador ingenuo que corta y pega sus etiquetas o sus declaraciones de goto
puede causar problemas importantes! (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 el switch
lugar de while
. Cualquier construcción de lenguaje con una palabra clave de break
probablemente funcionaría.
Opción 4. Aprovechar 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 ejemplo ingenuo carece sospechosamente) y deséchelo cuando haya terminado.
class MyContext
{
~MyContext()
{
DoSomethingNoMatterWhat();
}
}
void MainMethod()
{
MyContext myContext;
ok = DoSomething(myContext);
if (!ok)
{
_log.Error("Oops");
return;
}
ok = DoSomethingElse(myContext);
if (!ok)
{
_log.Error("Oops");
return;
}
ok = DoSomethingMore(myContext);
if (!ok)
{
_log.Error("Oops");
}
//DoSomethingNoMatterWhat will be called when myContext goes out of scope
}
Nota: asegúrese de comprender el ciclo de vida del objeto de su idioma de elección. Necesita algún tipo de recolección de basura determinista para que esto funcione, es decir, debe saber cuándo se llamará al destructor. En algunos idiomas, deberá usar 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, también puede hacerlo bien. Esta opción usa una clase para "envolver" los recursos que requieren limpieza, así como sus otras operaciones.
class MyWrapper
{
bool DoSomething() {...};
bool DoSomethingElse() {...}
void ~MyWapper()
{
DoSomethingNoMatterWhat();
}
}
void MainMethod()
{
bool ok = myWrapper.DoSomething();
if (!ok)
_log.Error("Oops");
return;
}
ok = myWrapper.DoSomethingElse();
if (!ok)
_log.Error("Oops");
return;
}
}
//DoSomethingNoMatterWhat will be called when myWrapper is destroyed
Una vez más, asegúrese de entender el ciclo de vida de su objeto.
Opción 5. Truco de lenguaje: usar evaluación de cortocircuito
Otra técnica es aprovechar la evaluación de cortocircuito .
if (DoSomething1() && DoSomething2() && DoSomething3())
{
DoSomething4();
}
DoSomethingNoMatterWhat();
Esta solución aprovecha la forma en que funciona el operador de &&. 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 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.
Hay una buena técnica que no necesita una función de envoltura adicional con las declaraciones de devolución (el método prescrito por Itjax). Utiliza un pseudo-loop do while(0)
. La while (0)
asegura que en realidad no es un bucle sino que se ejecuta solo una vez. Sin embargo, la sintaxis de bucle permite el uso de la instrucción break.
void foo()
{
// ...
do {
if (!executeStepA())
break;
if (!executeStepB())
break;
if (!executeStepC())
break;
}
while (0);
// ...
}
Los programadores de la vieja escuela C usan goto
en este caso. Es el único uso de goto
que realmente es alentado por la guía de estilo de Linux, se llama la salida de la función centralizada:
int foo() {
int result = /*some error code*/;
if(!executeStepA()) goto cleanup;
if(!executeStepB()) goto cleanup;
if(!executeStepC()) goto cleanup;
result = 0;
cleanup:
executeThisFunctionInAnyCase();
return result;
}
Algunas personas trabajan alrededor de usar goto
envolviendo el cuerpo en un bucle y rompiendo, pero efectivamente ambos enfoques hacen lo mismo. El enfoque goto
es mejor si necesita alguna otra limpieza solo si executeStepA()
fue exitoso:
int foo() {
int result = /*some error code*/;
if(!executeStepA()) goto cleanupPart;
if(!executeStepB()) goto cleanup;
if(!executeStepC()) goto cleanup;
result = 0;
cleanup:
innerCleanup();
cleanupPart:
executeThisFunctionInAnyCase();
return result;
}
Con el enfoque de bucle, terminarías con dos niveles de bucles en ese caso.
Puede poner todas las condiciones if
, formateadas como desee en una función propia, en el retorno, ejecute la función executeThisFunctionInAnyCase()
.
Desde el ejemplo base en el OP, la prueba de condición y la ejecución pueden dividirse como tales;
void InitialSteps()
{
bool conditionA = executeStepA();
if (!conditionA)
return;
bool conditionB = executeStepB();
if (!conditionB)
return;
bool conditionC = executeStepC();
if (!conditionC)
return;
}
Y luego llamado como tal;
InitialSteps();
executeThisFunctionInAnyCase();
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 envolverla en una lambda.
// Capture by reference (variable access may be required)
auto initialSteps = [&]() {
// any additional code
bool conditionA = executeStepA();
if (!conditionA)
return;
// any additional code
bool conditionB = executeStepB();
if (!conditionB)
return;
// any additional code
bool conditionC = executeStepC();
if (!conditionC)
return;
};
initialSteps();
executeThisFunctionInAnyCase();
Puedes usar un &&
(lógica AND):
if (executeStepA() && executeStepB() && executeStepC()){
...
}
executeThisFunctionInAnyCase();
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 caso
Si no te gusta goto
y no do { } while (0);
gusta do { } while (0);
Los bucles y como usar C ++ también pueden usar un lambda temporal para tener el mismo efecto.
[&]() { // create a capture all lambda
if (!executeStepA()) { return; }
if (!executeStepB()) { return; }
if (!executeStepC()) { return; }
}(); // and immediately call it
executeThisFunctionInAnyCase();
Simplemente use una función adicional para que su segunda versión funcione:
void foo()
{
bool conditionA = executeStepA();
if (!conditionA) return;
bool conditionB = executeStepB();
if (!conditionB) return;
bool conditionC = executeStepC();
if (!conditionC) return;
}
void bar()
{
foo();
executeThisFunctionInAnyCase();
}
El uso de ifs profundamente anidados (su primera variante) o el deseo de salir de "parte de una función" generalmente significa que sí necesita una función adicional.
Solo haz
if( executeStepA() && executeStepB() && executeStepC() )
{
// ...
}
executeThisFunctionInAnyCase();
Es así de simple.
Debido a tres ediciones en las que cada una ha cambiado fundamentalmente la pregunta (cuatro si uno cuenta la revisión de nuevo a la versión # 1), incluyo el ejemplo del código al que respondo:
bool conditionA = executeStepA();
if (conditionA){
bool conditionB = executeStepB();
if (conditionB){
bool conditionC = executeStepC();
if (conditionC){
...
}
}
}
executeThisFunctionInAnyCase();
También podrías hacer esto:
bool isOk = true;
std::vector<bool (*)(void)> funcs; //vector of function ptr
funcs.push_back(&executeStepA);
funcs.push_back(&executeStepB);
funcs.push_back(&executeStepC);
//...
//this will stop at the first false return
for (auto it = funcs.begin(); it != funcs.end() && isOk; ++it)
isOk = (*it)();
if (isOk)
//doSomeStuff
executeThisFunctionInAnyCase();
De esta manera, tiene un tamaño de crecimiento lineal mínimo, +1 línea por llamada, y es fácil de mantener.
EDIT : (Gracias @Unda) No es un gran fan porque pierde visibilidad IMO:
bool isOk = true;
auto funcs { //using c++11 initializer_list
&executeStepA,
&executeStepB,
&executeStepC
};
for (auto it = funcs.begin(); it != funcs.end() && isOk; ++it)
isOk = (*it)();
if (isOk)
//doSomeStuff
executeThisFunctionInAnyCase();
¿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):
bool (*step[])() = {
&executeStepA,
&executeStepB,
&executeStepC,
...
};
for (int i = 0; i < numberOfSteps; i++) {
bool condition = step[i]();
if (!condition) {
break;
}
}
executeThisFunctionInAnyCase();
Para obtener una solución C ++ limpia, debe crear una clase de interfaz que contenga un método de ejecución y ajuste sus pasos en objetos.
Entonces, la solución anterior se verá así:
Step *steps[] = {
stepA,
stepB,
stepC,
...
};
for (int i = 0; i < numberOfSteps; i++) {
Step *step = steps[i];
if (!step->execute()) {
break;
}
}
executeThisFunctionInAnyCase();
En C ++ (la pregunta está etiquetada tanto en C como en 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
struct function_failed {};
void attempt(bool retval)
{
if (!retval)
throw function_failed(); // or a more specific exception class
}
Entonces su código podría leer lo siguiente:
try
{
attempt(executeStepA());
attempt(executeStepB());
attempt(executeStepC());
}
catch (function_failed)
{
// -- this block intentionally left empty --
}
executeThisFunctionInAnyCase();
Si te interesa la sintaxis, puedes hacer que funcione a través de un reparto explícito:
struct function_failed {};
struct attempt
{
attempt(bool retval)
{
if (!retval)
throw function_failed();
}
};
Entonces puedes escribir tu código como
try
{
(attempt) executeStepA();
(attempt) executeStepB();
(attempt) executeStepC();
}
catch (function_failed)
{
// -- this block intentionally left empty --
}
executeThisFunctionInAnyCase();
Si su código es tan simple como su ejemplo y su idioma admite evaluaciones de cortocircuito, puede intentar esto:
StepA() && StepB() && StepC() && StepD();
DoAlways();
Si pasa argumentos a sus funciones y obtiene otros resultados para que su código no pueda escribirse de la manera anterior, muchas de las otras respuestas se adaptarían mejor al problema.
Solo haces esto ..
coverConditions();
executeThisFunctionInAnyCase();
function coverConditions()
{
bool conditionA = executeStepA();
if (!conditionA) return;
bool conditionB = executeStepB();
if (!conditionB) return;
bool conditionC = executeStepC();
if (!conditionC) return;
}
99 veces de 100, esta es la única manera de hacerlo.
Nunca, nunca, jamás intentes hacer algo "complicado" en el código de la computadora.
Por cierto, estoy bastante seguro de que la siguiente es la solución real que tenía en mente ...
La declaración de continuación es crítica en la programación algorítmica. (Al igual que, la declaración goto es crítica en la programación algorítmica).
En muchos lenguajes de programación puedes hacer esto:
-(void)_testKode
{
NSLog(@"code a");
NSLog(@"code b");
NSLog(@"code c/n");
int x = 69;
{
if ( x == 13 )
{
NSLog(@"code d---/n");
continue;
}
if ( x == 69 )
{
NSLog(@"code e---/n");
continue;
}
if ( x == 13 )
{
NSLog(@"code f---/n");
continue;
}
}
NSLog(@"code g");
}
(En primer lugar, tenga en cuenta que los bloques desnudos como ese ejemplo son una parte crítica e importante de la escritura de código hermoso, especialmente si se trata de programación "algorítmica").
De nuevo, eso es exactamente lo que tenías en tu 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 conozco Swift, lo siento), existe una función risible en la que se comprueba si el bloque adjunto es un bucle.
Así es como te mueves de eso ...
-(void)_testKode
{
NSLog(@"code a");
NSLog(@"code b");
NSLog(@"code c/n");
int x = 69;
do{
if ( x == 13 )
{
NSLog(@"code d---/n");
continue;
}
if ( x == 69 )
{
NSLog(@"code e---/n");
continue;
}
if ( x == 13 )
{
NSLog(@"code f---/n");
continue;
}
}while(false);
NSLog(@"code g");
}
Así que no olvides que ...
do {} 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 quisieras ... aquí está la salida ...
Entonces, es posible que sea así como se ve el algoritmo en tu cabeza. Siempre debes tratar de escribir lo que hay en tu cabeza. (Particularmente si no estás sobrio, ¡porque es cuando sale lo bonito! :))
En proyectos "algorítmicos" donde esto sucede mucho, en object-c, siempre tenemos una macro como ...
#define RUNONCE while(false)
... entonces puedes hacer esto ...
-(void)_testKode
{
NSLog(@"code a");
int x = 69;
do{
if ( x == 13 )
{
NSLog(@"code d---/n");
continue;
}
if ( x == 69 )
{
NSLog(@"code e---/n");
continue;
}
if ( x == 13 )
{
NSLog(@"code f---/n");
continue;
}
}RUNONCE
NSLog(@"code g");
}
Hay dos puntos:
a, aunque es estúpido que objetivo-c verifique el tipo de bloqueo en el que se encuentra una declaración de continuación, es preocupante "luchar contra eso". Así que es una decisión difícil.
B, ahí está la pregunta, ¿debes sangrar, en el ejemplo, ese bloque? Pierdo el sueño por preguntas como esa, así que no puedo aconsejar.
Espero eso ayude.
Suponiendo que no necesita variables de condición individuales, invertir las pruebas y utilizar el error de else en el camino "ok" le permitiría obtener un conjunto más vertical de declaraciones if / else:
bool failed = false;
// keep going if we don''t fail
if (failed = !executeStepA()) {}
else if (failed = !executeStepB()) {}
else if (failed = !executeStepC()) {}
else if (failed = !executeStepD()) {}
runThisFunctionInAnyCase();
Omitir la variable fallida hace que el código sea un poco demasiado oscuro IMO.
Declarar las variables dentro está bien, no te preocupes = = = ==.
// keep going if we don''t fail
if (bool failA = !executeStepA()) {}
else if (bool failB = !executeStepB()) {}
else if (bool failC = !executeStepC()) {}
else if (bool failD = !executeStepD()) {}
else {
// success !
}
runThisFunctionInAnyCase();
Esto es oscuro, pero compacto:
// keep going if we don''t fail
if (!executeStepA()) {}
else if (!executeStepB()) {}
else if (!executeStepC()) {}
else if (!executeStepD()) {}
else { /* success */ }
runThisFunctionInAnyCase();
Una forma interesante es trabajar con excepciones.
try
{
executeStepA();//function throws an exception on error
......
}
catch(...)
{
//some error handling
}
finally
{
executeThisFunctionInAnyCase();
}
Si escribes tal código, vas de alguna manera en la dirección equivocada. No lo veo como "el problema" tener tal código, sino tener una "arquitectura" tan desordenada.
Consejo: discuta esos casos con un desarrollador experimentado en el que confíe ;-)
Como mencionó Rommik, podrías aplicar un patrón de diseño para esto, pero yo usaría el patrón de Decorador en lugar de Estrategia ya que estás deseando encadenar llamadas. Si el código es simple, entonces me gustaría ir 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 Decorator es una buena opción. Aquí hay un diagrama de clase yUML :
Aquí hay un ejemplo del programa LinqPad C #:
void Main()
{
IOperation step = new StepC();
step = new StepB(step);
step = new StepA(step);
step.Next();
}
public interface IOperation
{
bool Next();
}
public class StepA : IOperation
{
private IOperation _chain;
public StepA(IOperation chain=null)
{
_chain = chain;
}
public bool Next()
{
bool localResult = false;
//do work
//...
// set localResult to success of this work
// just for this example, hard coding to true
localResult = true;
Console.WriteLine("Step A success={0}", localResult);
//then call next in chain and return
return (localResult && _chain != null)
? _chain.Next()
: true;
}
}
public class StepB : IOperation
{
private IOperation _chain;
public StepB(IOperation chain=null)
{
_chain = chain;
}
public bool Next()
{
bool localResult = false;
//do work
//...
// set localResult to success of this work
// just for this example, hard coding to false,
// to show breaking out of the chain
localResult = false;
Console.WriteLine("Step B success={0}", localResult);
//then call next in chain and return
return (localResult && _chain != null)
? _chain.Next()
: true;
}
}
public class StepC : IOperation
{
private IOperation _chain;
public StepC(IOperation chain=null)
{
_chain = chain;
}
public bool Next()
{
bool localResult = false;
//do work
//...
// set localResult to success of this work
// just for this example, hard coding to true
localResult = true;
Console.WriteLine("Step C success={0}", localResult);
//then call next in chain and return
return (localResult && _chain != null)
? _chain.Next()
: true;
}
}
En mi humilde opinión, el mejor libro para leer sobre patrones de diseño es Head First Design Patterns .
Como también tiene [... bloque de código ...] entre ejecuciones, supongo que tiene asignación de memoria o inicializaciones de objetos. De esta manera, debe preocuparse de limpiar todo lo que ya haya inicializado al salir, y también de limpiarlo si encuentra un problema y cualquiera de las funciones devolverá el valor falso.
En este caso, lo mejor que tuve en mi experiencia (cuando trabajé con CryptoAPI) fue crear clases pequeñas, en el constructor, usted inicializa sus datos y en el destructor, lo inicializa. Cada siguiente clase de función debe ser hija de la clase de función anterior. Si algo salió mal - lanza la excepción.
class CondA
{
public:
CondA() {
if (!executeStepA())
throw int(1);
[Initialize data]
}
~CondA() {
[Clean data]
}
A* _a;
};
class CondB : public CondA
{
public:
CondB() {
if (!executeStepB())
throw int(2);
[Initialize data]
}
~CondB() {
[Clean data]
}
B* _b;
};
class CondC : public CondB
{
public:
CondC() {
if (!executeStepC())
throw int(3);
[Initialize data]
}
~CondC() {
[Clean data]
}
C* _c;
};
Y luego en tu código solo necesitas llamar:
shared_ptr<CondC> C(nullptr);
try{
C = make_shared<CondC>();
}
catch(int& e)
{
//do something
}
if (C != nullptr)
{
C->a;//work with
C->b;//work with
C->c;//work with
}
executeThisFunctionInAnyCase();
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 limpiará.
Esto parece una máquina de estado, lo cual es útil porque puede implementarlo fácilmente con un state-pattern .
En Java se vería algo así:
interface StepState{
public StepState performStep();
}
Una implementación funcionaría de la siguiente manera:
class StepA implements StepState{
public StepState performStep()
{
performAction();
if(condition) return new StepB()
else return null;
}
}
Y así. Entonces puedes sustituir la condición grande con:
Step toDo = new StepA();
while(toDo != null)
toDo = toDo.performStep();
executeThisFunctionInAnyCase();
Haga que sus funciones de ejecución lancen una excepción si fallan en lugar de devolver falso. Entonces su código de llamada podría verse así:
try {
executeStepA();
executeStepB();
executeStepC();
}
catch (...)
Por supuesto, ¿estoy asumiendo que en tu ejemplo original el paso de ejecución solo devolvería falso en el caso de que ocurra un error dentro del paso?
Las cadenas de IF / ELSE en su código no son el problema del idioma, sino el diseño de su programa. Si puede redefinir o reescribir su programa, me gustaría sugerirle que busque en Design Patterns ( http://sourcemaking.com/design_patterns ) para encontrar una mejor solución.
Por lo general, cuando ve muchos IF y else 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 que 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 si la cadena cambia, qué tan rápido y sin errores?) seré implementado)
Otro do - while
bucle de aproximación , a pesar de que se mencionó anteriormente, no había ningún ejemplo que mostrara cómo se ve:
do
{
if (!executeStepA()) break;
if (!executeStepB()) break;
if (!executeStepC()) break;
...
break; // skip the do-while condition :)
}
while (0);
executeThisFunctionInAnyCase();
(Bueno, ya existe una respuesta con while
bucle, pero el do - while
bucle no verifica de forma redundante la verdad (al comienzo), sino que al final xD (aunque se puede omitir)).
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 utilizando las lambdas de C ++ 11 y algunas macros de ayuda:
template<typename F> struct ScopeExit
{
ScopeExit(F f) : fn(f) { }
~ScopeExit()
{
fn();
}
F fn;
};
template<typename F> ScopeExit<F> MakeScopeExit(F f) { return ScopeExit<F>(f); };
#define STR_APPEND2_HELPER(x, y) x##y
#define STR_APPEND2(x, y) STR_APPEND2_HELPER(x, y)
#define SCOPE_EXIT(code)/
auto STR_APPEND2(scope_exit_, __LINE__) = MakeScopeExit([&](){ code })
Esto le permitirá regresar temprano de la función y garantizar que el código de limpieza que defina siempre se ejecute al salir del alcance:
SCOPE_EXIT(
delete pointerA;
delete pointerB;
close(fileC); );
if (!executeStepA())
return;
if (!executeStepB())
return;
if (!executeStepC())
return;
Las macros son realmente solo decoración. MakeScopeExit()
Se puede utilizar directamente.
Para mejorar la respuesta de C ++ 11 de Mathieu y evitar el costo de tiempo de ejecución incurrido mediante el uso de std::function
, sugiero usar lo siguiente
template<typename functor>
class deferred final
{
public:
template<typename functor2>
explicit deferred(functor2&& f) : f(std::forward<functor2>(f)) {}
~deferred() { this->f(); }
private:
functor f;
};
template<typename functor>
auto defer(functor&& f) -> deferred<typename std::decay<functor>::type>
{
return deferred<typename std::decay<functor>::type>(std::forward<functor>(f));
}
Esta 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 una sobrecarga innecesaria. La plantilla de función adicional está ahí para simplificar el uso por la deducción de parámetros de plantilla (que no está disponible para los parámetros de plantilla de clase)
Ejemplo de uso:
auto guard = defer(executeThisFunctionInAnyCase);
bool conditionA = executeStepA();
if (!conditionA) return;
bool conditionB = executeStepB();
if (!conditionB) return;
bool conditionC = executeStepC();
if (!conditionC) return;
Al igual que la respuesta de Mathieu, esta solución es totalmente segura y executeThisFunctionInAnyCase
se llamará en todos los casos. En caso de executeThisFunctionInAnyCase
tirar, los destructores están marcados de forma implícita noexcept
y, por lo tanto std::terminate
, se emitirá una llamada a en lugar de provocar que se lance una excepción durante el desenrollado de la pila.
Parece que quieres hacer todas tus llamadas desde un solo bloque. Como otros lo han propuesto, debe usar un while
bucle y dejar de usar break
o una nueva función que puede dejar return
(puede estar más limpio).
Yo personalmente destierro goto
, incluso para salir de la función. Son más difíciles de detectar cuando se depura.
Una alternativa elegante que debería funcionar para su flujo de trabajo es construir una matriz de funciones e iterar en esta.
const int STEP_ARRAY_COUNT = 3;
bool (*stepsArray[])() = {
executeStepA, executeStepB, executeStepC
};
for (int i=0; i<STEP_ARRAY_COUNT; ++i) {
if (!stepsArray[i]()) {
break;
}
}
executeThisFunctionInAnyCase();
Varias respuestas apuntaban a un patrón que vi y usé muchas veces, especialmente en la programación en red. 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);
Utilicé una macro para while(false)
hacerla do { } once;
El patrón común era:
do
{
bool conditionA = executeStepA();
if (! conditionA) break;
bool conditionB = executeStepB();
if (! conditionB) break;
// etc.
} while (false);
Este patrón era relativamente fácil de leer, y permitía el uso de objetos que se destruirían adecuadamente y también evitaba las devoluciones múltiples, lo que hacía que las etapas y la depuración fueran un poco más fáciles.
Ya hay muchas respuestas buenas, pero la mayoría de ellas parecen compensar algunas (aunque muy poco) de la flexibilidad. Un enfoque común que no requiere esta compensación es agregar una variable de estado / mantenimiento . El precio es, por supuesto, un valor adicional para realizar un seguimiento de:
bool ok = true;
bool conditionA = executeStepA();
// ... possibly edit conditionA, or just ok &= executeStepA();
ok &= conditionA;
if (ok) {
bool conditionB = executeStepB();
// ... possibly do more stuff
ok &= conditionB;
}
if (ok) {
bool conditionC = executeStepC();
ok &= conditionC;
}
if (ok && additionalCondition) {
// ...
}
executeThisFunctionInAnyCase();
// can now also:
return ok;