Ruby 中也有结构体 ( struct ) ,这点经常会被忽略,存在感实在不敢恭维....一个完全面向对象的编程语言,实在想不出需要用到 struct 的场景。
即使偶尔能想到,也会用其它的方法代替,毕竟,struct 感觉是 C 语言 时代的东西。
而且,忘记说了,Ruby 中的 Struct 也是一个类,对的,你没看错,是一个类。
本着探奇的心里,我们还是来讲讲 Ruby 中的这个 struct 吧。
本章节会涉及到以下几个知识点
Struct
类- 结构体类型和结构体
- Ruby 结构体背后的那些实现
Struct 类
任何一门语言,除了变态的 C++ ,结构体的主要作用就是纯粹的存储数据。虽然不少的语言也能够给 Struct 定义一些方法,但那不是它的初衷。
如果既需要保存数据,又需要对数据进行各种逻辑操作的方法,那还不如直接定义一个类或对象。
Ruby 中的结构体也是虚拟数据容器。与对象或类不同,它用于捆绑和提供一组没有任何逻辑的信息。也就是纯粹只是数据容器。
Ruby 中的 Struct 类还为它包含的每个属性提供了一对 getter/setter
方法,这类似于 attr_accessor
方法
如果你不熟悉 Ruby 中的
attr_ *
方法,可以阅读我们的 Ruby 中的 Attributes 文章。
Ruby 中的 Struct
类是结构类型构建器。该类负责定义可以在之后生成结构实例的新结构类型
让我们来看看它的祖先链
irb> Struct.class => Class irb> Struct.ancestors => [Struct, Enumerable, Object, Kernel, BasicObject]
从祖先链中可以看出,Struct
类的默认祖先也是 Object
对象。
Struct
类的祖先中还包括 Enumerable 模块,负责向一个类添加一堆搜索,排序和遍历方法
如果你仔细观察, 其实,就会发现,Struct
类与 Array 和 Hash 类有着完全相同的祖先链
结构体类型和结构体
一个结构体类型是一个蓝图 ( blueprint ) 或者说是类,它包含一个不可变的属性列表 - 也称为成员
一个结构是则是该蓝图 ( 对象 ) 的内存表示。
现在让我们看看如何创建结构体类型。
Struct::new
方法是结构体类型构建器。它允许我们定义与作为参数传递的一组已定义成员关系的新结构类型
简单的说就是传递一组属性名给 Struct::new
方法来创建一个新的结构体类型
Address = Struct.new(:street, :city, :zip) home = Address.new('Broadway', 'NYC', 10040)
上面的代码中,我们定义了一个新的结构体类型 Address
,该 Address
结构体类型包含三个成员,分别是 :street
、 :city
、:zip
第二行代码,我们使用 'Broadway', 'NYC', 10040
三个参数创建了一个新的 Address
结构体的实例,并把它赋值给 home
变量。
Address.new('Broadway','NYC',10002)
的每个参数都按给定顺序匹配 Struct.new(:street,:city,:zip)
的相应参数
home.street # => "Broadway" home[:city] # => "NYC" home['zip'] # => 10040 home.not_exist # => NoMethodError: undefined method `not_exist' home[:not_exist] # => NameError: no member 'not_exist' in struct home['not_exist'] # => NameError: no member 'not_exist' in struct
上面的代码中,我们可以看到,有三种方式可以访问一个结构体实例的成员
home.street
:使用的是street
getter 访问方法home[:city]
:使用symbol
类型键的Struct#[]
方法home['zip'
:使用string
类型键的Struct#[]
方法
我们也看到了,如果尝试访问不存在的成员,则会抛出 NoMethodError
或 NameError
,具体取决于访问此成员的方式
除了获取访问成员之外,还可以修改给定结构的成员值
home # => #<struct Address street="Broadway", city="NYC", zip=10040> home.street = 'Opéra' home[:city] = 'Paris' home['zip'] = 75009 home # => #<struct Address street="Opéra", city="Paris", zip=75009>
与访问成员的三种方式对应的,修改成员也有三种方式
home.street=
:使用的是street=
setter 访问方法home[:city]
:使用symbol
类型键的Struct#[]
方法home['zip'
:使用string
类型键的Struct#[]
方法
另外,从上面的代码中,我们还注意到,如果尝试修改不存在的成员,则会引发 NoMethodError
或 NameError
,具体取决于修改此成员的方式
现在我们已经熟悉结构体和结构类型,接下来我们深入研究 Ruby 内部是如何定义结构类型的
Ruby 结构体背后的那些实现
与 Ruby 中的类一样,Struct::new
方法可以实例化一个 Struct 类型的对象
irb> Struct.allocate TypeError (allocator undefined for Struct) irb> Struct.methods(false) => [:new]
实际上,Struct#allocate
方法 - 负责分配包含 Struct 对象所需的内存空间 - 在 Struct 类定义中是未定义的
因此,Struct 类无法分配所需的内存来实例化 Struct 类型的对象
究其原因,是因为 Struct 类重写了 BasicObject#new
方法
那么,如果我们不能实例化 Struct,那么如何定义结构体类型 ?
谈起如何实现,这,讲起来还有点那么的神奇,毕竟总让人觉得这个类是可实例化的。
其实,所有神奇之处追根究底还是在 Struct#new
方法中定义。
Struct#new
方法并没有实例化 Struct,而是创建了自己的子类
Address = Struct.new(:street, :city, :zip) Address.class # => Class Address.superclass # => Struct
上面的代码中,Address
常量实际上是一个继承自 Struct 类的子类。
这样就允许了 Address
访问定义在 Struct
类中的所有方法和内部的一些逻辑
也可以说,结构类型实际上是一个从 Struct 类继承的命名类,而结构只是这个命名类的一个实例。
这个强大的设计允许我们的结构类型享受类在 Ruby 中提供的所有机制,如类开放,继承,混合等...
例如,我们可以重新打开 Address 类来添加 full_address 方法
Address = Struct.new(:street, :city, :zip) class Address def full_address "#{street} #{city} #{zip}" end end home = Address.new('Broadway', 'NYC', 10040) home.full_address # => "Broadway NYC 10040"
这是 Struct::new 方法的基本用法。
其实,Struct::new
方法还提供了另一种创建一个结构体类型的方法,我们直接看示例代码即可
Struct.new('Address', :street, :city, :zip) Struct::Address.superclass # => Struct home = Struct::Address.new('Broadway', 'NYC', 10040) home.class # => Struct::Address
通过将结构类型名称作为第一个参数传递给 Struct::new
,该方法自动在 Struct 类的作用域范围内定义一个新类,这个新类也继承自 Struct
然后我们可以实例化新定义的 Struct::Address
结构类型并将结构存储在 home 变量中