在 Ruby 的世界中,程序员们享受着各种光怪陆离的语法糖,也经历着各种各样的陷阱。而这一切的根本就在于 Ruby 强大的元编程能力。元编程就像 Ruby 世界的魔法,当其是白魔法的时候可以帮助你把程序变得异常简洁,美观;而当其是黑魔法的时候,你将会迷失在一些很难解释的 Bug 中。
《Ruby 元编程》就是一部告诉大家如何使用,控制 Ruby 元编程魔法的秘籍。该书的写作手法非常值得称道,作者把所有的知识点浓缩在了一个星期的工作过程中,通过一个菜鸟和大牛针对项目中遇到的各种问题的讨论,解决来引入各种元编程的知识点。 除此之外,在每个知识点的结尾处都还附带了有趣的小测验, 让读者可以跟随着菜鸟的思路,感受到自己在一步一步的掌握元编程的思想。这一切的编排让这本书读起来非常的有趣,并且书中的理论知识与项目中的实战相结合的讲述方式,让读者更容易去思考如何在自己的项目中运用这些知识。
我是从同事的口中听说这本书的,他读完这本书之后说:“这本书基本上改变了其写代码的习惯。”,作为一个码龄超过 10 年的程序员。如此赞誉一本书,让我决心一定要读一下这本书,读完之后,此书果然不负此赞誉。不管是初级程序员,还是编程高手,都应该读一下这本书,如果你是 Ruby 程序员,那么这本书可以算是必读书之一。该书分为 2 个部分。第一部分从对象模型,方法,代码块,类定义等方面一一剖析 Ruby 的设计原理,然后再通过实例告诉大家如何在实际应用中有效的利用这些设计原理,同时作者还非常善良的提醒了大家在使用这些技巧时的注意事项,防止这些魔法变成黑魔法。第二部分是剖析 Rails 中使用到的各种元编程技巧,读过之后,对理解 Rails 底层实现裨益良多,当然, 对 Rails 无爱的读者可以直接略过。
对象模型
提到对象,程序员首先想到的就是类这个概念,在本书第一章中,作者首先对 Ruby 世界的类进行了一番基础的讲解:
- 不同于 JAVA 等静态语言,类定义中只能执行定义变量和方法的语句,在 Ruby 中,类定义的代码和其他的代码是一样的,可以在其中执行任何的 Ruby 语句。
- Ruby 天生具有打开一个已经存在的类,并动态修改其内容的能力,但需注意猴子补丁的问题。
- 类的实例变量是存储在对象中,实例变量与该对象的类没有关系,当给对象的实例变量赋值时,该实例变量就生成了,实例变量就像是一个挂载在对象上的 HashMap,每个对象都可以拥有自己不同的 HashMap。
- 方法的定义在对象自身的类中,因为“共享同一个类的对象也必须共享同样的方法”。但是,不能说 Class 有一个叫做“method”的方法,因为无法使用"Class.method"调用该方法,而要说 Class 有一个实例方法“method”,这意味着必须创建该类的实例对象,通过实例对象调用该方法。
- Ruby 中同样可以定义类方法,或者说类宏,定义方法时,在方法名前加“self.”或者“类名.”前缀即可, 然后可以在类中像使用关键字一样使用该方法,依靠类宏,可以实现很多非常简洁的 DSL。
- 类本身也是对象,所有实例对象上的规则,同样可以适用于类对象本身。
- 类的继承体系:
在第四章:类定义中, 作者引入了更多关于 Ruby 对象模型的高级概念:当前类,单件方法,EigenClass 等:
- 不管代码执行到哪个位置,都会有一个当前对象 self,相对应的,也总会有一个当前类的存在。当定义一个方法时,该方法就会成为当前类的一个实例方法。跟踪当前类在 Ruby 中也并不困难,当使用 class 或 module 关键字打开一个类的时候,当前类就是被打开的那个类,在类定义时,当前对象 self 和当前类都是类对象本身,在调用方法时,当前对象 self 是调用方法的实例对象,当前类是该实例对象的类。
- Ruby 中,可以针对某个实例对象添加方法,这样,该扩展就不会对该类的其他实例对象产生影响,这种只针对单个对象生效的方法称之为’单件方法‘(singleton method)。
- 每一个对象都有一个特有的隐藏类 EigenClass,EigenClass 是一个很特殊的类,它只能有一个实例,且不能被继承,但是其自身可以继承其它类. 只对某个对象生效的方法就是保存在这个对象的 EigenClass 中,像实例对象的单件方法和类对象的类宏。
- 引入了 EigenClass 之后的 Ruby 对象模型继承体系:
最后,作者非常简练的总结了关于 Ruby 对象模型的知识点,这些初看起来非常复杂的概念,当你深入进去之后,就会发现,复杂性慢慢褪去。一切都变得简单,清晰起来,如果把 Eigenclass、类和模块归结为一个东西的话(因为它们本质上的概念差不多,姑且统称为模块),Ruby 的对象模型可以总结为一下几条规则:
- 关于对象,只有 2 种对象,要么是实例对象,要么是模块对象,用于存放实例变量的绑定。
- 关于模块,它可以是 Eigenclass,类,或模块。用于存放方法和一些类实例变量。
- 关于方法,方法必须存在于一种模块中。
- 每个对象(包括模块对象)都有自己的 Eigenclass,用于保存当前对象的单件方法(类对象的就是类宏)。
- 除了 BasicObjec 类无超类以外,所有的模块对象都有且只有一个父类,即从任何模块对象只有一条向上直到 BasicObject 的祖先链。
- 一个实例对象的 Eigenclass 的父类是该实例对象的类,一个模块对象的 eigenclass 的超类是该模块对象的超类的 eigenclass。
- 在类对象中插入一个模块时,该模块会出现在该类的祖先链的正上方。
- 调用方法时,Ruby 总是先向“右”迈一步进入接收者真正的类中,然后向上进入祖先链。
代码块的迷思
对于 OOP 出身的程序员来说,关于 Ruby 对象模型的介绍比较容易理解,接受。而代码块则是来自于函数式编程的世界。因此,阅读本章时,OOP 程序员需要清空自己的固有思维来接收新的概念和思维方式。代码块极大的增强了 Ruby 代码的表现力。在本章中,作者先介绍了块的基础知识,如何定义,使用代码块,然后进一步介绍了 Ruby 世界中的所有可调用对象。同时,在该章节中还讲解了作用域的基本概念,以及如何使用代码块技术控制作用域的知识。
- 定义一个代码块的方式有 2 种 ,一是使用 do … end, 另外一种是用大括号“{}”把代码内容括起来。代码块定义时也是可以接受参数的。但是,只有在调用一个方法的时候才可以定义一个块。
- 块定义好之后,会直接传递给调用的方法,在该方法中,使用“yield”关键字即可回调这个块。
- 如果一个方法定义的时候使用了 yield 关键字,但是调用的时候却没有传递代码块,方法会抛出“no block given (yield) (LocalJumpError)”异常。
- 代码在运行的时候,除了需要代码外,还需要运行环境,即一组各种变量的绑定。代码块就是由代码和一组绑定组成的,代码块可以获得自己定义的局部变量的绑定,和上下文可见的实例变量,在代码块执行的时候,只会根据自己定义时可见的绑定来执行。业界把块这样的特性称之为闭包(Closure)。
- 代码运行时,需要一组绑定, 这组绑定在代码的运行过程中,还会发生变化,这种变化发生的根本原因就是作用域发生改变,每个变量绑定都有自己的作用域,一但代码切换作用域,旧的绑定就会被一批新的绑定取代。
- uby 程序只会在 3 个地方关闭前一个作用域,同时打开一个新的作用域, 这三个地方通常称之为作用域们(Scope Gate):
- 类定义: class…end;
- 模块定义: module…end;
- 方法定义: def…end;
- 代码块可以转化为可调用对象, 这样就可以把代码块当做对象处理。
- Ruby 中有 4 种创建可调用对象的方法:
- proc{…}
- Proc.new { …}
- lambda{…}
- & 操作符。该操作符只有在方法调用时才有效,在方法定义时,可以给方法添加一个特殊的参数,该参数必须为参数列表中的最后一个,且以 & 符号开头,其含义就是,这是一个 Proc 对象,我想把它当做一个块来用,如果调用该函数时,没有传递代码块,那么该参数值将为 nil。
- 可调用对象在 Ruby 中都是 Proc 对象,但是 lambda 和 proc 创建的 Proc 对象还是有些细微差别。主要体现在 2 个方面:
- return 关键字的行为,lambda 中,return 仅表示从 lambda 中返回, 而 proc 中,则是从定义 proc 的作用域中返回。
- 参数校验规则:lambda 中,参数个数不对,会抛 ArgumentError 错误,而 proc 中,则会尝试调整参数为自己期望的形式,参数多,则忽略多余的,参数少则自动补 nil。
法术集
本书在每个章节的知识点讲解过程中,还包含了很多实战的技巧,这些小技巧有的可以帮助程序员快速定位问题,比方说,使用 Object#instance_eval(), 查看一个对象的内部行为,一些可以帮助开发者优雅实现一些的功能,比方说通过类宏和动态定义方法实现的 attr_accessor。
- 动态调用方法,通过使用 Object#send() 方法,可以直到最后一刻才决定到底运行哪个方法。
- 动态定义方法,通过使用 Module#define_method() 方法可以传入一个方法名和一个代码块动态定义一个方法。
- Kernal#method_missing() 方法,通过该方法的特性和动态调用方法结合,可以优雅的实现程序调用的动态代理。
- 扁平化作用域,通过使用 Class.new() 代替 class 关键字,Module.new() 代替 module 关键字,Module#define_method() 代替 def 关键字,这样所有的定义都在一个作用域,共享了该作用域的所有绑定。
- 上下文探针,通过使用 Object#instance_eval() 和 Object#instance_exec() 方法,可以轻松查看实例对象的内部状态。
- 环绕别名,通过 alias 关键字从一个新定义的方法中调用原始的,被重命名的版本。该法术可以很容易的扩展一个已存在的方法。
- 代码字符串,通过使用 Kernal#eval() 方法,可以把字符串自己当作代码执行。
- 钩子方法,Ruby 中提供了很多监控对象模型变化的钩子方法,比方说 Class#inherited(), 当类被继承时会调用该方法,还有 Module#method_added,Method#method_removed 等等。这个技术给人很多想象空间。
- …
更多的法术,等待着读者到书中去找寻。
结语
“其实世界上根本就没有什么元编程,有的只是编程而已”,作者在第 6 章中的点睛之句,升华了这本书的主题。所谓编程就是通过代码去解决实际的问题,作为程序员,我们总是尽力去寻找最精巧、最舒服的解决问题的方式。而元编程所展示的所有技巧,手法就正好为我们提供了这样的方式。
感谢张逸对本文的审校。
给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ )或者腾讯微博( @InfoQ )关注我们,并与我们的编辑和其他读者朋友交流。
评论