介绍一种跟类相似的构造:模块(module)。在设计程序的时候,我们会把大的组件分割成小块,你可以混合与匹配对象的行为。
跟类差不多,模块也捆绑方法与常量。不一样的是,模块没有实例。你可以把拥有特定功能的模块放到类或某个特定的对象里使用。
Class 这个类是 Module 类的一个子类,也就是所有的 class 对象应该也是一个 module 对象。
上午10:26 ***
创建与使用模块
上午10:26 ***
module MyFirstModule
def say_hello
puts 'hello'
end
end
我们创建了类以后可以去创建这个类的实例,实例可以执行类里面的实例方法。不过模块是没有实例的,模块可以混合(mixed in,mix-in,mixin)到类里面,用的方法是 include 还有 prepend 。这样类的实例就可以使用在模块里面定义的实例方法了。
使用一下上面定义的那个模块:
class ModuleTester
include MyFirstModule
end
mt = ModuleTester.new
mt.say_hello
上面的 ModuleTester 对象调用了 say_hello 这个方法,这样会输出一个 hello 。这个方法是混合到 ModuleTester 类里面的 MyFirstModule 里定义的实例方法。
在类里混合使用模块很像是去继承一个 superclass 。比如 B 类继承了 A 类,这样 B 类的实例就可以调用来自 A 类的实例方法。再比如 C 类混合了模块 M,这样 C 类的实例就可以调用在模块 M 里定义的方法。继承类与混合模块的区别是,你可以在一个类里混合使用多个模块,你不能让一个类去继承多个类。
模块可以让我们在多个类之间共用它的代码,因为任何的类都可以混合使用同一个模块。
创建一个模块
模块给我们提供了收集与封装行为的方法。下面我们可以写一个模块,去封装一些像堆(stack)的特性,然后把模块混合到一个或多个类里面,这样模块里的行为就会传授给对象。
堆(stack)是一种数据格式,后进来的,先出去(LIFO:last in, first out)。比如一堆盘子,用的第一个盘子,是最后一次放到这堆里的那个。经常跟堆一起讨论的还有个概念:队列(queue),它是先进来的,先出去(FIFO),比如在民政局窗口前排的队,排在第一位置上的人最先办完手续。
先把下面代码放到 stacklike.rb 文件里:
module Stacklike
def stack
@stack ||= []
end
def add_to_stack(obj)
stack.push(obj)
end
def take_from_stack
stack.pop
end
end
在上面的 Stacklike 模块里,我们使用了一个数组来表示堆,这个数组会存储在一个实例变量里面,名字是 @stack,这个实例变量可以通过 stack 这个方法得到。这个方法使用了条件设置变量,||= 是一个操作符,只有变量不是 nil 或 false 的时候,才会让这个变量的值等于一个特定的值。这里就是第一次调用 stack 的时候,它会设置 @stack 让它等于一个空白的数组,后续再次调用的时候,@stack 已经有值了,也就会去返回它的值。
调用 add_to_stack 方法会把一个对象添加到堆里面,就是会把对象添加到 @stack 数组的最后。take_from_stack 会删除掉数组里的最后一个对象。这些方法里用的 push 还有 pop ,它们是 Array 类里的实例方法。
我们定义的这个 Stacklike 模块,其实就是有选择的实施了已经在 Array 对象里存在的一些行为,添加一个元素到数组的最后,删除数组里的最后一个元素。相比堆,数组更灵活一些,堆不能干所有数组能干的事。比如你可以删除掉数组里的任意顺序的项目,在堆里就不行,你只能删除掉最近添加进来的元素。
现在我们定义好了一个模块,它实施了堆的一些行为,也就是管理一些项目,新的项目可以添加到最后,最近添加进来的可以被删除掉。下面再看一下怎么样使用模块。
在类里混合模块
做个实验,创建一个文件,名字是 stack.rb,添加下面这段代码:
require_relative 'stacklike'
class Stack
include Stacklike
end
这里混合用的方法是 include ,把 Stacklike 这个模块混合到了 Stack 这个类里,这样 Stack 类的对象就会拥有在 Stacklike 模块里定义的方法了。
使用 require 或 load 的时候,要加载的东西放到了一组引号里面,但是使用 include 与 prepend 的时候加载的东西不需要使用引号。因为 require 与 load 要使用字符串作为它们的参数值,include 载入的是模块的名字,模块的名字是常量。require 与 load 要找到在磁盘上的文件,include 与 prepend 会在内存里操作。
类的名字用的是名词,模块的名字用的是形容词。Stack objects are stacklike 。
做个实验:
s = Stack.new
s.add_to_stack('项目 1')
s.add_to_stack('项目 2')
s.add_to_stack('项目 3')
puts '当前在堆里的对象:'
puts s.stack
taken = s.take_from_stack
puts '删除了对象:'
puts taken
puts '现在堆里是:'
puts s.stack
执行一下会输出:
当前在堆里的对象:
项目 1
项目 2
项目 3
删除了对象:
项目 3
现在堆里是:
项目 1
项目 2
继续使用模块
再做个实验,创建一个文件,名字是 cargohold.rb(飞机货舱),代码如下:
require_relative 'stacklike'
class Suitcase
end
class CargoHold
include Stacklike
def load_and_report(obj)
print 'loading object:'
puts obj.object_id
add_to_stack(obj)
end
def unload
take_from_stack
end
end
ch = CargoHold.new
sc1 = Suitcase.new
sc2 = Suitcase.new
sc3 = Suitcase.new
ch.load_and_report(sc1)
ch.load_and_report(sc2)
ch.load_and_report(sc3)
first_unloaded = ch.unload
print '第一个下飞机的行里是:'
puts first_unloaded.object_id
执行它的结果是:
loading object:70328907390400
loading object:70328907390380
loading object:70328907390360
第一个下飞机的行里是:70328907390360
下午12:00 ***
模块,类与方法查找
下午12:06 ***
对象收到发送给它的信息以后,它会试着去执行跟信息一样的方法,方法可以是对象所属的类里面定义的,或者这个类的 superclass,或者是混合到这个类里的模块提供的。发送信息给对象究竟发生了什么?
方法查找
下面这个例子演示了加载模块与类的继承:
module M
def report
puts "'report' 方法在模块 M 里"
end
end
class C
include M
end
class D < C
end
obj = D.new
obj.report
report 这个实例方法是在模块 M 里定义的,在 C 类里面混合了模块 M ,D 类是 C 类的子类,obj 是 D 类的一个实例,obj 这个对象可以调用 report 方法。
从对象的视角来看一下,假设你就是一个对象,有人给你发了个信息,你得想办法作出回应,想法大概像这样:
我是个 Ruby 对象,别人给我发了个 'report' 信息,我得在我的方法查找路径里,试着去找一个叫 report 的方法,它可能在一个类或者模块里。
我是 D 类的一个实例。D 类里有没有 report 这个方法?
没有
D 类有没有混合使用模块?
没有
D 类的超级类(superclass)C,里面有没有定义 report 这个实例方法?
没有
C 类里混合模块了没?
是的,混合了模块 M
那 M 模块里有没有 report 这个方法?
有
好地,就调用一下这个方法。
找到了这个方法搜索就结束了,没找到就会触发错误,这个错误是用 method_missing 方法触发的。
同名方法
同一个名字的方法在任何时候,在每个类或模块里只能出现一次。一个对象会使用它最先在找到的方法。
做个实验:
module M
def report
puts '在模块 M 中的 report'
end
end
module N
def report
puts '在模块 N 中的 report'
end
end
class C
include M
include N
end
c = C.new
c.report
执行它的结果会是:
在模块 N 中的 report
多次加载同一个模块是无效的,这样试一下:
class C
include M
include N
include M
end
执行的结果仍然会是:
在模块 N 中的 report
下午12:49 ****
prepend
下午1:40 ***
使用 prepend 加载的模块,对象会先使用。也就是如果一个方法在类与模块里都定义了,会使用用了 prepend 加载的模块里的方法。
来看个例子:
module MeFirst
def report
puts '来自模块的问候'
end
end
class Person
prepend MeFirst
def report
puts '来自类的问候'
end
end
p = Person.new
p.report
执行的结果会是:
来自模块的问候
super
做个实验:
module M
def report
puts '在模块 M 里的 report 方法'
end
end
class C
include M
def report
puts 'C 类里的 report 方法'
puts '触发上一级别的 report 方法'
super
puts "从调用 super 那里回来了"
end
end
c = C.new
c.report
执行的结果是:
C 类里的 report 方法
触发上一级别的 report 方法
在模块 M 里的 report 方法
从调用 super 那里回来了
c 是 C 类的一个实例,c.report 是给 c 发送了一个 report 信息,收到以后开始查找方法,先找到的 C 类,这里定义了 report 方法,所以会去执行它。
在 C 类里的 report 方法里,调用了 super,意思就是即使对象找到了跟 report 这个信息对应的方法,它还必须继续查找下一个匹配的 report 方法,下一个匹配是在模块 M 里定义的 report 方法,也就会去执行一下它。
再试一个使用 super 的例子:
class Bicycle
attr_reader :gears, :wheels, :seats
def initialize(gears = 1)
@wheels = 2
@seats = 1
@gears = gears
end
end
class Tandem < Bicycle
def initialize(gears)
super
@seats = 2
end
end
上面有两个类,Bicycle 自行车,Tandem 双人自行车,Tandem 继承了 Bicycle 类。在 Tandem 的 initialize 方法里用了一个 super ,会调用 Bicycle 类里的 initialize 方法,也就是会设置一些属性的默认的值。双人自行车有两个座位,所以我们又重新在 Tandem 的 initialize 方法里设置了一下 @seats 的默认的值。
super 处理参数的行为:
不带参数调用 — super,super 会自动转发参数传递给它调用的方法。
带空白参数的调用 — super(),super 不会发送参数。
带特定参数的调用 — super(a, b, c),super 只会发送这些参数。
下午2:23 ***
method_missing 方法
下午 14:35 ***
Kernel 模块提供了一个实例方法叫 method_missing,如果对象收到一个不知道怎么响应的信息,就会调用这个方法。
试一下:
>> obj = Object.new
=> #
>> obj.blah
NoMethodError: undefined method `blah' for #
from (irb):2
from /usr/local/bin/irb:11:in `
我们可以覆盖 method_missing:
>> def obj.method_missing(m, *args)
>> puts "你不能在这个对象上调用 #{m},试试别的吧。"
>> end
=> :method_missing
>> obj.blah
你不能在这个对象上调用 blah,试试别的吧。
=> nil
组合 method_missing 与 super
一般我们会拦截未知的信息,然后决定到底怎么去处理它,可以处理,也可以把它发送给原来的 method_missing 。使用 super 就很容易实现,看个例子:
class Student
def method_missing(m, *args)
if m.to_s.start_with?('grade_for_')
# return the appropriate grade, based on parsing the method name
else
super
end
end
end
上面的代码,如果调用的方法是用 grade_for 开头的就会被处理,比如 grade_for_english 。如果不是,就会调用原始的 method_missing 。
再试一个复杂点的例子。比如我们要创建一个 Person 类,这个类可以这样用:
j = Person.new("John")
p = Person.new("Paul")
g = Person.new("George")
r = Person.new("Ringo")
j.has_friend(p)
j.has_friend(g)
g.has_friend(p)
r.has_hobby("rings")
Person.all_with_friends(p).each do |person|
puts "#{person.name} is friends with #{p.name}"
end
Person.all_with_hobbies("rings").each do |person|
puts "#{person.name} is into rings"
end
我们想要输出的东西像这样:
John is friends with Paul
George is friends with Paul
Ringo is into rings
一个人可以有朋友和爱好,Person 可以找出某个人的所有的朋友,或者拥有某个爱好的所有的人。这两个功能是用 all_with_friends 还有 all_with_hobbies 这两个方法实现的。
Person 类上的 all_with_* 方法可以使用 method_missing 改造一下,在类里定义一段代码:
class Person
def self.method_missing(m, *args)
# code here
end
end
m 是方法的名字,它可以是用 all_with 开头的,也可以不是,如果是我们就去处理一下它,如果不是就交给原始的 method_missing 。再这样修改一下:
class Person
def self.method_missing(m, *args)
method = m.to_s
if method.start_with?('all_with_')
# 在这里处理请求
else
super
end
end
end
Person 对象要跟踪它所有的朋友与爱好
Person 类跟踪所有的人
每个人都有个名字
class Person
PEOPLE = []
attr_reader :name, :hobbies, :friends
def initialize(name)
@name = name
@hobbies = []
@friends = []
PEOPLE << self
end
def has_hobby(hobby)
@hobbies << hobby
end
def has_friend(friend)
@friends << friend
end
每次实例化一个新人都会把它放到 PEOPLE 这个数组里。还有几个读属性,name,hobbies,friends。
initialize 方法里有个 name 变量,把它放到了 @name 属性里,同时也会初始化 hobbies 和 friends ,这两个属性在 has_hobby 与 has_friend 方法里用到了。
再完成 Person.method_missing :
def self.method_missing(m, *args)
method = m.to_s
if method.start_with?('all_with_')
attr = method[9..-1]
if self.public_method_defined?(attr)
PEOPLE.find_all do |person|
person.send(attr).include?(args[0])
end
else
raise ArgumentError, "Can't find #{attr}"
end
else
super
end
end
全部代码如下:
class Person
PEOPLE = []
attr_reader :name, :hobbies, :friends
def initialize(name)
@name = name
@hobbies = []
@friends = []
PEOPLE << self
end
def has_hobby(hobby)
@hobbies << hobby
end
def has_friend(friend)
@friends << friend
end
def self.method_missing(m, *args)
method = m.to_s
if method.start_with?('all_with_')
attr = method[9..-1]
if self.public_method_defined?(attr)
PEOPLE.find_all do |person|
person.send(attr).include?(args[0])
end
else
raise ArgumentError, "Can't find #{attr}"
end
else
super
end
end
end
j = Person.new("John")
p = Person.new("Paul")
g = Person.new("George")
r = Person.new("Ringo")
j.has_friend(p)
j.has_friend(g)
g.has_friend(p)
r.has_hobby("rings")
Person.all_with_friends(p).each do |person|
puts "#{person.name} is friends with #{p.name}"
end
Person.all_with_hobbies("rings").each do |person|
puts "#{person.name} is into rings"
end
执行的结果会是:
John is friends with Paul
George is friends with Paul
Ringo is into rings
我最强舞者 (I, Best Dancer)安卓版v8
我最强舞者(I, Best Dancer)是一款休闲放置类手
迷你世界国服版本2024 v1.43.0
迷你世界国服版本2024是一款自由度非常高的沙盒游戏,玩法和
烹饪乐园 安卓版v1.23.6
烹饪乐园(Cooking Town)是一款非常好玩的餐厅模拟
迷你世界小米服 最新安卓版v1.43.0
迷你世界小米版是由迷你玩科技开发的休闲模拟经营类游戏。此版本
布娃娃Sprunki沙盒 安卓版v0.0.1
布娃娃Sprunki沙盒是一个非常有趣的沙盒游戏,复古简约的