Ruby Série #6 – Exceções e Manipulação de Exceções – Parte 1
Posted in Programação, Ruby/Rails on February 16th, 2010 by Edipo L Federle – Be the first to commentWarning: Missing argument 1 for GeSHi::GeSHi(), called in /home/storage/f/73/ab/bitside1/public_html/edipo_blog/blog/wp-content/plugins/codecolorer/codecolorer-core.php on line 137 and defined in /home/storage/f/73/ab/bitside1/public_html/edipo_blog/blog/wp-content/plugins/deans_code_highlighter/geshi.php on line 432
Warning: Missing argument 2 for GeSHi::GeSHi(), called in /home/storage/f/73/ab/bitside1/public_html/edipo_blog/blog/wp-content/plugins/codecolorer/codecolorer-core.php on line 137 and defined in /home/storage/f/73/ab/bitside1/public_html/edipo_blog/blog/wp-content/plugins/deans_code_highlighter/geshi.php on line 432
Em linhas gerais uma exceção é um objeto que indica um tipo de condição excepcional, ela indica que algo muito errado aconteceu, um clássico exemplo de exceção é a divisão por zero, aposto que todos já passaram por algum caso de divisão por zero, chamar métodos que não existem, passar argumentos errados para métodos e assim por diante, esses exemplos citados são todos erros de programação, mas uma exceção não é somente levantada por conta disso, também podemos ter outros tipos de exceções como tentar fazer o uso da rede quando a mesama estiver off.
Não irei falar mais sobre o que são exceções mas recomendo fortemente a leitura na wikipedia sobre isso.
Quando algum erro acontece, por exemplo à passagem de um argumento inválido para um método qualquer dizemos que uma exceção é levantada, por padrão o Ruby finaliza o programa quando algo do tipo acontece, mas como iremos ver é possível fazer à declaração de manipuladores de exceções, isto é um bloco de código que é sempre executado se uma exceção acontecer na execução de um bloco de código, quando uma exceção é levantada o que acontece é a transferência do fluxo de controle para um código que irá tratar essa exceção. Possivelmente você pode tentar fazer uma comparação com a delaração break para sair de um laço por exemplo, mas como iremos ver mais adiante as exceções são bem diferentes, poderosas e flexíveis ao ponto de nos premitir transmitir o fluxo para n blocos.
No Ruby podemos fazer o uso do método raise para levantar exceções e fazer o uso da cláusula rescue para tratar as exceções.
Todas exceções que são levantadas por raise são uma instância de classe Exception ou de alguma de suas subclasses.
Segue abaixo a hierarquia da classe Exception:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | Exception NoMemoryError ScriptError LoadError NotImplementedError SyntaxError SignalException Interrupt StandardError ArgumentError IOError EOFError IndexError LocalJumpError NameError NoMethodError RangeError FloatDomainError RegexpError RuntimeError SecurityError SystemCallError SystemStackError ThreadError TypeError ZeroDivisionError SystemExit fatal |
Logicamente voçê não tem que saber cada uma delas(seus nomes não bem óbvios) o que você deve ter em mente é que algumas dessas classes extendem uma classe conhecida como StandardError, ou seja, exceções, digamos assim que acontecem com mais frequência, algumas outra expressões representam níveis bem mais baixos, coisas que com menos possibilidade de recuperação logo os programas Ruby(normalmente) não tentam fazer a sua manipulação.
Objetos dos métodos de excecção.
Temos dois métodos que podem retornar alguns detalhes sobre uma dada exeção, esses métos são da classe Exception, message irá retornar uma sequência que permite obter alguns detalhes que podem ser entendidos por seres humanos, facilitando o entendimento de erros.
Também temos outro método bastante importante que é o backtrace, backtrace irá retornar um array de sequências representando uma pilha no ponto em que a exceção foi levantada.
A[0] = posição no qual a exceção foi levantada.
A[1] = posição no qual o método que levantou a exceção foi chamado.
A[2] = posição no qual esse método foi chmado.
E por ai vai, para poder ver um rastreamento da pilha o Ruby fornece um Método Kernel caller.
Objetos de Exceção.
Objetos de exceção são criados pelo método raise, também temos a opção de fazer a criação de nossos próprios objetos, normalmente usando o new ou com outra classe de métodos exception.
Lançando Exceções com Raise.
Quando um método do Kernel é sobreescreito uma exceção é levantada, fail é usado certas vezes quando há certa expectativa que seu programa deva ser finalizado pela exceção.
Existem n formas de se invocar o método raise, veremos abaixo algumas dessas formas:
Raise sem Argumentos
Se o método raise for chamado sem nenhum argumento, o mesmo irá criar um objeto do tipo RunTimeError que não irá ter nenhuma mensagem e então o levanta, por hora se raise for usado também sem argumento mas dentro de um rescue(iremos ver logo ele) o que irá acontencer é que a exceção(que estava sem manipulada) irá ser re-levantada.
Raise com um único argumento
Caso o raise for chamdo com um único argumento (objeto exception) ele irá levantar essa exceção.( Não é um forma muito comun se fazer o uso do raise)
Raise com uma sequencia de argumentos.
Neste caso será criado um objeto novo de exceção( RunTimeError), é uma forma comun de fazer o uso do raise.
Raise com objeto que possua uma exceção
Neste caso raise invoca tal método e levanta o objeto Exception que o mesmo retona.
OBS: A classe Exception possue um método exception, isso quer dizer que você pode especificar a classe do objeto para qualquer tipo de exceçao para ser o primeiro argumento do raise.
Raise com sequencia no terceiro argumento
Neste caso um array de sequências é especificado e então serão usados como o backtrace do objeto(exceção).
OBS: Caso este terceiro argumentos não esteja especificado raise ira usar Kernel caller no backtrace.
Exemplo 1:
Até agora falamos muito, mas não colocamos nada em código ainda, vamos fazer um exemplo bem simples para começar, meio batido mas vou usar o exemplo fatorial novamente.
1 2 3 4 5 6 7 8 9 10 | def fatorial(n) raise "Argumento invalido" if n < 1 return 1 if n == 1 n * fatorial(n-1) end puts fatorial(5) puts fatorial(4) puts fatorial(10) puts fatorial(-4) |
ruby ex.rb
120
24
3628800
ex.rb:2:in `fatorial’: Argumento invalido (RuntimeError)
from ex.rb:10
Este caso se encaixa no tipo de uso do raise um um único argumento, como dito antes temos outras formas de fazer a mesma coisa, por exemplo:
raise “Argumento invalido” if n < 1
pode ser perfeitamente substituido por:
raise RuntimeError "Argumento invalido" if n < 1
raise RuntimeError.new "Argumento invalido" if n < 1 ou
raise RuntimeError.exception "Argumento invalido" if n < 1
Claramente uma exceção RuntimeError não parece o mais apropriada para o caso do fatorial acima seria mais adequado usar um ArgumentError, ficando assim:
1 2 3 4 5 6 7 8 9 10 | def fatorial(n) raise ArgumentError if n < 1 return 1 if n == 1 n * fatorial(n-1) end puts fatorial(5) puts fatorial(4) puts fatorial(10) puts fatorial(-4) |
ruby ex.rb
120
24
3628800
ex.rb:2:in `fatorial’: ArgumentError (ArgumentError)
from ex.rb:10
ou até mesmo você pode personalizar um pouco essa mensagem, por exemplo fazendo o seguinte:
1 2 3 4 5 6 7 | def fatorial(n) raise ArgumentError, "Esperava um argumento >= 1. Mas retornou #{n}", caller if n < 1 return 1 if n == 1 n * fatorial(n-1) end puts fatorial(-4) |
ruby ex.rb
ex.rb:7: Esperava um argumento >= 1. Mas retornou -4 (ArgumentError)
Bom, bem melhor a mensagem assim ? O exemplo do fatorial é um código simples, você não irá querer escrever testes unitários para ele(devia), pois podemos pensar facilmente nos casos em que ele irá falhar, se o código fose algo mais complexo e com n casos seria muito útil termos testes para eles, mas será que esse código de fatorial está ok ? claramente vemos que ele simplesmente checa se o número é positivo, mas não chega o tipo, isso pode ser facilmente resolvido adicionando um outro raise, assim:
1 2 3 4 5 6 | def fatorial(n) raise TypeError, "Valor invalido, somente inteiros positivos" unless n.is_a? Integer raise ArgumentError, "Esperava um argumento >= 1. Mas retornou #{n}", caller if n < 1 return 1 if n == 1 n * fatorial(n-1) end |
E por curiosidade aqui vai um simples teste unitário para o código acima:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | require "test/unit" require "fatorial.rb" class TestLibraryFileName < Test::Unit::TestCase def test_simples_cases assert_equal(120, fatorial(5)) assert_equal(6, fatorial(3)) end def test_negative_numbers assert_raise(ArgumentError) {fatorial(-1)} end def test_invalid_argument_type assert_raise(TypeError) {fatorial("a")} end end |
Uma coisa que deve ser entendida aqui, é que muitas vezes exceçõe irão acontecer mesmo sem agente levanta-lás, ou seja, exceções no próprio código Ruby, então uma lição importante é saber manipula-lás mesmo sem agente nunca lenvantar sequer uma delas.
O manipulador de Execções rescue
O rescue é um declaração que deve ser usada em conjunto com a declaração begin, begin é um delimitador de bloco de código dentro das exceções sendo manipuladas(Vimos um pouco disso no post anterior), a declaração begin parece bom isso:
1 2 3 4 5 | begin #algum código rescue Exception => e #código da manipualação da exceção vem aqui, qualquer exceção levantada pelo código acima, cai aqui. end |
OBS: o Exception => e é opcional, mas logo vermos que ele é útil.e
Dando um nome para um Objeto de Exceção
Podemos usar váriaveis para atribui-las um objeto de exceção(Exception), por exemplo:
1 2 3 4 5 6 7 8 9 | def simple(n) raise ArgumentError, "Argumento Invalido" if n < 0 end begin simple(-4) rescue Exception => e puts "#{e.class}: #{e.message}" end |
=>ArgumentError: Argumento Invalido
Agora temos um váriavel global que faz referência ao objecto de exceção ( Exception ), essa váriavel é a $!, temos outras n variáveis globais, outra interessante é a $@ que retorna o local do último erro.
Uma coisa a ser notada aqui é que a váriavel e ficará disponível após o rescue ser finalizado, veja esse exemplo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | def simple(n) raise ArgumentError, "Argumento Invalido" if n < 0 end begin simple(-4) rescue Exception => e puts "Dentro do Rescue" puts "#{e.class}: #{e.message}" puts $! puts "Erro em #{$@}" end puts "" puts "Fora do Rescue" puts $! puts e |
Exceções por Tipo
As execções mostras acima com o uso do rescue trata exceções StandardError, ou suas subclasses, outras exceções que não tiverem dentro dessa hierarquia sera ignorado, então como podem tratar qualquer tipo de exceções ? Simples, faça o seguinte:
Resuce Exception
Agora você deve saber que para manipular um ArgumentError e atribuir isso para e, é simples:
Rescue ArgumentError => ex
E para definir mais que um tipo? Simples, simplesmente separe por virgula(,) elas.
Então vemos um exemplo de como tratar duas exceções, ArgumentError, e TypeError, mas queremos tratas ambas de forma diferentes.
1 2 3 4 5 6 7 | begin x = fatorial("f") rescue ArgumentError => e puts "Numero >= que 1 eh preciso" rescue TypeError => e puts "Tente com intiero" end |
Bom pessoal essa foi a primeira parte sobre como tratar exceções no Ruby, na segunda parte iremos ver sobre propagação de exceções, exceções que ocorrem dentro de exceções e outras coisas mais.
Espero que tenham gostado e comentem