É ‘eval’ suposto ser desagradável?

Eu tenho usado o recurso eval do ruby ​​muitas vezes. Mas ouvi pessoas dizendo que as avaliações são desagradáveis. Quando perguntado, por que e como, eu nunca consegui uma razão convincente para não usá-lo. Eles são realmente desagradáveis? Se sim, de que maneira? Quais são as opções possíveis “mais seguras” para avaliar?

Se você estiver eval uma string submetida ou modificável pelo usuário, isso equivale a permitir a execução de código arbitrário. Imagine se a string contivesse uma chamada do SO para rm -rf / ou similar. Dito isto, em situações em que você sabe que as strings são apropriadamente restritas, ou seu interpretador Ruby é protegido adequadamente, ou idealmente ambos, o eval pode ser extraordinariamente poderoso.

O problema é análogo à injeção de SQL , se você estiver familiarizado. A solução aqui é semelhante à solução para o problema de injeção (consultas parametrizadas). Ou seja, se as declarações que você gostaria de eval são conhecidas por serem de uma forma muito específica, e nem toda a declaração precisar ser enviada pelo usuário, apenas algumas variables, uma expressão matemática ou similar, você pode considerar esses pequenos pedaços do usuário, higienize-os, se necessário, e avalie a declaração do modelo seguro com a input do usuário conectada nos locais apropriados.

Em Ruby existem vários truques que podem ser mais apropriados que eval() :

  1. #send que permite chamar um método cujo nome você tenha como string e passar parâmetros para ele.
  2. yield permite que você passe um bloco de código para um método que será executado no contexto do método de recebimento.
  3. Muitas vezes, o simples Kernel.const_get("String") é suficiente para obter a class cujo nome você tem como string.

Eu acho que não sou capaz de explicá-los corretamente em detalhes, então eu apenas dei-lhe as dicas, se você estiver interessado, você vai google.

eval não é apenas inseguro (como foi apontado em outros lugares), também é lento. Toda vez que ele é executado, o AST do código eval precisa ser analisado novamente (e por exemplo, JRuby, convertido para bytecode), o que é uma operação de cadeia pesada e provavelmente também é ruim para a localidade do cache (sob a suposição de que o programa em execução não é muito importante, e as partes correspondentes do interpretador são, portanto, cache-cold, além de serem grandes).

Por que há eval em Ruby, você pergunta? “Porque nós podemos” principalmente – Na verdade, quando eval foi inventado (para a linguagem de programação LISP), foi principalmente para mostrar ! Mais eval , usar eval é The Right Thing quando você deseja “adicionar um interpretador ao seu interpretador”, para tarefas de metaprogramação, como escrever um pré-processador, um depurador ou um mecanismo de modelagem. A idéia comum para tais aplicativos é massagear alguns códigos em Ruby e chamar eval , e com certeza é melhor reinventar e implementar uma linguagem de brinquedo específica a um domínio, uma armadilha também conhecida como Décima Regra de Greenspun . As advertências são: cuidado com os custos, por exemplo, para um mecanismo de modelagem, fazer toda a sua avaliação no tempo de boot e não no tempo de execução; e não eval código não confiável a menos que você saiba como “domá-lo”, ou seja, selecionar e impor um subconjunto seguro da linguagem de acordo com a teoria da disciplina de capacidade . O último é um trabalho muito difícil (veja, por exemplo, como isso foi feito para o Java ; infelizmente não estou ciente desse esforço para o Ruby).

Isso torna a debugging difícil. Isso dificulta a otimização. Mas acima de tudo, é geralmente um sinal de que existe uma maneira melhor de fazer o que você está tentando fazer.

Se você nos disser o que está tentando realizar com o eval , poderá obter respostas mais relevantes relacionadas ao seu cenário específico.

Eval é um recurso incrivelmente poderoso que deve ser usado com cuidado. Além dos problemas de segurança apontados por Matt J, você também descobrirá que o código avaliado em tempo de execução de debugging é extremamente difícil. Um problema em um bloco de código avaliado em tempo de execução será difícil para o intérprete expressar – então, procurar por ele será difícil.

Dito isto, se você estiver confortável com esse problema e não estiver preocupado com o problema de segurança, não deverá evitar o uso de um dos resources que torna o Ruby tão atraente quanto ele é.

Em certas situações, um teste bem colocado é inteligente e reduz a quantidade de código necessária. Além das preocupações de segurança mencionadas por Matt J, você também precisa se fazer uma pergunta bem simples:

Quando tudo estiver dito e feito, alguém pode ler seu código e entender o que você fez?

Se a resposta for não, então o que você ganhou com um eval é abandonado para manutenção. Esse problema não é aplicável apenas se você trabalha em equipe, mas também se aplica a você – você quer poder olhar para seus meses de código, se não anos a partir de agora, e saber o que você fez.

Se você está passando qualquer coisa que você recebe do “fora” para eval , você está fazendo algo errado, e é muito desagradável. É muito difícil escaping do código o suficiente para que seja seguro, então eu considero bastante inseguro. No entanto, se você estiver usando o eval para evitar duplicações ou outras coisas semelhantes, como o exemplo de código a seguir, não há problema em usá-lo.

 class Foo def self.define_getters(*symbols) symbols.each do |symbol| eval "def #{symbol}; @#{symbol}; end" end end define_getters :foo, :bar, :baz end 

No entanto, pelo menos no Ruby 1.9.1, o Ruby tem methods realmente poderosos de meta-programação, e você pode fazer o seguinte:

 class Foo def self.define_getters(*symbols) symbols.each do |symbol| define_method(symbol) { instance_variable_get(symbol) } end end define_getters :foo, :bar, :baz end 

Para a maioria das finalidades, você deseja usar esses methods e nenhum escape é necessário.

A outra coisa ruim sobre eval é o fato de que (pelo menos em Ruby), é bem lento, já que o interpretador precisa analisar a string, e então executar o código dentro da binding atual. Os outros methods chamam a function C diretamente e, portanto, você deve obter um aumento considerável de velocidade.