Ruby Ruby 中的 OpenStruct 详解

neverlandxy_naix · 2014年09月19日 · 最后由 zztczcx 回复于 2016年08月26日 · 7962 次阅读
本帖已被管理员设置为精华贴

什么是 Struct?

在 C 语言中,有一种数据类型叫做 Struct, 定义一个 Struct 相当于定义了一个自定义的数据类型,里面可以包含各种不同的其它数据类型。例如:

struct person
{
  int age;
  char name[50];
  char sex;
};

同样在 Ruby 中,也有 Struct, 纯 C 实现,用于快速声明一个 Class, 例如:

Person = Struct.new(:age, :name, :sex)
me = Person.new(24, 'Spirit', 'male')
me.age  # => 24
me.name = 'Test'
me.name # => 'Test'
me.height # => NoMethodError

当然,我们也可以通过定义 Class 来实现同样的功能,但是 Struct 的快速声明功能是直接定义 Class 无法实现的。

OpenStruct 详解

在上面的实例中,可以看到,me 无法动态响应任意的属性,OpenStruct 类恰巧就完成了这件事情。

还是上面那个例子:

require 'ostruct'

me = OpenStruct.new(age: 24, name: 'Spirit', sex: 'male')
me.height # => nil
me.height = '178'
me.height # => '178'

翻看 OpenStruct 的源码,可以看到其核心为 #initialize(), #new_ostruct_member()#method_missing() 三个方法:

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

初始化,一旦传入的参数为 hash, 则把 hash 的键值对一一保存到实例变量 @table 中,并且调用 #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

为每一个传入的 name(hash 的 key) 都生成两个实例方法 #name#name=, 并且定义其值为实例变量 @table 中对应的 key 的值

def method_missing(mid, *args)
  mname = mid.id2name
  len = args.length
  if mname.chomp!('=')
    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 方法

当初始化的时候没有传入 hash, 或者让对象响应一个任意的方法时:

如果需要响应方法为 mname=, 并且实参为一个,那么调用 #new_ostruct_member() 方法,为实例定义两个方法 #mname#mname=, 下次调用该方法时,则不需要经过 method_missing 这一步骤

如果需要响应的方法为 mname, 并且没有实参,那么直接返回 @table 中对应的 k 的值

事实上,除非为 @table 注入一个 { mname: value } 的键值对或者调用 obj.mname=value, 否则直接调用 obj.mname 经过 method_missing 后返回的值,都为 nil

me = OpenStruct.new(age: 24, name: 'Spirit', sex: 'male')
me.height # => 'nil'
me.instance_eval do
  @table[:height] = 178
end
me.height # => '178'

ok, OpenStruct 详解完毕,其余方法可自行挖掘。

最后说一句,OpenStruct 由于进行了 method_missing 判断,其性能与 Struct 相比,可谓是天差地别。http://stackoverflow.com/questions/1177594/ruby-struct-vs-openstruct

code walkthrough 什么的最有爱了

这个 OpenStruct 效率如何?有坑吗?

赞。 性能确实木有自带的 Struct 好,不过某些读取 yml 之类的代码能简洁很多。我在做的内部测试脚本里的对象描述就应用到了 OpenStruct,RecursiveOpenStruct就是一个增强版的 OpenStruct,复杂的 symbol 也能描述,看源码也很好理解。

#6 楼 @jianliao 没啥坑,效率上,看你怎么用了,读取写 yml 文件是没什么问题的。

#7 楼 @davidlichao 在项目中我用这个 settingslogic, 比 Struct 更方便。

自己写了一个 Struct,可以满足简单需求。 class LStruct

def initialize(hash) @attrs = {} hash.each_pair do |k, v| @attrs[k.to_s] = v end end

def method_missing(attr, *args) name = attr.to_s if name =~ /=$/ @attrs[name.chop] = args[0] else @attrs[name] end end end

并附上效率测试结果: user system total real struct 0.020000 0.000000 0.020000 ( 0.017825) ostruct 0.730000 0.010000 0.740000 ( 0.733454) lstruct 0.160000 0.000000 0.160000 ( 0.162041) 可以看到,struct 效率最高,ostruct 效率最低,自己实现的由于比较简单,效率在两者之间。

Code: require 'benchmark' require 'ostruct'

n = 50000 Benchmark.bm do |b| b.report('struct') { P = Struct.new(:sex, :age) n.times do |x| P.new(:'男', n % 60) end } b.report('ostruct') { n.times do |x| OpenStruct.new(:sex => '男', :age => n % 60) end } b.report('lstruct') { n.times do |x| LStruct.new(:sex => '男', :age => n % 60) end } end

看過測試,openstruct 內存使用驚人。

需要 登录 后方可回复, 如果你还没有账号请 注册新账号