rails params[:id] 实现原理

Posted by wxianfeng Thu, 10 Feb 2011 16:36:00 GMT

环境: ruby1.9.2 + rails 3.0.3

我们知道 params 返回的是一个 hash , 例如 {"id"=>1} ,那为什么 params[:id] = 1 ,而不是 nil 呢 ?
irb下测试一下:

ruby-1.9.2-p0 > h={"id"=>1}
 => {"id"=>1} 
ruby-1.9.2-p0 > h[:id]
 => nil 

带着这个疑问,设置断点 ,debug 进rails 源码 , 发现了原因,

1,跟到了 params 方法源码:

def params
@_params ||= request.parameters
end

给 @_params 设置Watch , 发现如下 :

发现 @_params 的class 是 ActiveSupport::HashWithIndifferentAccess
也就是说

params.class # => ActiveSupport::HashWithIndifferentAccess

2,继续 F7 跟进去

跟到了这个文件 的 default方法

require 'active_support/core_ext/hash/keys'

# This class has dubious semantics and we only have it so that
# people can write params[:key] instead of params['key']
# and they get the same value for both keys.

module ActiveSupport
  class HashWithIndifferentAccess < Hash
    def extractable_options?
      true
    end

    def initialize(constructor = {})
      if constructor.is_a?(Hash)
        super()
        update(constructor)
      else
        super(constructor)
      end
    end

    def default(key = nil)
      if key.is_a?(Symbol) && include?(key = key.to_s)
        self[key]
      else
        super
      end
    end

    def self.new_from_hash_copying_default(hash)
      ActiveSupport::HashWithIndifferentAccess.new(hash).tap do |new_hash|
        new_hash.default = hash.default
      end
    end

    alias_method :regular_writer, :[]= unless method_defined?(:regular_writer)
    alias_method :regular_update, :update unless method_defined?(:regular_update)

    # Assigns a new value to the hash:
    #
    #   hash = HashWithIndifferentAccess.new
    #   hash[:key] = "value"
    #
    def []=(key, value)
      regular_writer(convert_key(key), convert_value(value))
    end

    alias_method :store, :[]=

    # Updates the instantized hash with values from the second:
    #
    #   hash_1 = HashWithIndifferentAccess.new
    #   hash_1[:key] = "value"
    #
    #   hash_2 = HashWithIndifferentAccess.new
    #   hash_2[:key] = "New Value!"
    #
    #   hash_1.update(hash_2) # => {"key"=>"New Value!"}
    #
    def update(other_hash)
      other_hash.each_pair { |key, value| regular_writer(convert_key(key), convert_value(value)) }
      self
    end

    alias_method :merge!, :update

    # Checks the hash for a key matching the argument passed in:
    #
    #   hash = HashWithIndifferentAccess.new
    #   hash["key"] = "value"
    #   hash.key? :key  # => true
    #   hash.key? "key" # => true
    #
    def key?(key)
      super(convert_key(key))
    end

    alias_method :include?, :key?
    alias_method :has_key?, :key?
    alias_method :member?, :key?

    # Fetches the value for the specified key, same as doing hash[key]
    def fetch(key, *extras)
      super(convert_key(key), *extras)
    end

    # Returns an array of the values at the specified indices:
    #
    #   hash = HashWithIndifferentAccess.new
    #   hash[:a] = "x"
    #   hash[:b] = "y"
    #   hash.values_at("a", "b") # => ["x", "y"]
    #
    def values_at(*indices)
      indices.collect {|key| self[convert_key(key)]}
    end

    # Returns an exact copy of the hash.
    def dup
      HashWithIndifferentAccess.new(self)
    end

    # Merges the instantized and the specified hashes together, giving precedence to the values from the second hash
    # Does not overwrite the existing hash.
    def merge(hash)
      self.dup.update(hash)
    end

    # Performs the opposite of merge, with the keys and values from the first hash taking precedence over the second.
    # This overloaded definition prevents returning a regular hash, if reverse_merge is called on a HashWithDifferentAccess.
    def reverse_merge(other_hash)
      super self.class.new_from_hash_copying_default(other_hash)
    end

    def reverse_merge!(other_hash)
      replace(reverse_merge( other_hash ))
    end

    # Removes a specified key from the hash.
    def delete(key)
      super(convert_key(key))
    end

    def stringify_keys!; self end
    def stringify_keys; dup end
    undef :symbolize_keys!
    def symbolize_keys; to_hash.symbolize_keys end
    def to_options!; self end

    # Convert to a Hash with String keys.
    def to_hash
      Hash.new(default).merge!(self)
    end

    protected
      def convert_key(key)
        key.kind_of?(Symbol) ? key.to_s : key
      end

      def convert_value(value)
        case value
        when Hash
          self.class.new_from_hash_copying_default(value)
        when Array
          value.collect { |e| e.is_a?(Hash) ? self.class.new_from_hash_copying_default(e) : e }
        else
          value
        end
      end
  end
end

HashWithIndifferentAccess = ActiveSupport::HashWithIndifferentAccess

好了 所有 实现的原理都在 这个文件里了,HashWithIndifferentAccess是 Hash的子类,其中覆盖了default 方法,Hash当找不到 hash 的 key 时 会寻找default值,即执行 default 方法 , so ….

Hash#default 用法demo:

ruby-1.9.2-p0 > h={}
 => {} 
ruby-1.9.2-p0 > h.default=1
 => 1 
ruby-1.9.2-p0 > h[:a]
 => 1 

核心代码:

def default(key = nil)
      if key.is_a?(Symbol) && include?(key = key.to_s)
        self[key]
      else
        super
      end
end

当是symbol 时 转化为 string , 然后 self[string_key]

举一反三:

DEMO:

class MyHash < Hash

  def initialize(constructor = {})
    if constructor.is_a?(Hash)
      super()
      update(constructor) # Hash#update == Hash#merge!
    else
      super(constructor)
    end
  end

  def default(key = nil)
    p key #=> :id
    if key.is_a?(Symbol) && include?(key = key.to_s) # Hash#include? = Hash#has_key? = Hash#member? = Hash#key?
      p key #=> "id"
      self[key]
    else
      super
    end
  end
end

h = MyHash.new({"id"=>1})
p h #=> {"id"=>1}
p h.class #=> MyHash
p h[:id] #=> 1
p h["id"] #=> 1

SEE:

http://lukaszwrobel.pl/blog/ruby-hash-default-value

This entry was posted on Thu, 10 Feb 2011 16:36:00 GMT and Posted in . You can follow any any response to this entry through the Atom feed. You can leave a comment or a trackback from your own site.

Tags


Trackbacks

Use the following link to trackback from your own site:
http://wxianfeng.com/trackbacks?article_id=178

Comments

  1. Avatar
    charles almost {{count}} years ago:

    看你调试rails了,用的什么调试器?

  2. Avatar
    wxianfeng almost {{count}} years ago:

    @charles
    netbeans, 老早以前用的了, 现在不用了.

Leave a comment