do..end vs chaves para blocos em Ruby


154

Eu tenho um colega de trabalho que está ativamente tentando me convencer de que não devo usar do..end e, em vez disso, use chaves para definir blocos de múltiplas linhas no Ruby.

Estou firmemente acostumado a usar apenas chaves para uma linha curta e fazer ... terminar para todo o resto. Mas pensei em alcançar a comunidade maior para obter alguma resolução.

Então, qual é e por quê? (Exemplo de algum código deveria)

context do
  setup { do_some_setup() }
  should "do somthing" do
    # some more code...
  end
end

ou

context {
  setup { do_some_setup() }
  should("do somthing") {
    # some more code...
  }
}

Pessoalmente, basta olhar para as respostas acima para mim, mas eu queria abrir isso para uma comunidade maior.


3
Apenas imaginando, mas quais são os argumentos de seus colegas de trabalho para usar aparelho? Parece mais uma preferência pessoal do que uma coisa lógica.
Daniel Lee

6
Se você quiser uma discussão, torne-a um wiki da comunidade. Para sua pergunta: é apenas estilo pessoal. Eu prefiro o aparelho encaracolado, pois eles parecem mais nerds.
Halfdan

7
Não é uma preferência, existem razões sintáticas para usar uma sobre a outra, além de razões estilísticas. Várias das respostas explicam o porquê. Deixar de usar o correto pode causar bugs muito sutis, difíceis de encontrar, se outra escolha "estilística" for feita para nunca usar parênteses de quebra automática para métodos.
the Tin Man

1
As "condições de borda" têm o mau hábito de espiar as pessoas que não sabem sobre elas. Codificar defensivamente significa muitas coisas, incluindo a decisão de usar estilos de codificação que minimizem a chance de encontrar os casos. Você pode estar ciente, mas o cara que duas pessoas depois de você pode não estar depois que o conhecimento tribal foi esquecido. Infelizmente, isso costuma acontecer em ambientes corporativos.
the Tin Man

1
@tinman Esses tipos de condições de borda são tratados pelo TDD. É por isso que Rubist pode escrever um código como esse e ter uma noite inteira de descanso sabendo que esses erros não existem em seu código. Ao desenvolver com TDD ou BDD e um erro de precedência é cometido, o vermelho mostrado na tela é o que nos lembra o "conhecimento tribal". Normalmente, a solução é adicionar alguns parênteses em algum lugar que seja totalmente aceitável nas convenções padrão. :)
Blake Taylor

Respostas:


246

A convenção geral é usar do..end para blocos de várias linhas e chaves para blocos de linha única, mas também há uma diferença entre as duas que podem ser ilustradas com este exemplo:

puts [1,2,3].map{ |k| k+1 }
2
3
4
=> nil
puts [1,2,3].map do |k| k+1; end
#<Enumerator:0x0000010a06d140>
=> nil

Isso significa que {} tem uma precedência mais alta do que o final .., lembre-se disso ao decidir o que você deseja usar.

PS: Mais um exemplo a ter em mente enquanto você desenvolve suas preferências.

O código a seguir:

task :rake => pre_rake_task do
  something
end

realmente significa:

task(:rake => pre_rake_task){ something }

E este código:

task :rake => pre_rake_task {
  something
}

realmente significa:

task :rake => (pre_rake_task { something })

Portanto, para obter a definição real desejada, com chaves, você deve:

task(:rake => pre_rake_task) {
  something
}

Talvez o uso de chaves para parâmetros seja algo que você queira fazer de qualquer maneira, mas se não o fizer, provavelmente é melhor usar do..enviar nesses casos para evitar essa confusão.


44
Eu voto por sempre usar parênteses em torno dos parâmetros do método para contornar todo o problema.
the Tin Man

@ The Tin Man: Você ainda usa parênteses no Rake?
10138 Andrew Grimm

9
É um hábito de programação da velha escola para mim. Se um idioma suporta parênteses, eu os uso.
the Tin Man

8
+1 por apontar o problema de precedência. Às vezes, recebo exceções inesperadas quando tento converter o do; fim a {}
idrinkpabst 22/07/2013

1
Por que puts [1,2,3].map do |k| k+1; endapenas imprime o enumerador, ou seja, uma coisa. Não se coloca dois argumentos aqui, a saber, [1,2,3].mape do |k| k+1; end?
ben

55

De programação Ruby :

Suspensórios têm uma alta precedência; do tem uma baixa precedência. Se a chamada do método tiver parâmetros que não estejam entre parênteses, a forma entre parênteses de um bloco será vinculada ao último parâmetro, não à chamada geral. O formulário do será vinculado à invocação.

Então o código

f param {do_something()}

Vincula o bloco à paramvariável enquanto o código

f param do do_something() end

Vincula o bloco à função f.

No entanto, isso não é problema se você colocar argumentos de função entre parênteses.


7
+1 "No entanto, isso não é problema se você colocar argumentos de função entre parênteses.". Faço isso por uma questão de hábito, em vez de confiar no acaso e no capricho do intérprete. :-)
the Tin Man

4
@theTinMan, se o seu intérprete usa o acaso no analisador, você precisa de um novo intérprete.
Paul Draper

@PaulDraper - de fato, mas nem todos os idiomas concordam com esse problema. Mas (eu acho) é muito raro que parênteses não "façam seu trabalho", portanto, usar parênteses universalmente evita que você se lembre das decisões "aleatórias" tomadas pelos designers de linguagem - mesmo que sejam executadas fielmente pelos implementadores de compiladores e intérpretes .
dlu 5/03

15

Existem alguns pontos de vista sobre isso, é realmente uma questão de preferência pessoal. Muitos rubiistas adotam a abordagem que você faz. No entanto, dois outros estilos comuns são sempre usar um ou outro, ou {}para blocos que retornam valores e do ... endpara blocos executados para efeitos colaterais.


2
Não é apenas uma questão de preferência pessoal. A ligação de parâmetros pode causar erros sutis se os parâmetros de método não estiverem entre parênteses.
the Tin Man

1
No momento, não estou fazendo a última opção, mas estou marcando +1 por mencionar que algumas pessoas adotam essa abordagem.
Andrew Grimm

Avdi Grimm defende isso em uma postagem no blog: devblog.avdi.org/2011/07/26/…
alxndr

8

Há um grande benefício em chaves - muitos editores têm muito mais facilidade de combiná-las, facilitando muito certos tipos de depuração. Enquanto isso, a palavra-chave "do ... end" é um pouco mais difícil de corresponder, especialmente porque "end" s também corresponde a "if" s.


RubyMine por exemplo, destacar do ... endpalavras-chave por defeito
ck3g

1
Embora eu prefira chaves, não vejo razão para que um editor tenha problemas para encontrar uma sequência de caracteres de 2/3 contra um único caractere. Embora exista o argumento de que a maioria dos editores já combina com chaves, enquanto do ... endé um caso especial que requer lógica específica de idioma.
Chinoto Vokro 08/02

7

A regra mais comum que eu vi (mais recentemente no Eloquent Ruby ) é:

  • Se for um bloco de várias linhas, use do / end
  • Se for um bloco de linha única, use {}

7

Estou votando para fazer / terminar


A convenção é do .. endpara multilinhas e { ... }one-liners.

Mas eu gosto do .. endmais, então, quando tenho um liner, uso de do .. endqualquer maneira, mas formato-o normalmente para fazer / terminar em três linhas. Isso deixa todo mundo feliz.

  10.times do 
    puts ...
  end

Um problema { }é que ele é hostil ao modo de poesia (porque eles se ligam firmemente ao último parâmetro e não à chamada de método inteira, portanto você deve incluir parênteses de método) e eles, na minha opinião, não parecem tão agradáveis. Eles não são grupos de instruções e se chocam com constantes de hash para facilitar a leitura.

Além disso, já vi o suficiente { }em programas em C. O jeito de Ruby, como sempre, é melhor. Existe exatamente um tipo deif bloco e você nunca precisa voltar e converter uma instrução em uma instrução composta.


2
"Eu já vi bastante {} em programas C" ... e Perl. E +1 para várias declarações lá e por defender os estilos de codificação zen do Ruby. :-)
the Tin Man

1
A rigor, há outra sintaxe "se" - o postfix "if" ( do_stuff if x==1), que é meio irritante de se converter na sintaxe regular. Mas isso é outra discussão.
Kelvin

Existem prefixo if, postfix if, postfix unless(também um tipo de if, se não) e uma linha ifcom then. A maneira Ruby é ter várias maneiras de expressar de fato algo que é sempre a mesma coisa, muito pior do que o que C oferece, que segue regras muito simples e estritas.
Mecki

2
Então, se gostamos do {} em C, isso significa que também devemos usá-lo no Ruby?
Warren Dew

6

Alguns rubiistas influentes sugerem usar chaves quando você usa o valor de retorno e terminar / terminar quando não usa.

http://talklikeaduck.denhaven2.com/2007/10/02/ruby-blocks-do-or-brace (em archive.org)

http://onestepback.org/index.cgi/Tech/Ruby/BraceVsDoEnd.rdoc (em archive.org)

Parece uma boa prática em geral.

Eu modificaria um pouco esse princípio para dizer que você deve evitar o uso de do / end em uma única linha, porque é mais difícil de ler.

Você precisa ter mais cuidado ao usar chaves, porque ele se vincula ao parâmetro final de um método, em vez de à chamada de método inteira. Basta adicionar parênteses para evitar isso.


2

Na verdade, é uma preferência pessoal, mas tendo dito isso, nos últimos 3 anos das minhas experiências com ruby, o que aprendi é que o ruby ​​tem seu estilo.

Um exemplo seria, se você está vindo de um plano de fundo JAVA, para um método booleano, você pode usar

def isExpired
  #some code
end 

observe o caso camel e, na maioria das vezes, o prefixo 'is' para identificá-lo como um método booleano.

Mas no mundo do rubi, o mesmo método seria

def expired?
  #code
end

então, pessoalmente, acho que é melhor seguir o 'caminho do rubi' (mas sei que leva algum tempo para que alguém entenda (demorei cerca de 1 ano: D)).

Finalmente, eu iria com

do 
  #code
end

blocos.


2

Coloquei outra resposta, embora a grande diferença já tenha sido apontada (precedência / encadernação), e isso possa causar problemas difíceis de encontrar (o Homem de Lata e outros apontaram isso). Acho que meu exemplo mostra o problema com um trecho de código não tão comum, mesmo programadores experientes não lêem como os domingos:

module I18n
    extend Module.new {
        old_translate=I18n.method(:translate)
        define_method(:translate) do |*args|
            InplaceTrans.translate(old_translate, *args)
        end
        alias :t :translate
    }
end

module InplaceTrans
    extend Module.new {
        def translate(old_translate, *args)
            Translator.new.translate(old_translate, *args)
        end
    }
end

Então eu fiz algum código embelezando ...

#this code is wrong!
#just made it 'better looking'
module I18n
    extend Module.new do
        old_translate=I18n.method(:translate)
        define_method(:translate) do |*args|
            InplaceTrans.translate(old_translate, *args)
        end
        alias :t :translate
    end
end

se você alterar {}aqui para do/endobter o erro, esse método translatenão existe ...

Por que isso acontece é apontado aqui mais de uma precedência. Mas onde colocar aparelho aqui? (@ the Tin Man: eu sempre uso aparelho, como você, mas aqui ... supervisionado)

então cada resposta como

If it's a multi-line block, use do/end
If it's a single line block, use {}

está errado se usado sem o "MAS fique de olho nos aparelhos / precedência!"

novamente:

extend Module.new {} evolves to extend(Module.new {})

e

extend Module.new do/end evolves to extend(Module.new) do/end

(o que quer que o resultado da extensão faça com o bloco ...)

Portanto, se você quiser usar o / end use:

#this code is ok!
#just made it 'better looking'?
module I18n
    extend(Module.new do 
        old_translate=I18n.method(:translate)
        define_method(:translate) do |*args|
            InplaceTrans.translate(old_translate, *args)
        end
        alias :t :translate
    end)
end

1

No que diz respeito ao viés pessoal, prefiro chaves em vez de um bloco do / end, pois é mais compreensível para um número maior de desenvolvedores, porque a maioria das linguagens de segundo plano os usa durante a convenção do / end. Com isso dito, a chave real é chegar a um acordo dentro de sua loja, se do / end for usado por 6/10 desenvolvedores, que TODOS deveriam usá-los, se 6/10 usar chaves, então atenha-se a esse paradigma.

Trata-se de criar um padrão para que a equipe como um todo possa identificar as estruturas de código mais rapidamente.


3
É mais que preferência. A ligação entre os dois é diferente e pode causar problemas difíceis de diagnosticar. Várias das respostas detalham o problema.
the Tin Man

4
Em termos de pessoas de outros idiomas - acho que a diferença de "compreensibilidade" é tão pequena nesse caso que não merece consideração. (Isso não era o meu downvote btw.)
Kelvin

1

Há uma diferença sutil entre eles, mas {} se liga mais estreitamente que do / final.


0

Meu estilo pessoal é enfatizar a legibilidade sobre regras rígidas de {... }vs do... endescolha, quando essa escolha é possível. Minha idéia de legibilidade é a seguinte:

[ 1, 2, 3 ].map { |e| e + 1 }      # preferred
[ 1, 2, 3 ].map do |e| e + 1 end   # acceptable

[ 1, 2, 3 ].each_with_object [] do |e, o| o << e + 1 end # preferred, reads like a sentence
[ 1, 2, 3 ].each_with_object( [] ) { |e, o| o << e + 1 } # parens make it less readable

Foo = Module.new do     # preferred for a multiline block, other things being equal
  include Comparable
end

Foo = Module.new {      # less preferred
  include Comparable
}

Foo = Module.new { include Comparable }      # preferred for a oneliner
Foo = module.new do include Comparable end   # imo less readable for a oneliner

[ [ 1 ], [ 1, 2 ] ].map { |e| e.map do |e| e + 1 end }  # slightly better
[ [ 1 ], [ 1, 2 ] ].map { |e| e.map { |e| e + 1 } }     # slightly worse

Em sintaxe mais complexa, como blocos aninhados com várias linhas, tento intercalar {... }e do... enddelimitadores para obter o resultado mais natural, por exemplo.

Foo = Module.new { 
  if true then
    Bar = Module.new {                          # I intersperse {} and keyword delimiters
      def quux
        "quux".tap do |string|                  # I choose not to intersperse here, because
          puts "(#{string.size} characters)"    # for multiline tap, do ... end in this
        end                                     # case still loks more readable to me.
      end
    }
  end
}

Embora a falta de regras rígidas possa produzir escolhas ligeiramente diferentes para programadores diferentes, acredito que a otimização caso a caso para legibilidade, embora subjetiva, seja um ganho líquido sobre a aderência a regras rígidas.


1
Vindo de Python, talvez eu deva pular Ruby e aprender Wisp.
Cees Timmerman

Eu costumava acreditar que Rubyera a melhor linguagem pragmática do mercado. Devo dizer linguagem de script. Hoje, acho que você estaria igualmente bem aprendendo Brainfuck.
Boris Stitnicky

0

Tomei um exemplo da resposta mais votada aqui. Dizer,

[1,2,3].map do 
  something 
end

Se você verificar qual implementação interna no array.rb, o cabeçalho do método map diz :

Invokes the given block once for each element of self.

def map(*several_variants)
    (yield to_enum.next).to_enum.to_a
end

isto é, aceita um bloco de código - o que estiver entre do e end é executado como rendimento. Em seguida, o resultado será coletado como matriz novamente, retornando um objeto completamente novo.

Portanto, sempre que você encontrar um bloco do-end ou {} , basta ter um mapa mental de que o bloco de código está sendo passado como um parâmetro, que será executado internamente.


-3

Existe uma terceira opção: escreva um pré-processador para inferir "end" em sua própria linha, a partir do recuo. Os pensadores profundos que preferem código conciso estão certos.

Melhor ainda, corte o ruby ​​para que esta seja uma bandeira.

Obviamente, a solução mais simples de "escolher suas lutas" é adotar a convenção de estilo de que uma sequência de fins aparece na mesma linha e ensinar sua cor de sintaxe a silenciá-los. Para facilitar a edição, pode-se usar scripts de editor para expandir / recolher essas seqüências.

20% a 25% das minhas linhas de código ruby ​​são "fim" em sua própria linha, todas trivialmente inferidas por minhas convenções de indentação. Ruby é a linguagem do tipo lisp que alcançou o maior sucesso. Quando as pessoas contestam isso, perguntando onde estão todos os horríveis parênteses, mostro a eles uma função rubi que termina em sete linhas de "fim" supérfluo.

Eu codifiquei por anos usando um pré-processador lisp para inferir a maioria dos parênteses: Uma barra '|' abriu um grupo que foi fechado automaticamente no final da linha, e um cifrão '$' serviu como um espaço reservado vazio, onde de outra forma não haveria símbolos para ajudar a inferir os agrupamentos. É claro que este é um território de guerra religiosa. Lisp / esquema sem parênteses é o mais poético de todos os idiomas. Ainda assim, é mais fácil simplesmente acalmar os parênteses usando cores de sintaxe.

Eu ainda codigo com um pré-processador para Haskell, para adicionar heredocs e usar como padrão todas as linhas de descarga como comentários, tudo recuado como código. Não gosto de comentar os caracteres, seja qual for o idioma.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.