Estilo de programação: Verificando novamente os erros

4

Ei, eu tenho uma pergunta sobre o estilo de programação, porque no meu código atual eu estou usando uma função maior que chama algumas funções menores e todas elas precisam ser verificadas por erros. Então, algo assim:

 void bigFunction() {
      /* some computations */
      if(smallFunction1() == -1) {
           free(mem1);
           free(mem2);
           fclose(file);
           unlink(filename);
           return -1;
      }
      if(smallFunction2() == -1) {
           free(mem1);
           free(mem2);
           fclose(file);
           unlink(filename);
           return -1;
      }
        if(smallFunction3() == -1) {
           free(mem1);
           free(mem2);
           fclose(file);
           unlink(filename);
           return -1;
      }
      /* more computations and stuff in biggerFunction */
 }

Eu acho que você pode ver claramente o meu problema: o código depois de uma dessas funções falhar é sempre o mesmo, e eu sinto como repetir este codificador de novo e de novo fará o meu código cada vez mais ilegível.

Como lidar com esse problema? gotos veio em minha mente, mas em meus cursos de programação na universidade me disseram nunca usem gotos (embora eu esqueça a razão pela qual ...)

    
por Chris 04.04.2011 / 09:21
fonte

8 respostas

11

Que tal

 void bigFunction() {
      /* some computations */
      if(smallFunction1() == -1 || smallFunction2() == -1 || smallFunction3() == -1) {
           free(mem1);
           free(mem2);
           fclose(file);
           unlink(filename);
           return -1;
      }
      /* more computations and stuff in biggerFunction */
 }

Ou estou sentindo falta de alguma coisa aqui?

    
por 04.04.2011 / 09:49
fonte
12

Uma opção é colocar o código repetido em seu próprio método:

void bigFunction() {
    /* some computations */
    if(smallFunction1() == -1) {
        CleanUpAfterError();
        return -1;
    }
    if(smallFunction2() == -1) {
        CleanUpAfterError();
        return -1;
    }
    if(smallFunction3() == -1) {
        CleanUpAfterError();
        return -1;
    }
    /* more computations and stuff in biggerFunction */
}

void CleanUpAfterError() {
    free(mem1);
    free(mem2);
    fclose(file);
    unlink(filename);
}
    
por 04.04.2011 / 09:49
fonte
6

Esta é provavelmente a única circunstância em que um goto seria aceitável, mas você pode fazê-lo sem.

void bigFunction() {
    /* some computations */
    bool has_error = smallFunction1() == -1;
    if (!has_error) {
        has_error |= smallFunction2() == -1;
    }
    if (!has_error) {
        has_error |= smallFunction3() == -1;
    }
    if(has_error) {
        free(mem1);
        free(mem2);
        fclose(file);
        unlink(filename);
        return -1;
    }
    /* more computations and stuff in biggerFunction */
}
    
por 04.04.2011 / 10:19
fonte
5

Em C, este é um daqueles casos em que se pode argumentar que o uso judicioso de goto é justificável, por exemplo

void bigFunction()
{
    // NB: important to initialise these so that we can clean up properly in all cases
    void * mem1 = NULL;
    void * mem2 = NULL;
    FILE * file = NULL;

    /* some computations */

    if (smallFunction1() == -1)
        goto CleanUpAndExit;

    if (smallFunction2() == -1)
        goto CleanUpAndExit;

    if (smallFunction3() == -1)
        goto CleanUpAndExit;

    /* more computations and stuff in biggerFunction */

cleanUpAndExit:
    free(mem1); // NB: OK to call free on NULL pointer
    free(mem2);
    if (file != NULL) fclose(file); // NB: NOT OK to call fclose on NULL FILE *
    unlink(filename);
    return;
}

Claro que em outras línguas existem maneiras melhores de fazer esse tipo de coisa, por exemplo exceções em C ++.

    
por 04.04.2011 / 10:53
fonte
4

goto geralmente é desaprovado, e em 25 anos eu acho que tive que usá-lo uma vez.

No entanto, esse tipo de coisa adquire nuances de dogma religioso às vezes e o pensamento racional é empurrado para um lado.

Se um goto é o caminho mais simples e mais limpo, use-o. No final, a clareza é boa. Torturar-se para evitar o uso de um goto é desperdiçar seu valioso tempo e causar confusão ao mantenedor que o segue.

Em caso de dúvida, explique por que você fez isso com um comentário.

Meu padrão de codificação diz que o goto é proibido. Mas também tem uma seção dizendo que qualquer uma das regras pode ser quebrada por um bom motivo, desde que a razão seja explicada no código-fonte com um comentário. Isso permite que qualquer coisa que seja pragmática seja feita se for a melhor coisa para a tarefa. Os padrões de codificação devem ser sobre as melhores práticas, com uma cláusula "out", se fizer sentido.

    
por 04.04.2011 / 11:39
fonte
1

Sua intuição está certa. Como dividir a função depende exatamente do que os diferentes bits fazem (fique à vontade para postar mais alguns detalhes). Mas, do alto da minha cabeça, possibilidades promissoras seriam:

  • Se for possível usar algum código C ++ neste projeto, não use malloc / free, em vez disso, use new e atribua-o a um auto_ptr e, em seguida, quando "retornar" e a variável sair do escopo , a memória será automaticamente excluída. (Uma abordagem semelhante pode ser usada para fazer automaticamente a outra limpeza sempre que a variável ficar fora do escopo. A palavra-chave nomeada de maneira desajeitada é "RAII: Aquisição de recurso é inicialização"

  • Se for possível usar algum código C ++ neste projeto, pode ou não ser apropriado transformar parte ou toda essa função em uma classe, que usa alguns dos argumentos como argumentos de construtor e executa limpeza na destruição e as entranhas da função em uma ou mais funções membro. (Você pode fazer algo semelhante em C, colocando as variáveis em uma estrutura, e tendo todas as funções relevantes, um ponteiro para uma instância dele).

  • Eu assumo que o código de limpeza depende de variáveis de membro no bigFunction? (se não, seria razoável dividir isso em uma função separada.) Mas você pode ser capaz de quebrar outras partes. Por exemplo, que tal:

bool /* or int */ smallFunctionsOK( /* args */ ) {
    if (smallFunction1( /* args */ ) == -1) return false;
    if (smallFunction2( /* args */ ) == -1) return false;
    if (smallFunction3( /* args */ ) == -1) return false;
    return true;
}

void bigFunction() {
        someComputations( /* args */ );
     if(smallFunctionsOK()) {
          moreComputations( /* args */ );
     } else {
          free(mem1);
          free(mem2);
          fclose(file);
          unlink(filename);
          return -1;
     }
}

Você provavelmente não pode fazer tudo isso porque algumas das partes precisam permanecer na função principal, mas veja se você pode quebrar algumas partes dele. Não tenha medo de colocar os bits importantes em outra função e a limpeza entediante neste - é igualmente provável que leia com facilidade o contrário.

    
por 08.04.2011 / 21:45
fonte
0

Existe (pelo menos) um contexto em que você pode limpar bastante o código, colocando o fardo de verificar o status nas pequenas funções em vez do grande. Isso só deve ser usado em uma situação em que as funções são estreitamente relacionadas e consistentemente usadas em sucessão semelhante.

Cada pequena função recebe um sinalizador de status como parte de seus parâmetros e o verifica primeiro antes de prosseguir para sua respectiva computação.

void smallFunction1(..., bool* status)
{
    if(*status != True)
        return;

    // Computation...
}

A grande função é então codificada da seguinte forma:

bool status = True;
smallFunction1(..., &status);
smallFunction2(..., &status);
smallFunction3(..., &status);
if(!status)
{
     // Clean up...
     return -1;
}

Evidentemente, há uma oportunidade aqui para algum processamento desnecessário, onde várias chamadas de função são feitas sem motivo. Portanto, isso é melhor usado em casos em que a taxa de erro esperada é baixa.

Um exemplo em que esse tipo de coisa pode ser adequado seria onde vários campos de um pacote de dados são codificados separadamente. Por exemplo, encodeHeader(...) , encodePayload(...) , encodeCRC(...) , etc.

    
por 08.04.2011 / 19:15
fonte
-1

Diferentes idiomas lidam com isso de maneiras diferentes.

Em C ++, sua melhor aposta é usar objetos e ter a limpeza nos destruidores, usando RAII. Uma vantagem é que isso mantém todo o código para algo em um lugar, em vez de ter tudo em uma inicialização e uma seção de limpeza.

Em alguns idiomas, você pode colocar uma construção try { ... } finally { ... } para limpeza.

O próprio C não possui nenhum desses, então você terá que encontrar uma maneira de fingir. Uma técnica é goto ( goto s não é tão ruim quando usada de maneira limitada; as diretrizes nunca são usadas, a menos que se avance para uma seção significativa de código).

    
por 08.04.2011 / 19:51
fonte