Sobrecarga e argumentos marotos nos métodos Ruby
Continuando com o assunto de métodos em ruby, vamos falar sobre sobrecarga e argumentos. Para isso imagine um método para fazer uma saudação para alguma pessoa do tipo: Bom Dia Jõao, Boa Tarde Maria, etc…
def seja_educado(saudacao, pessoa) puts "#{saudacao} #{pessoa}" end seja_educado("Bom Dia","Iuri") => Bom Dia Iuri
Essa foi muito fácil! Afinal de contas já sabemos “tudo” sobre criar métodos em ruby.
Então vamos tornar o exemplo mais interessante. Eu quero poder usar o metodo seja_educado das seguintes formas:
seja_educado("Boa Tarde","Joao") => Boa Tarde Joao seja_educado("Joao") => Olá Joao
Uma típica solução seria usar o velho truque do metodo sobrecarregado então vamos tentar.
def seja_educado(saudacao, pessoa) puts "#{saudacao} #{cidadao}" end def seja_educado(pessoa) puts "Ola #{pessoa}" end seja_educado("Joao") =>Olá Joao seja_educado("Bom dia", "Luiz") => ArgumentError: wrong number of arguments (2 for 1)
Porque este erro meu Deus? Porque?
Bem, a triste verdade é que Ruby não da suporte sobrecarga de métodos. Quando escrevemos o metodo pela seja_educado pela segunda vez, na verdade o que fizemos foi redefini-lo e nao criar um outro metodo de mesmo nome. Por isso, quando executamos: seja_educado(“Boa dia”,”Luiz”) obtivemos o erro relacionado ao numero argumentos.
Tem algum jeito de resolver isso?
Sim, a falta de sobrecarga de métodos pode ser compensada se utilizarmos alguns recursos do ruby como por exemplo: o de valor default para argumentos. Vamos fazer com que o argumento saudacao receba a string “Ola” sempre que alguem “se esquecer” de informar um valor para ele. O código fica assim:
def seja_educado(saudacao="Ola", pessoa) puts "#{saudacao} #{pessoa}" end
Ao colocar o sinal de = e a string “Ola” ao lado do argumento saudacao dizemos que se ninguem mandar valor para este argumento, entao ele deverá assumir o valor “Ola”. Desta forma as chamadas abaixo fucionarão:
seja_educado("Iuri") => Ola Iuri seja_educado("Bom dia","Luiz") => Bom dia Luiz
Incrementando o Exemplo
Agora eu quero evoluir ainda mais o nosso metodo seja_educado. Eu quero ser capaz de informar zero, um ou vários nomes de pessoas para cumprimentar, ou seja, as seguintes chamadas de método devem ser suportadas:
seja_educado("Iuri") seja_educado("Bom dia", "Luiz") seja_educado("Bom dia", "Luiz", "Iuri", "Sergio")
Bom como já sabemos apenas as duas primeiras irão funcionar, já a última chamada vai ocasionar um ArgumentError. O problema agora é que a quantidade de pessoas que devem receber a saudacao pode variar de zero até qualquer número.
Como resolver isso?
Para nossa sorte ruby possui um tipo de argumento conhecido como opcional (optional argument), que muito se parece com o varargs do Java. Para usar este tipo de argumento, basta pôr um asterisco (estrela) na frente do argumento, após fazer isso seu argumento poderá receber zero ou N valores.
Vamos então aplicar o recurso de optional argument e fazer um teste:
def seja_educado(saudacao="Ola", *pessoas) if pessoas.empty? print "#{saudacao} " else pessoas.each { |p| print "#{saudacao} #{p} " } end end # Teste com varias pessoas seja_educado("Bom dia", "Luiz", "Iuri", "Sergio") =>Bom dia Luiz Bom dia Iuri Bom dia Sergio # Teste com apenas uma pessoa seja_educado("Bom dia","Joao") =>Bom dia Joao # Teste sem pessoas seja_educado("Bom dia") =>Bom dia
Tivemos que modificar um pouco o método, e acabamos usando o método each que pode receber blocos de código como parâmetro, se você não “pegou” bem essa parte de uma olhada aqui.
Agora sim hein…. Podemos usar o método seja_educado com nenhuma ou uma quantidade arbitrária de pessoas, como vimos no exemplo anterior.
Momento de Reflexão
Uma ideia me veio a cabeça agora. Repare que no método seja_educado, o argumento saudacao possui um valor default (padrão). O que pode acontecer se utilizarmos este método informando apenas o nome de uma pessoa, ou informando o nome de duas, ou quem sabe com três pessoas? Vamos ver:
#teste com apenas uma pessoa seja_educado("Luiz") =>Luiz #teste com duas pessoas seja_educado("Luiz","Iuri") =>Luiz Iuri #teste com tres pessoas seja_educado("Luiz","Iuri","Sergio") =>Luiz Iuri Luiz Sergio
Curioso! Em nehuma das chamadas anteriores o valor default do argumento saudacao foi usado. *(1)
E se tentarmos inverter a ordem dos argumentos? Vamos ver no que vai dar: *(2)
def seja_educado( *pessoas, saudacao="Ola" ) if pessoas.empty? print "#{saudacao} " else pessoas.each { |p| print "#{saudacao} #{p} " } end end # Agora nosso novo teste seja_educado("Luiz") =>syntax error, unexpected tIDENTIFIER, expecting tAMPER or '&' def seja_educado(*pessoas,saudacao="Ola")
Acho que não foi uma boa ideia inverter a ordem dos argumentos no metodo.
Ruby explica
Bem vamos com calma, a primeira coisa é que o Ruby tem basciamente 3 tipos de argumentos:
- Obrigatorios ou requeridos
- Com valores padrao ou default
- Opcionais
Como ruby associa os valores aos argumentos
Quando o ruby recebe uma chamada de método ele faz o seguinte divisão dos argumentos:
Primeiro, ele procura todos os argumentos requeridos/obrigatorios e associa valores a estes. Neste ponto se algum argumento obrigatório ficar sem valor, um erro do tipo ArgumentException será exibido.
Depois, se ainda restarem valores, estes serão aplicados aos argumentos default que existirem no método. E por último, ruby aplica os valores “que sobraram” ao argumento opcional, isso se ainda restar algum valor.
Pra entender melhor vejamos uns exemplos:
def metodo_teste( a, b="oi", *c ) p "a = #{a}, b = #{b}, c = #{c}" end metodo_teste( 1,2,3 ) => "a = 1, b = 2, c = 3" metodo_teste( 1,2,3,4,5 ) => "a = 1, b = 2, c = 345" metodo_teste( 1,2 ) => "a = 1, b = 2, c = " metodo_teste( 1 ) => "a = 1, b = oi, c = "
Além disso, ruby impõe restrições quanto utilizacao dos argumentos dentro de um método.
A tabela abaixo vai nos ajudar a enteder melhor esta ordem, ela lista as possiblidades válidas de combinação e uso dos diferentes tipos de argumentos ruby:
| Tipo(s) de Argumento | Exemplo de método | Obs |
|---|---|---|
| Obrigatorio | def teste(a,b,c) | |
| Default | def teste(a=1,b=2) | |
| Opcional | def teste(*a) | |
| Obrigatorio/Opcional | def teste(a,*b) | |
| Obrigatorio/Default | def teste(a,b=3) | |
| Obrigatorio/Default/Opcional | def teste(a,b=3,*c) | |
| Obrigatorio/Default/Opcional/Obrigatorio | def teste(a,b=3,*c,d) | válido apenas a partir da versao 1.9.1 |
Então…
Conseguimos compreender o que aconteceu nos pontos *(1) e *(2) deste post. No trecho de *(1), vimos que a estratégia ruby para aplicação de valores à argumentos, foi a responsável por aquela “pegadinha”. Já no ponto *(2), uma violação cometida por nós, da regra de utlização dos argumentos ruby em métodos foi a causa do erro.
Enfim, conhecer a linguagem é de fundamental importância para o programador, não cair em “pegadinhas” como estas.
Abraços, e até a próxima.
Sergio Junior.
Referências:
Writing own ruby Methods
Ruby Overloading Methods
The Well-Grounded Rubyist (Book)
Olá
Gostei do post, muito interessante, mas eu testei essa (“def teste(a,b=3,*c,d)”) declaracao de método que está na tabela e deu erro.
Será que fiz alguma coisa errada?
Abraços
Obrigado Richard, na verdade vc nao fez nada de errado. O que acontece é que este tipo de uso de argumentos de métodos, são validos apenas a partir da versao 1.9.1 do ruby. Se vc estiver usando uma versao anterior a esta este exemplo vai falhar msm. Coloquei uma observacao no post sobre isso.
Obrigado.
Ótimo post, parabéns pela didática
Muito didático.
Eu li esse texto antes de tentar fazer na pratica a sobrecarga em ruby e não entendi bem a idéia(li e nada ficou gravado rs).
Ontem tava fazendo uns exemplos em ruby e tentei usar a sobrecarga igual uso no java e não entendia o q poderia ta errado. Li novamente esse texto e agora ficou bem claro. vlw