元编程是编写能在运行时操作语言构件的代码
在我们使用编辑器编写代码后,代码按照我们所写的内容进行编译,运行。在启动后我们无权再对代码进行干涉,而在一些业务场景中,我们想在不修改源代码的前提下,对一个类进行增强,这在 Java 中也是一个成熟的技术,例如反射,动态代理。但是 Java 所能给予的操作相比 Ruby,就显得格外的严格且复杂。
Ruby 是一门可以在运行时操作语言构建的工具。语言构建就是我们代码中的各个成员(对象,类,模块儿,实例变量等)。通俗来说可以使用 Ruby 在运行时对已有的类进行灵活的修改,例如修改一个方法的定义。实例变量的定义,甚至我们可以在运行时创建一个没有的类。下面我们使用一些伪代码来进行演示。
我们想对数据库进行操作,最初我们的想法就是写一个 Entity 基类,然后由子类继承
class Entity
# 提供访问器
attr_accessor :table,:id
def initialize table,id
@table = table
@id = id
Database.sql "insert into #{@able} 'id' values #{@id}}"
end
def set(col,val)
Database.sql "update #{@table} set #{col} = #{val} where id = #{@id}"
end
def get(col)
Database.sql "select #{col} from #{@table} where id = #{@id}"
end
end
class Movie < Entity
def initialize id
super 'movies',id
end
def title
get "title"
end
def title= value
set "title",value
end
def article
get "article"
end
def article= value
set "article",value
end
end
# --------------------------------
# 插入一条数据,简单且方便
movie = Movie.new(1)
movie.title = "猫捉老鼠"
movie.article = "相当不错"
上面的代码看起来是可以解决问题,但如果一张表的字段特别多,我都需要定义到 Movie 类中吗?能不能用更少的代码解决问题?我们使用Active Record
类库操作:
class Movie < ActiveRecord::Base
end
# --------------------------------
# 插入一条数据,简单且方便
movie = Movie.new(1)
movie.title = "猫捉老鼠"
movie.article = "相当不错"
我们看到这次在 Movie 继承ActiveRecord::Base
后,没有指定是哪个数据表,没有写 SQL,没有定义像上面一样的操作方法,我们就可以轻松插入数据。这底层到底是干了什么?
实际上是 ActiveRecord 在运行期,通过内省机制查看类的名字,通过 Movies 推断出表名为movies
,并在在读取数据表时,发现有 title,article 两个字段,动态的定义了两个同名的属性和相应的访问器。也是就动态的生成了 Movie#title 和 Movie#title=
这样的方法。
这就是 Ruby 的特点,也是我们要学习的元编程的一种表现形式,我们后面将试着分析和学习它。
现在我们有一个需求,将给定的字符串,添加一个后缀,我们可以定义一个函数
def appendTxt(content)
content = content + ".txt"
end
p appendTxt "alibaba" # => "alibaba.txt"
但这不填符合我们面向对象的方法,应该将这个函数封装到一个具体类中,定义它的职责。如果我们因此封装一个ApendTxtString
类,会不会导致类太多了,能不能让原本 Ruby 中的 String 具有新的行为,答案是可以的。我们可以直接修改 Ruby 原先定义的标准类。
# 这里相当于从新打开String的上下文,添加一个新的方法
class String
def appendTxt
to_s + ".txt"
end
end
# 我们使原先的String具有了新的行为
p "alibaba".appendTxt
class 更像是一个作用域操作符,当你第一次使用 class 时,会判断是否有这个类,如果没有进行创建,如果有则带回到类的上下文中,可以修改以往的方法,实例等等,这带给开发者很大的灵活性。
但如果使用不当,也会导致很大的问题,比如原本 String 拥有 appendTxt 方法,而很多地方都应用这个函数,而你一旦从新定义,就会导致全局的 bug,而且不容易排查,所以使用前一定要检查是否有重名方法。这种简单粗暴的修改在 Ruby 中也称为:猴子补丁:Monkeypatch
,后面我们也有一些其他办法来替代猴子补丁,如细化(Refinement)
,来将发生 bug 的可能降到最低
# 我们定义了一个类
class MyClass
def my_method
# 定义实例变量
@v = 10
end
end
# 创建实例
obj = MyClass.new
# 输出当前对象是哪个类的实例
obj.class # => MyClass
如果可以用 Ruby 解释器查看 obj 对象内部,我们可以发现什么?内部只有一个@v实例变量,而且仅属于 obj 对象
obj.my_method
p obj.instance_variables # => [:@v]
与 Java 不同,Ruby 中对象所属类和对象的实例变量没有任何关系,当你赋值创建一个实例变量时,它就出现了,如果你不使用obj.my_method
,这个对象就没有@v
这个实例变量
# 查看实例拥有哪些方法,因为每个类实例都是继承了Object类,所以会继承很多的方法
p obj.methods
# 这里用正则筛选一下
p obj.methods.grep(/my/)
一个对象内部其实只包含了自身的实例变量和对自身类的引用,方法并不在对象中,而在类中。这就是同一类的实例共享方法,但不共享实例变量的原因。
# String实例的方法
p String.instance_methods
# String实例方法+类方法
p String.methods
# String忽略继承的方法
p String.instance_methods(false)
在上面,我们先查看对象拥有的实例方法:obj.instance_methods
,后面我们又查看了类的实例方法:String.instance_methods
,发现并没有报错,那看来对象和类都拥有自己的实例方法,那能不能推断:类本身也是一个对象
在 Ruby 中类本身其实也是对象,是另一个类的实例。
# String类实际上是Class类的一个实例
p String.class # => Class
# 而Class类还是Class的实例
p Class.class # => Class
这确实是挺绕的。不过这么看来一个类所拥有的方法就是 Class 类的实例方法。
p Class.instance_methods(false) # => [:allocate, :superclass, :subclasses, :new]
这里看到 Class 的实例方法有四个,其中 new 是我们最常用的,allocate 是 new 方法的支撑方法,而 superclass 与我们 Java 中熟悉的继承有关,找到他的父类是谁?
p String.superclass # String的父类:Object
p Object.superclass # Object的父类:BasicObject
p BasicObject.superclass # BasicObject的父类:nil 空 到头了
p Class.superclass # Class的父类是 Module
p Module.superclass # Module的父类是 Object
可以看到 Class 是继承了 Module,并自身定义了实例化的操作。所以类和模块儿使用上看起来那么像。
每个类最终继承于 BasicObject,而每个类又是 Class 类的实例。
任何大写字母开头的引用,都代表着常量,而常量一般指不会修改的东西,在 Ruby 中常亮也可以看做是变量,而当你修改,编译器会发出警告,但仍然会进行修改。常量与变量最大的区别在于作用域的不同。
module MyModule
# 外部的MyConstant
MyConstant = "Outer constant"
class MyClass
# 内部的MyConstant
MyConstant = "Inner constant"
end
end
p MyModule::MyConstant
p MyModule::MyClass::MyConstant
我们以树形结构为例:
这里内部的 MyConstant 和外部的 MyConstant 实际上处于两个作用域中,是完全不同的东西,而我们可以通过::
来访问他们
如果处于模块儿较深的位置,想用绝对路径来访问外部的常量,可以使用 :: 表示路径的根位置。
Y = "a root-level constant"
module M
Y = "a constant in M"
::Y # => "a root-level constant"
end
Module.constants
会返回当前范围内所有常量,这里需要注意一点,Module 中定义 class,其类名也是一个常量。如果想知道当前代码所在路径,则可以知道Module.nesting
方法。
而我们一般会利用常量域的不同,作为名称空间,这样避免类名冲突
module M
class MyClass
def hi
puts "hello"
end
end
end
mc = M::MyClass.new
mc.hi
什么是对象?
对象就是一组实例变量外加一个指向其类的引用
什么是类?
类就是一个对象(Class 类的一个实例),外加一组实例方法和对其超类的引用。
当我们自定义一个类时,如果在一个复杂系统中,很有可能发生命名冲突,最好使用一个业务模块儿将自定义的类进行封装,这样发生命名冲突的概率也就降低了。
# 可能导致bug
class Test
end
# 通过 MyField::Test 来使用类,发生冲突的概率就降低了
module MyField
class Test
end
end
self
的东西 这里有两个概念:接收者和祖先链
比如说上面的代码 mc.hi()
,其中 mc 就是方法的接受者,在执行这个方法前需要先找到这个方法的定义,所以先到接收者中去找该方法,如果没有则找他的父类或者是引入的 Module 中寻找。而接收者,接受者内引入模块儿,父类共同构成了该对象的祖先链。
module M1
def m1_method
"m1.method"
end
end
module M2
def m2_method
"m2.method"
end
end
class MyClass
# 引入M1,模块儿在祖先链中位置为自身类上面
include M1
# 引入M2,模块儿在祖先链中位置为自身类下面
prepend M2
end
class AC < MyClass
end
# [AC, M2, MyClass, M1, Object, Kernel, BasicObject]
p AC.ancestors # 查看他的祖先链
如果在祖先链中多次引入一个 module,会怎么样?
module M1; end
p M1.ancestors # => [M1]
module M2
include M1
end
p M2.ancestors # => [M2, M1]
module M3
prepend M1
include M2
end
p M3.ancestors # => [M1, M3, M2]
在 Ruby 中我们常常使用print
,就好像所有对象都有 print 方法一样。但实际上这些方法来着Kernel
模块儿私有实例方法,因为 Object 引入了 Kernel,所以每个对象都可以调用 Kernel 方法,也叫做内核方法,我们当然也可以加入自己的方法,这样所有的对象都拥有了新的方法
当我们找到了该方法,如何去执行呢?
比如我们现在找到了该方法:
class MyClass
def initialize
@x = 1
end
def my_method
p self # => #<MyClass:0x000000010dcdb320 @x=1> self为obj
temp = @x + 1 # 这里实例变量@x 也是访问self内定义的
end
end
obj = MyClass.new
obj.my_method # obj调用my_method时,obj为当前对象self
p self # => main , 在顶级作用域下,调用 p ,接受者为main对象,main为self
请问:@x是属于哪个对象的,my_method 属于哪个对象?
一般情况下,会将最初方法的接收者作为当前对象,也就是作为 self,所有实例变量和方法都属于 self,如果没有明确指定接受者的实际上都指向 self,除非转而调用其他对象的方法,则 self 就会转为这个对象。
Ruby 中 private 修饰的方法,不能明确指定接受者来调用私有方法,只能通过隐性的接受者 self 调用。这与 Java 中对私有方法的定义是不同的
class MyClass
def hi
p "Hi "
end
def Hello
# 这里Hello 调用 私有方法hello ,使用的隐藏当前对象,也就是下面的obj
hello
end
private
def hello
p "hello"
end
end
obj = MyClass.new
obj.hi # 正常调用
obj.Hello # 正常调用
obj.hello # 无法调用,因为private方法不能指定接收者调用,只能隐性调用,也就是内部调用
如果没有调用任何方法,那这时谁是 self 呢?
# main
p self
# Object
p self.class
在 Ruby 程序运行时,Ruby 解释器创建一个名为 main 对象作为当前对象,这个对象有时被称为顶层上下文。此时处于调用堆栈的顶层
在定义类或模块儿时(且在任何方法定义之外),self 对象由这个类或模块儿本身担任
class MyClass
self # => MyClass
end
在前面我们使用了猴子补丁对原有的类进行修改,但这一操作是全局性的,如果把控不好,会导致许多隐性的 Bug,所以 Ruby 又引入了细化(refinement)
,起到同样的作用,但是可以限制作用域。
module StringAppend
# 细化 String 标准类库,传入一个Block
refine String do
# 在Block内,定义一个append_txt方法,现在self转为String,相当于给String定义实例方法
def append_txt
to_s + ".txt"
end
end
end
module StringStuff
# 上面定义好,并未生效,需要主动启动 using
using StringAppend
# 这里正常执行
p "alibaba".append_txt
end
# 这里就会报错,因为跳回了顶层上下文,这里没有引入对String细化,
# 所以通过细化,可以控制修改的访问范围,不会使全局都看到这个修改
p "taobao".append_txt
细化只在两种场合有效:
refine
代码块内部using
语句位置到模块儿结束,或者到文件结束(在顶层上下文使用 using)class MyClass
def my_method
p "old method"
end
def other_method
my_method
end
end
module MyClassRefinement
refine MyClass do
def my_method
p "new method"
end
end
end
# 在顶级上下文中使用using
using MyClassRefinement
obj = MyClass.new
# 这里已经更新为细化后修改的内容
obj.my_method # => new method
# 这里仍维持原先的内容
obj.other_method # => old method
这里虽然使用了细化,但当其他实例方法调用细化方法,还是会调用之前定义的代码,如果直接调用细化方法,则修改为细化内容。需要注意:Ruby 规定虽然可以在一个普通的模块儿中调用 refine 方法,但不能在类中调用这个方法
include
方法时,模块插入祖先链中,位置在类的正上方,使用 prepend 方法包含一个模块儿时,这个模块儿也会被插入祖先链中,位置在类的正下方