How to dynamically create a local variable?

后端 未结 4 2161
温柔的废话
温柔的废话 2020-11-22 03:46

I have a variable var = \"some_name\" and I would like to create a new object and assign it to some_name. How can I do it? E.g.

var         


        
相关标签:
4条回答
  • 2020-11-22 04:25

    You cannot dynamically create local variables in Ruby 1.9+ (you could in Ruby 1.8 via eval):

    eval 'foo = "bar"'
    foo  # NameError: undefined local variable or method `foo' for main:Object
    

    They can be used within the eval-ed code itself, though:

    eval 'foo = "bar"; foo + "baz"'
    #=> "barbaz"
    

    Ruby 2.1 added local_variable_set, but that cannot create new local variables either:

    binding.local_variable_set :foo, 'bar'
    foo # NameError: undefined local variable or method `foo' for main:Object
    

    This behavior cannot be changed without modifying Ruby itself. The alternative is to instead consider storing your data within another data structure, e.g. a Hash, instead of many local variables:

    hash = {}
    hash[:my_var] = :foo
    

    Note that both eval and local_variable_set do allow reassigning an existing local variable:

    foo = nil
    eval 'foo = "bar"'
    foo  #=> "bar"
    binding.local_variable_set :foo, 'baz'
    foo  #=> "baz"
    
    0 讨论(0)
  • 2020-11-22 04:35

    Speaking of ruby 2.2.x it is true that you can't create local variables programatically in current context/binding.. but you can set variables in some particular binding you have a handle of.

    b = binding
    b.local_variable_set :gaga, 5
    b.eval "gaga"
    => 5
    

    Interesting here is that calls to binding give you a new binding each time. So you need to get a handle of the binding you are interested in and then eval in it's context once desired variables are set.

    How is this useful? For example I want to evaluate ERB and writing ERB is much nicer if you can use <%= myvar %> instead of <%= opts[:myvar] %> or something like that.

    To create a new binding I'm using a module class method (I'm sure somebody will correct me how to call this properly, in java I'd call it a static method) to get a clean binding with particular variables set:

    module M
      def self.clean_binding
        binding
      end
    
      def self.binding_from_hash(**vars)
        b = self.clean_binding
        vars.each do |k, v|
          b.local_variable_set k.to_sym, v
        end
        return b
      end
    end
    my_nice_binding = M.binding_from_hash(a: 5, **other_opts)
    

    Now you have a binding with only the desired variables. You can use it for nicer controlled evaluation of ERB or other (possibly third party) trusted code (this is not a sandbox of any kind). It's like defining an interface.

    update: A few additional notes about bindings. Place you create them also affects the availability of methods and Constants resolution. In the above example I create a reasonably clean binding. But if I want to make available the instance methods of some object, I could create a binding by a similar method but within the class of that object. e.g.

    module MyRooTModule
      class Work
        def my_instance_method
          ...
        end
        def not_so_clean_binding
          binding
        end
      end
      class SomeOtherClass
      end
    end
    

    Now my my_object.not_so_clean_binding will allow code to call #my_instance_method on my_object object. In the same way, you can call for example SomeOtherClass.new in code using this binding instead of MyRootModule::SomeOtherClass.new. So there is sometimes more consideration needed when creating a binding than just local variables. HTH

    0 讨论(0)
  • 2020-11-22 04:38

    It is true what others wrote that you cannot dynamically declare true variable in a local context. However you can achieve similar functionality with object attributes and since in the Ruby world everything is an object (even main context) you can easily extend those objects with new attributes. Of corse, this operation can be done dynamically. Let's examine this approach.

    Firstly, let's look at the main scope with irb.

    > self
    => main
    > self.class
    => Object
    > self.class.ancestors
    => [Object, Kernel, BasicObject]
    

    As you can see now, main is truly an object. Objects can have attributes which have same indirection property as variables. Normally, when declaring new class we would use attr_accessor method but main is already an instantiated object thus we cannot declare new attributes directly. Here module mixins come for rescue.

    variable_name = 'foo'
    variable_value = 'bar'
    
    variable_module = Module.new do
      attr_accessor variable_name.to_sym
    end
    
    include variable_module
    
    instance_variable_set("@#{variable_name}", variable_value)
    
    p foo # "bar"
    
    self.foo = 'bad'
    
    p foo # "baz"
    
    self.class.ancestors
    # [Object, #<Module:0x007f86cc073aa0>, Kernel, BasicObject]
    

    Now you see that main object was tainted with new module that introduced new attribute foo. For further inspection you can run methods to see that main now have two more methods foo and foo=.

    To simplify this operation I wrote metaxa gem which I highly encourage you to check out. This is example of how to use it.

    require 'metaxa'
    
    include Metaxa
    
    introduce :foo, with_value: 'foo'
    
    puts foo == 'foo' # true
    puts foo === get(:foo) # true
    
    set :foo, 'foobar'
    
    puts foo == 'foobar' # true
    puts foo === get(:foo) # true
    
    self.foo = 'foobarbaz'
    
    puts foo == 'foobarbaz' # true
    puts foo === get(:foo) # true     
    
    0 讨论(0)
  • 2020-11-22 04:41

    Although, as others have pointed out, you cannot dynamically create local variables in Ruby, you can simulate this behavior to some degree using methods:

    hash_of_variables = {var1: "Value 1", var2: "Value 2"}
    
    hash_of_variables.each do |var, val|
      define_method(var) do
        instance_variable_get("@__#{var}")
      end
      instance_variable_set("@__#{var}", val)
    end
    
    puts var1
    puts var2
    var1 = var2.upcase
    puts var1
    

    Prints:

    Value 1
    Value 2
    VALUE 2
    

    Some libraries combine this technique with instance_exec to expose what appear to be local variables inside a block:

    def with_vars(vars_hash, &block)
      scope = Object.new
      vars_hash.each do |var, val|
        scope.send(:define_singleton_method, var) do
          scope.instance_variable_get("@__#{var}")
        end
        scope.instance_variable_set("@__#{var}", val)
      end
      scope.instance_exec(&block)
    end
    
    with_vars(a: 1, b:2) do
      puts a + b
    end
    

    Prints: 3

    Be aware though that the abstraction is by no means perfect:

    with_vars(a: 1, b:2) do
      a = a + 1
      puts a
    end
    

    Results in: undefined method `+' for nil:NilClass. This is because a= defines an actual local variable, initialized to nil, which takes precedence over the method a. Then a.+(1) gets called, and nil doesn't have a + method, so an error is thrown.

    So while this method is pretty useful for simulating read-only local variables, it doesn't always work well when you try to reassign the variable inside the block.

    0 讨论(0)
提交回复
热议问题