我...在前一章节 Ruby 中的 struct 浅谈 翻 Struct 类的文档的时候,竟然还发现了一个 OpenStruct
,这... 闹哪样啊...我还以为今天可以休息了...剩下的大量的 Ruby 知识以后再来了解,谁知又冒出一个 OpenStruct
好吧,那今天就把 OpenStruct 也讲完吧...
翻了下 OpenStruct 的官方文档,内容有点多,我们就多分几个小节吧。 本章,我们的主要知识点有
OpenStruct
类- 数据结构: 创建和操作
OpenStruct
和懒加载- 数据结构背后的实现
加载库 ( 模块 )
首先,需要说明的是,OpenStruct 类是 Ruby 标准库的一部分,但不在核心库中,所以使用前需要先加载相应的库
require 'ostruct'
OpenStruct
类
一句话介绍 OpenStruct 类: 「 OpenStruct 类用于创建灵活的数据结构 」
OpenStruct 类非常简单易用,且不需要提供严格的成员列表,因为它没有定义任何结构类型,而是之后填充的数据结构
OpenStruct 类是 Ruby 标准库的一部分,处于 stdlib
中
要了解 OpenStruct 类,第一件要做的事情,就是了解它的祖先链
irb> require 'ostruct' => true irb> OpenStruct.ancestors => [OpenStruct, Object, Kernel, BasicObject]
上面的代码中,由于 OpenStruct 不是 Ruby Core 的一部分,因此我们必须使用 require ostruct
加载相应的库
然后,和其它大多数类一样,OpenStruct 也是继承自默认的 Object 对象
不过也有不一样的地方,那就是 OpenStruct 没有包含任何其它模块,与 Struct
不同的是,没有加载 Enumerable
模块,也就是 OpenStruct 类的实例不能进行比较相关操作
数据结构: 创建和操作
OpenStruct::new
方法可以用来创建新的数据结构
new
方法接受一个参数,可选的参数有 Hash
、Struct
或另一个 OpenStruct
require 'ostruct' computer = OpenStruct.new(ram: '4GB') computer.class # => OpenStruct computer.ram # => "4GB" computer[:ram] # => "4GB" computer['ram'] # => "4GB" computer.screens = 2 # OR: computer[:screens] = 2 # OR: computer['screens'] = 2 computer.screens # => 2 computer[:screens] # => 2 computer['screens'] # => 2
上面这段代码中
- 首先,我们使用一个名为
ram
的属性实例化一个新的OpenStruct
对象,并将该对象存储在变量computer
中 -
然后我们通过 3 种不同的方式访问 ram 属性
computer.ram
:ram
getter 方法computer[:ram]
: 使用一个符号symbol
作为键的OpenStruct#[]
方法computer['ram']
: 使用一个字符串作为键的OpenStruct#[]
方法
-
接着,我们定义了
screens
属性。有 3 种方法可以定义新属性或修改现有属性的值computer.screens=
:screens=
setter 方法computer[:screens]=
: 使用一个符号symbol
作为键的OpenStruct#[]=
方法computer['screens']=
: 使用一个字符串作为键的OpenStruct#[]=
方法
- 接着,我们使用之前访问
ram
属性同样的方式访问screens
属性。
与使用固定属性列表定义结构类型的 Struct
类不同,OpenStruct
类定义了可以在数据结构定义范围外动态添加属性的新数据结构 - 就像上面示例中的 screen 属性一样
OpenStruct 和懒加载
为了节省内存并加快对属性的访问,OpenStruct
实例的属性的访问器方法在某些点处是延迟加载的
也就是说,仅在触发一堆定义的操作时才定义方法。
这样,我们的程序只需要定义数据结构工作所需的最少量方法
让我们来看看 OpenStruct 中的懒加载是如何工作的
require 'ostruct' computer = OpenStruct.new(ram: '4GB') computer.class # => OpenStruct computer.methods(false) # => [] computer[:ram] # => "4GB" computer.methods(false) # => [] computer.ram # => "4GB" computer.methods(false) # => [:ram, :ram=] computer[:screens] = 2 # => 2 computer.methods(false) # => [:ram, :ram=, :screen, :screen=]
从上面的代码中可以看出
-
在 OpenStruct 实例初始化时,尚未定义
computer.ram
和computer.ram=
方法 -
当调用了
computer.ram
获取属性ram
的值之后,发现已经computer.ram
和computer.ram=
方法。需要注意的是,使用
computer[:ram]
获取属性的值并不会触发定义computer.ram
和computer.ram=
方法 -
直到我们调用了
computer[:ram] = 2
,才定义了computer.screens
和computer.screens=
两个方法
综上所述,我们发现访问者方法仅在以下情况下定义
- 第一次调用存在或不存在的属性的
getter
访问器方法 - 第一次调用
OpenStruct#[]=
方法,例如范例中的computer[:ram] = 2
数据结构背后的实现
经过前面的几个小节,想必你已经对 OpenStruct 和数据结构有着相当的了解了。
接下来,我们开始深入了解在创建和操作 OpenStruct 对象时幕后发生的事情
OpenStruct::new
创建的每个数据结构内部都定义了一个哈希 ( hash ) ,这是该数据结属性与其值的对应不安息表
这个内部哈希,通常称之为 「 容器 」 ( container ) ,也称之为 「 表 」( table )
computer.instance_variables # => [:@table, :@modifiable]
需要注意的是,每个属性在添加到 「 表 」之前,都需要将属性 ( 键 ) 转换成一个 symbol
那么,现在我们来看看 computer
表的演变
require 'ostruct' computer = OpenStruct.new(ram: '4GB')
运行上面的代码,变量 computer
中的表的数据为 {ram: "4GB"}
接着,我们把属性 screens
添加到数据结构 computer
中
computer[:screens] = 2
添加完毕后,computer
中的表的数据为 {ram: "4GB", screens: 2}
那么,当调用不存在的属性时会发生什么?
例如,当我们调用 computer.cores=
设置方法时 ?
computer.cores = 2
这种情况下,Ruby 的处理逻辑是
「 如果未在给定对象的整个祖先链中定义方法,那么,Ruby 会自动调用此祖先链中第一个定义的 method_missing
钩子方法 」
凑巧的是,OpenStruct
定义了自己的 method_missing
方法。
所以,当调用 computer.cores = 2
时,相当于就是调用 OpenStruct#method_missing
方法
我们详细说明下当调用 computer.cores = 2
时该方法的执行流程
- 定义 getter 和 setter 方法,并将作为参数传递的 key 转换为
symbol
- 在我们的示例中为:cores
- 将键值对插入内部 「 表 」 中,也就是
@table[:cores] = 2
- 返回值为
2
结束语
撒花...真的是为了结束而草率的结束了...
OpenStruct 还是非常有意思的,其实看起来就是一个哈希表,既然是这样,那为什么还要存在一个 OpenStruct 呢 ?
谁直到答案,私聊我,有赏