Esses dias, começou-se a discutir na lista de discussão da Python Brasil razões para se utilizar exceções. Em um certo momento, um participante reconhecidamente competente comentou o quanto é difícil tratar erros através do retorno de funções, como em C.
Quando se tem um algoritmo complexo, cada operação passível de erro implica em uma série de if
s para verificar se a operação ocorreu corretamente. Se a operação tiver falhado, será necessário reverter todas as operações anteriores para sair do algoritmo sem alterar o estado do programa.
Vejamos um exemplo. Suponha que eu tenha a segunte struct
para representar arrays:
typedef struct { int size; int *array; } array_t;
Agora, eu vou fazer uma função que lê, de um arquivo texto, o número de elementos a ser posto em um desses arrays e, logo em seguida, os elementos. Essa função também vai alocar a struct
do array e o array de fato. O problema é que essa função é bastante propensa a erros, pois podemos não conseguir
- abrir o arquvo dado;
- alocar a
struct
; - ler o número de elementos do arquvo dado, seja por erro de entrada/saída, seja por fim do arquivo;
- alocar memória para guardar os elementos a serem lidos;
- ler um dos elementos, seja por erro de entrada/saída, seja por fim do arquivo.
Complicado, né? Note que, se conseguirmos abrir o arquivo mas não conseguirmos alocar a struct
, temos de fechar o arquivo; se conseguirmos abrir o arquivo e alocar a struct
mas não conseguirmos ler o número de elementos do arquivo, temos de dealocar a struct
e fechar o arquivo; e assim por diante. Assim sendo, se verificarmos todos os erros e adotarmos a tradição de, em caso de erro, retornar NULL
, nossa função seria mais ou menos assim:
array_t *readarray(const char *filename) { FILE *file; array_t *array; int i; file = fopen(filename, "r"); if (file == NULL) return NULL; array = malloc(sizeof(array_t)); if (array == NULL) { fclose(file); return NULL; } if (fscanf(file, "%d", &(array->size)) == EOF) { free(array); fclose(file); return NULL; } array->array = malloc(sizeof(int)*array->size); if (array->array == NULL) { free(array); fclose(file); return NULL; } for (i = 0; i < array->size; i++) { if (fscanf(file, "%d", array->array+i) == EOF) { free(array->array); free(array); fclose(file); return NULL; } } return array; }
De fato, bastante trabalhoso, e com muito código repetido…
Note, porém, como há duas situações no código acima. Em uma, quando tenho duas operações para reverter, preciso reverter primeiro a última executada, e depois a anterior. Por exemplo, quando vou dealocar tanto a struct
quanto o array de inteiros, preciso dealocar primeiro o array de inteiros e depois a struct
. Se dealoco a struct
primeiro. posso não conseguir dealocar o array posteriormente.
Na outra situação, a ordem não importa. Por exemplo, se vou dealocar a struct
e fechar o arquivo, não importa em que ordem eu o faça. Isso implica que eu posso, também, reverter primeiro a última operação executada e depois a primeira operação.
Qual o sentido disso? Bem, na prática, nunca vi uma situação onde eu tenha de reverter primeiro a primeira operação executada, depois a segunda e assim por diante. Isso significa que, quando faço as operações a()
, b()
, c()
etc. a maneira “natural” de revertê-las é chamando os reversores de trás para frente, mais ou menos como:
a();
b();
c();
/* ... */
revert_c();
revert_b();
revert_a();
Agora, vem o pulo do gato. No código acima, após cada operação, vamos colocar um if
para verificar se ela falhou ou não. Se falhou, executar-se-á um goto
para o reversor da última operação bem sucedida:
a();
if (failed_a()) goto FAILED_A;
b();
if (failed_b()) goto FAILED_B;
c();
if (failed_c()) goto FAILED_C;
/* ... */
revert_c();
FAILED_C:
revert_b();
FAILED_B:
revert_a();
FAILED_A:
return;
Se a()
falhar, o algoritmo retorna; se b()
falhar, o algoritmo vai para FAILED_B:
, reverte a()
e retorna; se c()
falhar, o algoritmo vai para FAILED_C
, reverte b()
, reverte a()
e retorna. Consegue ver o padrão?
Pois bem, se aplicarmos esse padrão à nossa função readarray()
o resultado será algo como:
array_t *readarray(const char *filename) { FILE *file; array_t *array; int i; file = fopen(filename, "r"); if (file == NULL) goto FILE_ERROR; array = malloc(sizeof(array_t)); if (array == NULL) goto ARRAY_ALLOC_ERROR; if (fscanf(file, "%d", &(array->size)) == EOF) goto SIZE_READ_ERROR; array->array = malloc(sizeof(int)*array->size); if (array->array == NULL) goto ARRAY_ARRAY_ALLOC_ERROR; for (i = 0; i < array->size; i++) { if (fscanf(file, "%d", array->array+i) == EOF) goto ARRAY_CONTENT_READ_ERROR; } return array; ARRAY_CONTENT_READ_ERROR: free(array->array); ARRAY_ARRAY_ALLOC_ERROR: SIZE_READ_ERROR: free(array); ARRAY_ALLOC_ERROR: fclose(file); FILE_ERROR: return NULL; }
Quais as vantagens desse padrão? Bem, ele reduz a repetição de código de reversão de operações e separa o código de tratamento de erro da lógica da função. Na verdade, apesar de eu achar exceções o melhor método de tratamento de erros moderno, para tratamento de erros in loco (dentro da própria função) eu acho esse método muito mais prático.
Post Revisions:
There are no revisions for this post.