OpenStruct 详解

作者:周星 发布:2017-10-07

OpenStruct 是一种常见的数据结构,有点类似于 Hash,OpenStruct 不是 ruby 内置模块,使用之前你必须 require 'ostruct',使用 OpenStruct.new 直接生成一个对象,你可以随意指定这个对象的属性及对应的值。请看下面这个例子:

require 'ostruct'

person = OpenStruct.new
person.name    = "John Smith"
person.age     = 70
person.pension = 300

puts person.name     # -> "John Smith"
puts person.age      # -> 70
puts person.address  # -> nil

你可以通过传入一个 hash 来 new 一个 OpenStruct,当这个 hash 的 key 里含有空格或者其它字符时,你就不能用一些常规的方法来取这个 key 对应的值,比如 . () 等,但是你可以通过 send 方法动态的来实现这一需求,每次为对象增加一个属性时,OpenStruct 会对应生成 getter 和 setter 方法。

person = OpenStruct.new("age is " => 23)
person."age is "                     #-> 报错
person("age is ")                    #-> 报错
person.send("age is ")               #-> 23
person.send("age is =", 34)          #-> 34
person.send("age is ")               #-> 34

想要删除一个 OpenStruct 对象的方法需要调用 delete_field 方法,赋值这个属性为 nil 是不会删除方法的。

person1 = OpenStruct.new(:name => 'Edward', :age => '12')
person1.age = nil
person2 = OpenStruct.new(:name => 'Edward')

person1 == person2   # -> false

person1.delete_field(:age)
person1 == person2   # -> true<

可以在 OpenStruct 对象上调用 to_h 方法,将其转化为一个 Hash 对象

person = OpenStruct.new("name" => "Edward", "age" => 20)
person.to_h           # -> {:name=>"Edward", :age=>20}

OpenStruct 还有一个 protected 实例方法—— new_ostruct_member,它在各种 gem 源码中出现频率极高,因为这个方法是 protected,所以不能在对象上直接调用它,它提供了元编程的方式动态的在对象上生成属性,看下面这个例子:

#!/usr/bin/ruby
require 'ostruct'
class A < OpenStruct
  def generate_attributes(attribute_name)
    s = self.class.new
    s.new_ostruct_member("#{attribute_name}")
    s.send("#{attribute_name}=".to_sym, "value")
    s
  end
end
p A.new.generate_attributes("key")

类 A 继承了 OpenStruct,它的实例方法 generate_attributes 通过调用 new_ostruct_member 来动态生成属性,所以当你不确定你的属性值时,你可以通过这样的方式来按需生成属性。

看了这么多,我产生了一个疑问,OpenStruct 为什么具有这些特性?索性让我们通过源码来探个究竟:

首先我们看一下 OpenStruct 的 initialize 方法是如何定义的:

def initialize(hash=nil)
  @table = {}
  if hash
    hash.each_pair do |k, v|
      k = k.to_sym
      @table[k] = v
      new_ostruct_member(k)
    end
  end
end

这里面有一个非常重要的实例变量 @table,它被初始化为一个 hash,当 OpenStruct 通过传入 Hash 初始化对象时,initialize 方法会遍历这个 Hash,将其键值对全部赋给 @table,并对这个 Hash 的每一个 Key 执行 new_ostruct_member 方法。我们再看一下 new_ostruct_member 方法是如何定义的:

def new_ostruct_member(name)
  name = name.to_sym
  unless respond_to?(name)
    define_singleton_method(name) { @table[name] }
    define_singleton_method("#{name}=") { |x| modifiable[name] = x }
  end
  name
end
protected :new_ostruct_member

原来它内部调用了两次 define_singleton_method 方法,生成了一个 getter 和 一个 setter 方法,最后再声明它为 protected。接下来我们再看一个非常重要的方法:method_missing。

我们这里不会介绍什么是 method_missing,如果你对它感觉陌生,请去看一遍 ruby 元编程,博主再次声明本人不支持在团队协作中使用 method_missing。

def method_missing(mid, *args) # :nodoc:
  len = args.length
  if mname = mid[/.*(?==\z)/m]
    if len != 1
      raise ArgumentError, "wrong number of arguments (#{len} for 1)", caller(1)
    end
    modifiable[new_ostruct_member(mname)] = args[0]
  elsif len == 0
    @table[mid]
  else
    err = NoMethodError.new "undefined method `#{mid}' for #{self}", mid, args
    err.set_backtrace caller(1)
    raise err
  end
end

看到这里顿时有一种醍醐灌顶的感觉,一切的疑问都迎刃而解。当方法名是以等号结尾时,method_missing 方法内部进入第一个 if 条件(if mname = mid[/.*(?==\z)/m]),如果参数个数不等于1,则抛出异常,报告 "wrong number of arguments ..."

person.send('ff=')
ArgumentError: wrong number of arguments (0 for 1)
    from /Users/xingzhou/.rbenv/versions/2.1.5/lib/ruby/2.1.0/ostruct.rb:170:in `block in new_ostruct_member'

当只有一个参数时,则调用 modifiable 方法,给 @table 这个 Hash 里增加一个 key/value。

当方法名不以等号结尾且没有参数时,直接在 @table 里增加一个 key: nil

person = OpenStruct.new
person.name        # -> nil
# name 不以等号结尾

源码里还定义了一些方法,解读起来不是很困难,这里我们就不一一解释了,如果你想继续探索,请查看 OpenStruct 源码,传送门

如果您对本文有什么意见或建议,请联系博主

支付宝扫码赞助博主


评论(0)