Ruby学习笔记之Ruby 模块

介绍一种跟类相似的构造:模块(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
=> #<Object:0x007fdef1958fa8>
>> obj.blah
NoMethodError: undefined method `blah' for #<Object:0x007fdef1958fa8>
 from (irb):2
 from /usr/local/bin/irb:11:in `<main>'
我们可以覆盖 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

时间: 2024-10-22 22:06:02

Ruby学习笔记之Ruby 模块的相关文章

Ruby学习笔记之Ruby 对象

大部分 Ruby 程序,它们的设计,逻辑,动作,都是围绕着对象进行的.写一个 Ruby 程序,主要的工作就是去创建对象,然后给它们能力,让它们可以去执行动作. Ruby 是 OOP 语言,就是面向对象的语言,你执行的计算,数据处理,输入与输出这些动作,都是通过创建对象,然后让这个对象去执行指定的动作来完成的.对象(object)在现实世界里,就是一个东西.一个苹果是一个对象,一张桌子也是一个对象. 每个对象是一个特定的类的实例(instance),对象的行为大部分是在它们所属的那个类上面定义的方

ruby学习笔记(11)--symbol与hash参数

symbol是啥就不深入的讨论了,只简单说说symbol的好处 ruby内部对于每个对象,都会有一个数字id用来标识并区分,可以用xxx.object_id来查看 puts "0001".object_id puts "0001".object_id puts "0001".object_id puts "0001".object_id 输出结果类似如下: 32088750320887303208871032088690 可以

Ruby学习笔记_索引贴

学习Ruby也有段时间了,在学习的同时也做了些笔记并发到了园子睐.看到园子里的大虾们在出了一系列文章后都会做个索引贴,这样很方便,所以本人今天抽了个空就把它整理了下,方便自己的同时也方便感兴趣的朋友.   Ruby学习笔记目录: 1.Ruby入门 2.Ruby-循环与选择结构 3.Ruby-String 4.Ruby-Array 5.Ruby-Hash 6.Ruby-Block, Proc and Lambda 7.Ruby-正则表达式 8.Ruby-Symbol 9.Ruby-Method,C

Ruby学习笔记

Ruby语言中,以对象为基本单位,可以说所有的元素都是对象.按照之前对于面向对象程序的理解,对象是指包含了特定属性和方法集合的一组程序.对象由类来定义,具体的表现为对象实例.也就是说,对象是类的实例化[2]. Ruby语言的基础元素 对象:数值对象.字符串对象.正则表达式对象.时间对象.文件对象.目录对象.数组.哈希.例外对象等 数值对象      由于Ruby中一切数据都是对象,所以我们处理的数字实际上也是对象.      a = 10,这样一个简单的赋值语句,实际上应当理解为 a = Num

ruby学习笔记(5)-模块module的运用

ruby中的module与.net中的namespace有点类似,可以用来区分同名但属于不同开发者(或组织)的代码. 下面的代码,定义了一个Me模块,里面的sqrt与Math模块中的sqrt重名,另外还有一个重名常量PI #定义一个模块(有点类似.net中的命名空间) module Me def sqrt(num1,num2=-1) return "num1=#{num1},num2=#{num2}" end PI = 3.14; end puts Math::PI #在未includ

ruby学习笔记(1)--初识语法

虽然ruby/ruby on rails从2007年就一直获奖无数,但身为一个中国人,一直对小日本创造的东西不怎么感兴趣,想想其实也没必要,技术本身是无国界的,日本其实也有值得学习的地方(扯远了,呵) 单从技术而言,ruby本身确实很爽,令程序员的工作变得轻松有趣! 下面的代码演示了如何找出100以内的素数: using System; namespace Mersenne { class Program { static void Main(string[] args) { for (int

ruby学习笔记(2)--类的基本使用

ruby语言跟c#的一些重要差别在于: 1.ruby是动态语言,c#是静态语言--即对象在new出来以后,ruby还可以动态给对象实例添加一些属性或方法(javascript也是如此) 2.ruby中刻意弱化了变量类型这个概念,默认情况下变量/方法都不需要声明具体(返回)类型,但其实在ruby内部,会自动根据变量的值分配类型.(可以通过 "puts 变量.class"查看) 3.ruby相对c#来讲,可能有些雷的地方在于:父类中的private成员,居然是可以在子类中使用的! ...其

Ruby学习笔记-Module

Module: 模块的定义和类比较相似,使用module关键字.但模块不能被实例化,也不能被子类化,模块是独立的,且一个模块对像是Module类的一个实例.模块最常用的两个用途是作为命空间和混入(mixin).       在模块中,可以定义实例变量.实例方法.类变量.类方法和属性等,并且在模块中还可能以定义类和模块.在类中也可以定义模块.       在访问模块中的实例成员,需要在类中饱含模块,然后实例化类以访问模块的实例成员.               module FirstModule

Ruby学习笔记二 使用Ruby实现通过Proxy的方式请求网页

现在很多网站上某些活动都有限制同一IP只能投一票的规定,但是有时候迫于 压迫,又不得不想办法多投几票,以前是采用Apache里的HttpClient来实现这些功 能,日前正在看Ruby,就用它也来玩下: require 'net/http' ##获得网页内容 def query_url(url) return Net::HTTP.get(URI.parse(url)); end #抓取cnproxy上所有的代理列表,并将结果保存到proxy.txt中去 #你可以修改这块代码或者其他的代理服务器列