Is the ruby operator ||= intelligent?

前端 未结 5 1994
长发绾君心
长发绾君心 2020-11-29 10:30

I have a question regarding the ||= statement in ruby and this is of particular interest to me as I\'m using it to write to memcache. What I\'m wondering is, does ||= check

相关标签:
5条回答
  • 2020-11-29 10:36
    CACHE[:some_key] ||= "Some String"
    

    is equivalent to

    CACHE[:some_key] = "Some String" unless CACHE[:some_key]
    

    (which is equivalent to if + nil? unless CACHE[:some_key] is a boolean value).

    In other words: yes, ||= will only write if the LHS is nil or false.

    0 讨论(0)
  • 2020-11-29 10:40

    Here's another demonstration that's a bit different than the other answers in that it explicitly shows when the Hash is being written to:

    class MyHash < Hash
      def []=(key, value)
        puts "Setting #{key} = #{value}"
        super(key, value)
      end
    end
    
    >> h = MyHash.new
    => {}
    >> h[:foo] = :bar
    Setting foo = bar
    => :bar
    >> h[:bar] ||= :baz
    Setting bar = baz
    => :baz
    >> h[:bar] ||= :quux
    => :baz
    

    And by way of comparison:

    // continued from above
    >> h[:bar] = h[:bar] || :quuux
    Setting bar = baz
    => :baz
    
    0 讨论(0)
  • 2020-11-29 10:44

    This is extremely easy to test:

    class MyCache
      def initialize
        @hash = {}
      end
    
      def []=(key, value)
        puts "Cache key '#{key}' written"
        @hash[key] = value
      end
    
      def [](key)
        puts "Cache key '#{key}' read"
        @hash[key]
      end
    end
    

    Now simply try the ||= syntax:

    cache = MyCache.new
    cache["my key"] ||= "my value"  # cache value was nil (unset)
    # Cache key 'my key' read
    # Cache key 'my key' written
    
    cache["my key"] ||= "my value"  # cache value is already set
    # Cache key 'my key' read
    

    So we can conclude that no assignment takes place if the cache key already exists.

    The following extract from the Rubyspec shows that this is by design and should not be dependent on the Ruby implementation:

    describe "Conditional operator assignment 'obj.meth op= expr'" do
      # ...
      it "may not assign at all, depending on the truthiness of lhs" do
        m = mock("object")
        m.should_receive(:foo).and_return(:truthy)
        m.should_not_receive(:foo=)
        m.foo ||= 42
    
        m.should_receive(:bar).and_return(false)
        m.should_not_receive(:bar=)
        m.bar &&= 42
      end
      # ...
    end
    

    In the same file, there is a similar spec for [] and []= that mandates identical behaviour.

    Although the Rubyspec is still a work in progress, it has become clear that the major Ruby implementation projects intend to comply with it.

    0 讨论(0)
  • 2020-11-29 10:48

    [I removed my example that was less accurate than other's. I leave my answer for the benchmarks that might be interesting to some. My point was:]

    So basically

    CACHE[:some_key] ||= "Some String"
    

    is the same as

    CACHE[:some_key] = "Some String" unless CACHE[:some_key]
    

    I'm more for the first syntax, but then it's up to you since readibility is a bit reduced in that case.


    I was curious, so here's some benchmarks:

    require "benchmark"
    CACHE = {}
    Benchmark.bm do |x|
      x.report { 
        for i in 0..100000
          CACHE[:some_key] ||= "Some String" 
        end
      }
      x.report { 
        for i in 0..100000
          CACHE[:some_key] = "Some String" unless CACHE[:some_key] 
        end
      }
    end
    
    
          user     system      total        real
      0.030000   0.000000   0.030000 (  0.025167)
      0.020000   0.000000   0.020000 (  0.026670)
    
    0 讨论(0)
  • 2020-11-29 10:51

    According to §11.3.1.2.2 of the Draft ISO Specification,

    CACHE[:some_key] ||= "Some String"
    

    expands to

    o = CACHE
    *l = :some_key
    v = o.[](*l)
    w = "Some String"
    x = v || w
    l << x
    o.[]=(*l)
    x
    

    Or, in the more general case

    primary_expression[indexing_argument_list] ω= expression
    

    (I am using ω here to denote any operator, so it could be ||=, +=, *=, >>=, %=,…)

    Expands to:

    o = primary_expression
    *l = indexing_argument_list
    v = o.[](*l)
    w = expression
    x = v ω w
    l << x
    o.[]=(*l)
    x
    

    So, according to the specification, []= will always get called. But that is actually not the case in current implementations (I tested MRI, YARV, Rubinius, JRuby and IronRuby):

    def (h = {}).[]=(k, v) p "Setting #{k} to #{v}"; super end
    h[:key] ||= :value # => :value
    # "Setting key to value"
    h[:key] ||= :value # => :value
    

    So, obviously either the specification is wrong or all five currently released implementations are wrong. And since the purpose of the specification is to describe the behavior of the existing implementations, it's obviously that the specification must be wrong.

    In general, as a first approximation

    a ||= b
    

    expands to

    a || a = b
    

    However, there's all kinds of subleties involved, for example, whether or not a is undefined, whether a is a simple variable or a more complex expression like foo[bar] or foo.bar and so on.

    See also some of the other instances of this same question, that have already been asked and answered here on StackOverflow (for example, this one). Also, the question has been discussed so many times on the ruby-talk mailinglist, that there are now discussion threads whose sole purpose it is to summarize the other discussion threads. (Although please note that that list is far from complete.)

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