分享 Ruby 和元编程的故事 - 第 2 回: 类与模块,Ruby 的绝代双骄

hisea · 2012年03月03日 · 最后由 hisea 回复于 2014年02月27日 · 16316 次阅读
本帖已被管理员设置为精华贴

广告:http://hisea.me/p/ruby-story-ep2

开篇

上回说到,在 Ruby 语言中,万物皆为对象。

类 class 和模块 module 也不例外,也是对象,只不过方法和用途各有不同。

本回综合的看一下 class 跟 module 的一些故事。

Ruby 的类,就是一段被执行的代码

如果你用过 Java 等静态 OO 语言,当你定义一个类的时候,你是在声明一个数据类型。

public class Test {
}

在 Ruby 中,并不存在这个声明的过程, 类的定义与其他的代码没有什么不同,完全就是在执行一段代码。

例如 >> class Test >> puts "hello world" >> end hello world => nil

如果不是在声明定义,完全是在执行代码,那么 class 这个 key word 到底起到什么作用呢?

第一回中我们说,self 在 Ruby 中是个重要的概念

self 是一个特殊的变量,里面保存的是当前的对象,方法的调用/定义,实例变量的解析,接受者都会默认为 self 对象。self 保存的值不能被显式的修改,只有通过特殊途径可以修改 self 的值。一个是(.)这个符号,当我们调用一个方法时,例如 obj.method() 之后 method 的解析和运行就会把 self 的值切换到 obj。

另外一个能修改 self 值的途径,就是class关键字。

class 能够达到下面几个作用, 比如我们有前面的 class Test

  1. 定义一个常量,就是 class 后面的名字 Test
  2. 生成一个新的 Class 的实例对象并赋值给第一步里面生成的常量
  3. 把 self 的值换成第二步生成的 Class 实例对象

以上完成之后,Ruby 继续执行 class 内的代码,直到 end.

实际上,如果已经存在一个同名常量,Ruby 会重新使用那个常量,如果常量不是 class 就会报错 >> Test = 2 => 2 >> class Test >> end TypeError: Test is not a class from (irb):2

如果是已存在的类,class 会把 self 转到那个类对象,继续执行后面的代码,包括重新定义方法的代码。 这个行为看上去就像是打开类重新定义新内容,Monkey Patching 就是利用了这个特性。

Ruby 的类,就是一个普通的对象

上一个部分说了 Ruby 的类就是在不同的上下文环境 (class 的 self) 中执行一段代码。
这个 self 指向的,class 本身,其实就是一个很简单的对象,这个对象是 Class 类的一个实例,当然 Class 类也是一个对象。

我们完全可以不用 class 关键字来定义类。

我们可以用Class.new来生成一个类的对象赋值给一个常量。这样生成的类跟 class 关键字生成的一样好用。 >> Test = Class.new => Test

从这里可看出来,Test 就是 Class 类的一个对象而已,这个对象有 Class 的实例方法。

>> Class.instance_methods => ["private_class_method", "inspect", "name", "tap", "clone", "public_methods", "send", "method_defined?", "instance_variable_defined?", "yaml_tag_read_class", "autoload", "equal?", "freeze", "extend", "send", "const_defined?", "methods", "to_yaml_properties", "ancestors", "module_eval", "hash", "dup", "object_id", "instance_methods", "public_method_defined?", "yaml_as", "instance_variables", "class_variable_defined?", "eql?", "constants", "id", "instance_eval", "singleton_methods", "module_exec", "instance_method", "const_missing", "taint", "autoload?", "instance_variable_get", "frozen?", "to_enum", "private_method_defined?", "public_instance_methods", "display", "instance_of?", "superclass", "to_a", "included_modules", "const_get", "instance_exec", "type", "<", "protected_methods", "<=>", "class_eval", "==", "class_variables", ">", "===", "instance_variable_set", "enum_for", "protected_instance_methods", "protected_method_defined?", "yaml_tag_class_name", "taguri", "respond_to?", "kind_of?", ">=", "method", "public_class_method", "to_s", "<=", "const_set", "allocate", "taguri=", "class", "new", "private_methods", "=~", "tainted?", "id", "class_exec", "untaint", "nil?", "private_instance_methods", "to_yaml", "to_yaml_style", "include?", "is_a?"]

这些实力方法中,有些是大家比较熟悉的,例如#class, #superclass,最关键的是,有一个方法叫做new.

就是说 Class 的实例对象 Test,有一个实例方法叫做 new,这个方法可以生成一个 Test 自己的实例对象。

>> t = Test.new => #Test:0x105e026c0

如果想往这个类上定义实例方法,可以用 Monkey Patch 的办法再次打开类,也可以用 class_eval.

类的 self 和类的方法

类有两种方法,一是实例方法,例如:

class Test
  def hello
    puts "hello"
  end
end

这里定义的是一个实例方法,只有通过 Test 的实例才能调用这个方法。如Test.new.hello

另一种是类方法,类方法就是类似这样的方法: Test.hello 看上去是不是很像 Test.new, 我们定义这一类型的方法时其实就是利用的影子类为 Test 对象创造了一个单例方法。 最简单的办法就是

>> Test = Class.new => Test >> def Test.hello >> puts "hello" >> end => nil >> Test.hello hello => nil

上面的例子也能显示 Test 其实就是一个简单的对象,hello 只不过是这个对象的单例方法,关于单例方法,详见第一回

大部分时候,类方法的定义是在类的内部。

class Test
  def self.class_method1
    puts "hello1"
  end
  def Test.class_method2
    puts "hello2"
  end
end

以上时两种在类的内部定义类方法的途径。 class_method2 的定义途径跟前面一个例子的一样,利用了 class 内部只是在执行代码这一特性。 这部分代码在类的内部和外部执行都是一个结果,给类对象添加一个 singleton method.

而 class_method1 的定义途径只是利用了 self 在 class 的内部及方法定义外部的时候,其值就是类对象本身。简单的说就是 self 的值就是 Test,所以两个定义方法是等价的。

除非在实例方法定义内,self 是实例对象,self 在类的内部其他部位就是其本身。
加上前面一个特性,类就是一段被执行的代码,就变的非常的强大了。

这个两个 特性被广泛的应用。例如我们熟悉的 has_many

class Post << ActiveRecord::Base
  has_many :post
end

has_many 是一个方法,这个方法的接受者 receiver 是默认的 self,也就是类本身,换句话说,has_many 这个方法是个类方法。我们这里在类的内部调用了一个类方法。

我们可以用一个非常简单的例子来看 has_many 是怎么实现的:

>> Test = Class.new => Test >> def Test.has_many(name) >> puts "Rails style has_many #{name}" >> end => nil >> class Test >> has_many "photos" >> end Rails style has_many photos => nil >>

这里如果我们把 Test.has_many 类方法内的代码,换成能动态生成一系列方法,能 include 外部的 module,或者其他元编程的技巧,就达到了类似 Rails 中 ActiveRecord 的效果。这些元编程的具体技巧我们以后在研究。

模块 module

module 跟 class 是绝代双骄,是因为他们都是对象,而且有很多相似指出。

其实说法不太对。

  • Module 是 Class 的父类:

    Class.superclass => Module

  • module 没有实例变量

  • module 没有 new 不能生成实例对象

module 内可以有常量

>> module Test >> PI=3.14 >> end => 3.14 >> Test.PI >> Test::PI => 3.14

module 的方法有两种,一种是 module 方法,这类方法可以直接调用。

>> module Test >> def Test.test_method >> puts "hello from module" >> end >> end => nil >> Test::test_method hello from module => nil

另一种是没有 module 名字的方法,这种方法不能直接调用,需要 mixin 到一个类中。

>> module Test >> def hello >> puts "hello" >> end >> end => nil >> Test::hello NoMethodError: undefined method `hello' for Test:Module from (irb):23

把 module 的方法添加到类中有两种方法。
一种是 include,方法会被添加到实例方法中。
一种是 extend,方法会被添加到类方法中。

继续前面的 module Test 的例子

>> class Class1 >> include Test >> end => Class1 >> Class1.new.hello hello => nil >> class Class2 >> extend Test >> end => Class2 >> Class2.hello hello => nil

module 常用的一个 hook/callback 是 included 方法,这个方法在 module 被 include 到一个类中的时候会被调用。

>> module Test >> def self.included(cls) >> puts "including module in class #{cls.name}" >> end >> end => nil >> class Class1 >> include Test >> end including module in class Class1 => Class1

本回完

本回讲了 class 跟 module 的一些东西。

本文的例子非常非常的简单,不过很多东西结合起来使用就会很强大,

例如,我们可以定义一个 module Test,include Test 的时候 extend 另外一个 module Test::ClassMethods,这时就会给当前类添加很多类方法。

这些类方法又可以来定义跟生成一部分功能。达到类似下面的效果:

class Artist
  include Mongoid::Document
  field :name, type: String
  embeds_many :instruments
end

如果你感兴趣,可以考虑一下 mongoid 怎么能实现上面的功能。

且听下回分解

还没想好下回讲什么,或者是 eval 系列,或者是 define_method,或者是 block/lambda/Proc.new

联系作者

如果你有任何问题,欢迎讨论。

作者: Hisea
web: http://hisea.me
email: zyinghai@gmail.com
weibo: http://www.weibo.com/zyinghai
twitter: https://twitter.com/zyinghai
github: https://github.com/hisea

Hisea.me 版权所有

写得不好,好的教程应当深入浅出,引人入胜!

#1 楼 @kevinhua 能不能具体点,我以后改进

#1 楼 @kevinhua 这哥们写的不是教程,你不要当教程看。应该看小说看。

#2 楼 @hisea 就是看这个教程,还没有直接看 Metaporgramming Ruby 来得爽快。需要设定情境,让读者很容易移情,进入你所设定的情境。这方面,有两本书写得很不错:敏捷开发、元编程。可以参考。

#2 楼 @hisea 另外,ruby-china 不适合放教程,字太小。需要放大看。

#5 楼 @kevinhua 本来这也不是教程,只是楼主对一些知识点的总结。让本来就具备这方面知识的人也可以顺便梳理一下思路。 楼主写这文章的目的也不是要你看完就明白类和模块是怎么回事,那些东西当然是要看书的,没人会指望看完一小页就学会了多少东西。 我觉得社区需要这样的有价值的知识点分享。

#4 楼 @kevinhua 恩,你说的两本书我都看过,我觉得他们写的都不错,适合初学者,只不过有一个问题,这种循序渐进尤其是元编程添加了很多故事情节的书,适合初学这学习,不适合入门之后的进阶速查或者研究。 在这方面 Rails 3 Way 或者 Eluquent Ruby 采取的就是另外一种方式。

你如果对 Ruby/Rails 的书感兴趣,我写过一片 26 本 Ruby/Rails 的书的简评: http://hisea.me/posts/tagged_with?tag_name=Books

至于我写的这个系列的元编程。是想总结一下自己的一些心得。一些是读书学到的,一些是自己领悟的,希望跟其他的朋友分享讨论。

#5 楼 @kevinhua 字体问题我也注意到了,打算以后整理整理弄成 pdf,或者电子书。

#8 楼 @hisea 确实你这个排版有点丑啊,正文都是黑色,“类的 self 和类的方法” 这样的 title 却是灰色。额。。

#9 楼 @ywencn 这个。。。我就是用 markdown 写的,ruby-china 所有的 h 都是灰色的。。

每回必读。。赞一个

还是去你发的 “广告” 里面看舒服!

写的不错,赞。每次都看看副标题会有什么新意。

#12 楼 @kimigao1986 是啊,等着好好排排版整理个电子书。

#13 楼 @piginzoo 谢谢,辛苦了啊。

#14 楼 @kenshin54 已经绞尽脑汁,快想不出来了。

写得不错 围观中

写得很好啊,期待后续

写的很不错,看完又有新收获,期待着 block,lamada 的部分了

Class.instance_methods 这个不错啊

#22 楼 @wxianfeng 是啊,Ruby 有很多 reflection 的方法,都很好用。

觉得写得不错

写得不错, #23 楼 @hisea 对这个问题理解得很好,元编程的基本问题是 self,而改变 self 有 2 种途径,一个是方法调用,第二个就是定义类。Dave Thomas 的视频将这个概念说得很透彻!

#25 楼 @googya 元编程 is all about self definee and scope.

#2 楼 @hisea 下面的 << 应该是 <,对吧?

class Post << ActiveRecord::Base
  has_many :post
end

#28 楼 @hisea 额,还没睡觉啊?

最近研究元编程!头晕,希望来出一些通俗的介绍文章!

写得很好啊:bowtie:

写的很好啊~

ps 你的网站打不开了,因为我看时间也比较久了,请问还在维护吗?

#34 楼 @jicheng1014 重启了一下服务器。谢谢提醒啊。最近没怎么写东西,瞎忙了。。打算过段时间搬完家专心写一点。

ery 有多少人读过《Ruby for Rails》 提及了此话题。 04月03日 10:56
需要 登录 后方可回复, 如果你还没有账号请 注册新账号