Pass arguments by reference to a block with the sp

2019-02-24 21:41发布

问题:

It seems that the arguments are copied when using the splat operator to pass arguments to a block by reference.

I have this:

def method
  a = [1,2,3]
  yield(*a)
  p a
end

method {|x,y,z| z = 0}
#=> this puts and returns [1, 2, 3] (didn't modified the third argument)

How can I pass these arguments by reference? It seems to work if I pass the array directly, but the splat operator would be much more practical, intuitive and maintainable here.

回答1:

  1. In Ruby when you write x = value you are creating a new local variable x whether it existed previously or not (if it existed the name is simply rebound and the original value remains untouched). So you won't be able to change a variable in-place this way.

  2. Integers are immutable. So if you send an integer there is no way you can change its value. Note that you can change mutable objects (strings, hashes, arrays, ...):

    def method
      a = [1, 2, "hello"]
      yield(*a)
      p a
    end
    
    method { |x,y,z| z[1] = 'u' }
    # [1, 2, "hullo"]
    

Note: I've tried to answer your question, now my opinion: updating arguments in methods or blocks leads to buggy code (you have no referential transparency anymore). Return the new value and let the caller update the variable itself if so inclined.



回答2:

The problem here is the = sign. It makes the local variable z be assigned to another object.

Take this example with strings:

def method
  a = ['a', 'b', 'c']
  yield(*a)
  p a
end

method { |x,y,z| z.upcase! }   # => ["a", "b", "C"]

This clearly shows that z is the same as the third object of the array.

Another point here is your example is numeric. Fixnums have fixed ids; so, you can't change the number while maintaining the same object id. To change Fixnums, you must use = to assign a new number to the variable, instead of self-changing methods like inc! (such methods can't exist on Fixnums).



回答3:

Yes... Array contains links for objects. In your code when you use yield(*a) then in block you works with variables which point to objects which were in array. Now look for code sample:

daz@daz-pc:~/projects/experiments$ irb
irb(main):001:0> a = 1
=> 1
irb(main):002:0> a.object_id
=> 3
irb(main):003:0> a = 2
=> 2
irb(main):004:0> a.object_id
=> 5

So in block you don't change old object, you just create another object and set it to the variable. But the array contain link to the old object.

Look at the debugging stuff:

def m
  a = [1, 2]
  p a[0].object_id
  yield(*a)
  p a[0].object_id
end

m { |a, b| p a.object_id; a = 0; p a.object_id }

Output:

3
3
1
3


回答4:

How can I pass these arguments by reference?

You can't pass arguments by reference in Ruby. Ruby is pass-by-value. Always. No exceptions, no ifs, no buts.

It seems to work if I pass the array directly

I highly doubt that. You simply cannot pass arguments by reference in Ruby. Period.



标签: ruby splat