sábado, 14 de abril de 2007

Aplicações Robustas: Conhecendo mais sobre Exceções

Esse artigo é continuação do post “Robustez no Delphi

Conforme vimos no post anterior, sobre a importância de cuidarmos para que os softwares que venhamos a desenvolver sejam robustos, daremos mais um passo no intuito de conhecer com mais profundidade o mecanismo de tratamento de exceções nessa ferramenta. Fizemos uma introdução sobre o emprego das estruturas “Try, finally” e “try, except” como elas funcionam e como empregá-las. Associado a essa questão, chamamos a atenção também para as vantagens que podemos colher, quando efetivamente aplicamos os recursos existentes no Delphi para o tratamento de exceções e proteção de bloco de código. Além da garantia de execução segura do seu aplicativo, o que por si só já justifica seu emprego, obsevamos vantagens quanto a organização do código, facilidade de entendimento e manutenibilidade.
Vimos que as classes de exceções são definidas na exatamente como as demais classes existentes no Delphi. É recomendado que qualquer exceção que o desenvolvedor venha a criar seja derivada da classe “Exception”, que está definida na “SysUtils”. Quando seu programa faz uso da unit Sysutils todos os erros de runtime (erros em tempo de execução) automaticamente são traduzidos em exceções. Erros estes que eventualmente podem comprometer o funcionamento da sua aplicação, tais como: “insuffient memory”, ou “division by zero” e etc.


Trabalhando com Exceções parte II


1 – Criando uma exceção: Raise é a palavra reservada para se criar uma exceção (uma instância da classe Exception, ou qualquer uma de suas extenções) que é passada para o manipulador de exceções do Delphi. Para criar um objeto da classe “Exception” basta chamar o construtor da classe com o statement “raise”. Exemplo:

raise EMathError.Create(‘Erro: EMathError’);// Específico

Ou simplesmente

raise Exception.Create(‘Erro bla: Exception’);// Genérico


Quando vc cria uma exceção, como no exemplo acima, imediatamente o fluxo de código é interrompido e essa exceção passa a ser gerenciada pelo manipulador de exceção. Caso vc não a trate, o manipulador de exceções do Delphi irá exibir a string que foi passada como parâmetro para o construtor da classe exception, em seguida tentará retornar a execução normal do programa. Exemplo:

(* para esse exemplo coloquei um edit e um button, palheta standard, num form. *)
procedure TForm1.Button1Click(Sender: TObject);
begin
if Edit1.Text = '' then
raise EMathError.Create('Digite um número.');

end;


Salve o exemplo (simultaneamente pressione: “Ctrl + Shift + S”) e compile (sequencialmente digite: “Alt, p, b”), na pasta aonde vc salvou rode o programa a partir do executável e teste. Caso vc seja iniciante, e não saiba reconhecer o executável, basta procurar no diretório um arquivo de extenção “.exe”. Para executar, dê um duplo click no executável.


Vamos agora colocar algum nível de tratamento para essa exceção que acabamos de criar. Para reforçar a idéia do manipulador de exceção vamos codificar esse tratamento em outro procedimento. Vamos adicionar no Form1 mais um botão, Button2, e no vento OnClick do mesmo chamaremos o evento Button1Click que codificamos para o exemplo anterior.

procedure TForm1.Button2Click(Sender: TObject);
begin
try
Edit1.Text := '';// atribui vazio para forçar a exceção
Button1Click(Button1);// chama o procedimento que vai criar a axceção.
Form1.Color := clYellow;
except
Edit1.Text := '2'; // Ocorrendo a exceção, atribui ‘2’ para a prop. text de edit1.
Form1.Caption := 'Exceção ocorrida.'; // atribui valor para o captio do From1
Button2.Caption := 'Pressionado!';
end;

end;

Novamente, salve o exemplo e compile, rode o programa a partir do executável e teste. Lembre-se agora vc deve pressionar o Button2.



Note que o form1 não fica amarelo. Claro, a exceção interrompe a execução e desvia para o fluxo do “except” executando todos os comandos dispostos ali. Nenhuma mensagem é exibida, pq quando vc está trabalhando com um estrutura “try/except”, é como se vc falasse pro Delphi: Deixa que essa exceção eu trato. Ou seja, vc é responsável por esse tratamento, agora a manipulação dessa exceção é da sua competência, não mais do manipulador do Delphi Mas como podemos manipular a exceção? Vamos mudar um pouco o código para exemplificar isso:
.
(* Adicione mais um botão, TButton da palheta standard *)
procedure TForm1.Button3Click(Sender: TObject);
begin
try
Edit1.Text := '';
Button1Click(Button1);
Form1.Color := clYellow;// executado somente se não ocorrer a exceção
except
on MyVar: Exception do
MessageDlg(‘A msg capturada foi: ’ + MyVar.Message, mtWarning, [mbOK], 0);
end;
end;




Com a palavra reservada “on” podemos manipular a exceção. Ela permite usar uma variável (no exemplo eu usei MyVar), sem a necessidade de uma declaração prévia, numa seção var, para que possamos acessar a propriedade “message” da classe exeption capturar a mensagem e exibi-la no formato que desejarmos. Se eu quiser, ainda para esse tratamento executar mais de um comando, basta delimitar o bloco por “begin ... end”. Exemplo:

procedure TForm1.Button3Click(Sender: TObject);
begin
try
Edit1.Text := '';
Button1Click(Button1);
Form1.Color := clYellow;// executado somente se não ocorrer a exceção
except
on MyVar: Exception do
begin
Form1.Caption := MyVar.Message;
Button3.Caption := 'Pressionado!';
MessageDlg('A msg capturada foi: ' + MyVar.Message, mtWarning, [mbOK], 0);
end;
end;
end;


Para testar: salve e compile, rode o programa a partir do executável. Lembre-se agora vc deve pressionar o Button3.


2 - Estrutura “on ... do”: A idéia de tratamento de exceção comporta também a ação do programador recuperar em tempo de execução (run time) a exceção que ocorreu, identificar seu tipo e transformar a mensagem de erro que ela exibiria numa mensagem mais amigável, inteligível para o usuário. É justamente isso que fazemos os quando empregamos as palavras reservadas “on ... do”. Logo, podemos fazer tantos testes quanto necessário, dependendo da quantidade de tipos de exceções diferentes possam ocorrer.

(* Adicione mais um Edit, Edit2, e um Button, Button4. Ambos da palheta standard *)
procedure TForm1.Button4Click(Sender: TObject);
var
AuxInteiro: Integer;
begin
try
AuxInteiro := StrToInt(Edit2.Text);
if (Edit1.Text <> '') and (Edit2.Text <> '') then
raise Exception.Create('Exceção genérica!');

Edit1.Text := '';
Button1Click(Button1);

except
on Bl: EMathError do
begin
Form1.Color := clRed;
Form1.Caption := E.Message;
MessageDlg('Msg capturada: Edit1 ' + Bl.message, mtWarning, [mbOK], 0);
end;

on E: EConvertError do
begin
Form1.Color := clGreen;
Form1.Caption := E.Message;
MessageDlg('Msg (EConvertError): ' + E.message, mtConfirmation, [mbCancel], 0);
end;

on E: Excetpion do
begin
Form1.Color := clWhite;
Form1.Caption := '';
MessageDlg(E.message, mtInformation, [mbAbort], 0);
end;
end;
end;

Vamos testar: salve, compile, em seguida rode o programa a partir do executável. Se vc simplesmente apenas clicar o Button4. Vai acontecer um erro de conversão, visto que o processador vai tentar converter a string “Edit2” para inteiro. Portanto serão executados os comandos onde testamos se a exceção e do tipo “EConvertError”.

Entretanto, basta vc digitar qualquer número no Edit2 para que sejam executados os comandos da última estrutura “on ..do , onde testamos o tipo mais genérico de todos, “Exception”.



Agora, se vc digitar um número no Edit2 e limpar o conteúdo do Edit1 serão executados os comandos da primeira estrutura “on ..do , onde testamos o tipo da exceção “EMathError”.



Ok, até agora tudo parece simples. Concordo, de fato é simples, entretanto, devemos prestar muita atenção num pequeno detalhe que pode fazer toda a diferença na hora de tratarmos exceções: As estruturas “on ... do” devem sempre ser organizadas testando do tipo mais especializado para o mais genérico. Do contrário o processador jamais irá testar o mais especializado se ele encontrar um teste de um tipo genérico o qual extende a exceção corrente. Para exemplificar isso, altere o código do exemplo anterior colocando a últuma estrutura “on ... do” para ser a primeira linha após o “except”. Ao testar, vc poderá perceber que, embora a mensagem mude, nunca mais serão executados os comandos das outras estruturas não importe o que vc faça durante o teste.


(* trocando a estrutura de lugar *)
procedure TForm1.Button4Click(Sender: TObject);
var
AuxInteiro: Integer;
begin
try
AuxInteiro := StrToInt(Edit2.Text);
if (Edit1.Text <> '') and (Edit2.Text <> '') then
raise Exception.Create('Exceção genérica!');

Edit1.Text := '';
Button1Click(Button1);

except
on E: Excetpion do
begin
Form1.Color := clWhite;
Form1.Caption := '';
MessageDlg(E.message, mtInformation, [mbAbort], 0);
end;

(* não importa o que vc faça, essas linhas nunca serão executadas *)
on Bl: EMathError do
begin
Form1.Color := clRed;
Form1.Caption := E.Message;
MessageDlg('Msg capturada: Edit1 ' + Bl.message, mtWarning, [mbOK], 0);
end;

on E: EConvertError do
begin
Form1.Color := clGreen;
Form1.Caption := E.Message;
MessageDlg('Msg (EConvertError): ' + E.message, mtConfirmation, [mbCancel], 0);
end;
end;
end;


Como já falamos antes, a classe “Exception” é a mais genérica de todas as classes de exceção. Isso quer dizer que, ela é o ancestral mais alto de todas as outras exceções. Por isso, não importa que tipo exceção ocorra (todas, ou herdam diretamente de “Exception”, ou herdam de algum nível de herdeiro de “Exception”), quando vc for verificar que tipo de exceção ocorreu, se para isso vc tomar como referência “Exception”, isso será verdadeiro para qualquer descendente, sendo irrelevante o grau da descendência. Mas, pergunto eu, todas as demais estruturas “on ... do” não serão verificadas pelo processador? Não. Ele executa somente um bloco de código da estrutura “on ... do” dentro de um “try ... except”. Ao terminar de executa a última linha de comando do “on ..do”, cujo a classe de exceção ocorrida é igual, ou hierarquicamente inferior, ele sai do “try ..except ” para continuar a execução dos outros comandos que por ventura possam existir após o “end” delimitador da estrutura. É por isso que se o tipo mais genérico das classes de exceção estiver sendo verificado no primeiro “on .. do” os posteriores jamais serão executados. Retirei um trecho de código da SysUtils, fiz uma pequena adequação para exemplificar melhor a herança, na definição das classes de exceção:

Type

EExternal = class(Exception)
EExternalException = class(EExternal);
EIntError = class(EExternal);
EDivByZero = class(EIntError);
ERangeError = class(EIntError);
EIntOverflow = class(EIntError);
EMathError = class(EExternal);
EInvalidOp = class(EMathError);
EZeroDivide = class(EMathError);
EOverflow = class(EMathError);
EUnderflow = class(EMathError);
EInvalidPointer = class(EHeapException);
EInvalidCast = class(Exception);
EConvertError = class(Exception);
EAccessViolation = class(EExternal);
.
.
.


Um comentário:

  1. Fala bro. Ótimo tópico.

    Uma dica: é possível rodar o programa no depurador do Delphi sem parar o programa a cada exceção, permitindo testar nosso tratamento de erros sem precisar abrir mão do depurador.

    [Delphi 7] No menu Toos -> Debugger Options -> Aba Language Exceptions
    Desmarque a caixa 'Stop on Delphi Exceptions'

    Desse jeito nós rodamos o programa por dentro do Delphi mas ele age como se fosse executado por fora - Isso é muito útil na fase final do desenvolvimento do programa.

    Abraços.

    ResponderExcluir

 
BlogBlogs.Com.Br