2010-01-08 5 views
4

En essayant de rafraîchir mes compétences Ruby je continue à courir à travers ce cas dont je ne peux pas trouver une explication en lisant simplement les documents de l'API. Une explication serait grandement appréciée. Voici l'exemple de code:Comment les variables sont-elles liées au corps d'un define_method?

for name in [ :new, :create, :destroy ] 
    define_method("test_#{name}") do 
    puts name 
    end 
end 

Ce que je veux/vous attendre à arriver est que la variable name sera lié au bloc donné à define_method et que lorsque #test_new est appelée sortie sera « nouvelle ». A la place, chaque méthode définie sort "destroy" - la dernière valeur affectée à la variable name. Qu'est-ce que j'ai mal compris à propos de define_method et de ses blocs? Merci!

+0

Maintenant, je pense que je devrais attendre que define_method fonctionne comme le mot-clé def, auquel cas le bloc donné à define_method ne peut fonctionner qu'avec des variables locales ou passées comme arguments. – Chris

Répondre

6

Les blocs en Ruby sont des fermetures: le bloc que vous passez à define_method capture lui-même la variable name - et non sa valeur - afin qu'elle reste dans la portée à chaque fois que ce bloc est appelé. C'est la première pièce du puzzle.

Le deuxième élément est que la méthode définie par define_methodest le bloc lui-même. Fondamentalement, il convertit un objet Proc (le bloc qui lui est passé) en un objet Method et le lie au récepteur.

Alors, que vous vous retrouvez avec est une méthode qui a capturé (est fermé sur) la variable name, qui par le temps que votre boucle Complètes est réglé sur :destroy.

Addition: La construction for ... in crée en fait une nouvelle variable locale, la construction [ ... ].each {|name| ... } correspondant ne pas faire. Autrement dit, votre boucle for ... in est équivalente à la suivante (en Ruby 1.8 de toute façon):

name = nil 
[ :new, :create, :destroy ].each do |name| 
    define_method("test_#{name}") do 
    puts name 
    end 
end 
name # => :destroy 
+1

Je pense que je comprends maintenant. On dirait que vous avez trouvé la source de la confusion, bien que sans la nommer explicitement: le problème n'est pas tellement que le bloc passé à 'define_method' est une fermeture (en fait, c'est * précisément * ce que l'OP veut), mais plutôt que le corps d'une expression 'for' n'est * pas * une fermeture. Une expression 'for' crée une nouvelle variable * locale * * dans * la portée dans laquelle l'expression' for' est définie. L'itérateur 'each' OTOH prend un bloc, ce qui est bien sûr une fermeture, ce qui signifie que maintenant' name' fait référence à l'instance spécifique de la variable de bloc 'name' * pour cette itération * –

+0

Oh, true. Et en fait, la variable créée par l'expression 'for ... in' est toujours dans la portée * après * la boucle, pas seulement dans le corps de la boucle (comme je le teste dans' irb'). – Kevin

0

Le problème ici est que for expressions de boucle ne créent pas une nouvelle portée. Les seules choses qui créent de nouvelles portées dans Ruby sont les corps de script, les corps de module, les corps de classe, les corps de méthode et les blocs.

Si vous regardez réellement le comportement des for expressions en boucle dans le projet Ruby ISO spécifications, vous constaterez que l'expression de la boucle for est exécuté exactement comme un each iterator sauf le fait qu'il ne crée pas une nouvelle portée.

Aucun Rubyist ne jamais utiliser une boucle for, de toute façon: ils utiliseraient un itérateur à la place, qui -t prendre un bloc et crée ainsi une nouvelle portée.

Si vous utilisez un iterator idiomatiques, tout fonctionne comme prévu:

class Object 
    %w[new create destroy].each do |name| 
    define_method "test_#{name}" do 
     puts name 
    end 
    end 
end 

require 'test/unit' 
require 'stringio' 
class TestDynamicMethods < Test::Unit::TestCase 
    def setup; @old_stdout, $> = $>, (@fake_logdest = StringIO.new) end 
    def teardown; $> = @old_stdout end 

    def test_that_the_test_create_method_prints_create 
    Object.new.test_create 
    assert_equal "create\n", @fake_logdest.string 
    end 
    def test_that_the_test_destroy_method_prints_destroy 
    Object.new.test_destroy 
    assert_equal "destroy\n", @fake_logdest.string 
    end 
    def test_that_the_test_new_method_prints_new 
    Object.new.test_new 
    assert_equal "new\n", @fake_logdest.string 
    end 
end 
+2

"No Rubyist would ever ...." est une affirmation sans fondement. Beaucoup d'entre nous trouvent des boucles beaucoup plus sémantiques et concises que "chacun" des blocs. – scrozier

1
for name in [ :new, :create, :destroy ] 
    local_name = name 
    define_method("test_#{local_name}") do 
    puts local_name 
    end 
end 

Cette méthode se comportera comme prévu. La raison de la confusion est que 'nom' n'est pas créé une fois par itération de la boucle for. Il est créé une fois et incrémenté. De plus, si je comprends bien, les définitions de méthodes ne sont pas des fermetures comme les autres blocs. Ils conservent une visibilité variable, mais ne se referment pas sur la valeur actuelle des variables.

+1

Un regard en profondeur sur le même problème (et la solution) en C#: http://blogs.msdn.com/ericlippert/archive/2009/11/12/closing-over-the-loop-variable-considered-harmful .aspx – kejadlen