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

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

广告: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: [email protected]
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
需要 登录 后方可回复, 如果你还没有账号请 注册新账号