Table of Contents generated with DocToc
元编程,即动态生成代码的代码。其本质上就是在运行过程中,动态的改变类或者实例的方法、属性等特性,在调用前不需要像静态语言那样预先定义好所有的代码。Ruby 动态语言的特性和其暴露出的 API 可以轻松的达到元编程所需的条件。
- 对象由一组实例变量和一个类的引用组成
- 类本身是 Class 类的对象。类名仅仅是一个常量
- 当类包含一个模块时,该模块会被插入到祖先链中,位于该类的正上方
# 获取对象的实例方法 obj.methods
# 相当于 Class.instance_methods
"1".methods === String.instance_methods
# 获取对象的实例变量
obj.instance_variables# 类自身也是对象,适用于对象的规则也适用于类
"1".class # String
String.class # Class
# 可以通过 superclass 获取到类的超类
String.superclass # Object
Object.superclass # BasicObject
BasicObject.superclass # nil
# 类也是模块,只是增强的 Module
Class.superclass # Module
Module.superclass # Object
# 当一个方法被调用的时候,Ruby 会沿着调用的接受者(receiver)的祖先链(ancestors)上寻找
# 祖先链会包括引用的模块
String.ancestors # [String, Comparable, Object, Kernel, BasicObject]
# Object 类包含了 Kernel 模块,因此 Kernel 进入了每个对象的祖先链在调用一个方法或属性时,即给某个对象传递消息,要求调用该对象的方法或属性。 Ruby 维持对这个接受者(self)的引用,以便在作用域内寻找方法调用时所需的条件。 在一开始运行 Ruby 时,解释器会创建一个名为 main 的对象作为当前对象,即顶级上下文,此时处于调用堆栈的顶层
对于调用私有方法而言:
- 如果调用方法的接受者不是自己,则必须指明一个接受者
- 私有方法只能被隐含的接受者调用
打开已有的类,为其增改方法的行为被称之为“猴子补丁”
class String
def to_n
self.to_i
end
end
"1".to_n # 1
# 除了方法之外,还能够打开类扩展模块,将模块的方法作为类的实例方法
class SomeClass
include SomeModule
end除了打开类以外,还能够为实例扩展方法
"string".extend(SomeModule)# 已知类
class Foo
end
# 扩展类方法
class << Foo
def hello(name)
puts "hello #{name}"
end
end
class Foo
class << self
def hello(name)
puts "hello #{name}"
end
end
end
class Foo
def self.hello(name)
puts "hello #{name}"
end
end
def Foo.hello(name)
puts "hello #{name}"
end
Foo.hello('123') # 调用类方法
# 扩展模块,将模块的方法作为类方法
class Foo
extend SomeModule
end同样的,可以扩展单例方法,即仅此实例可以调用
str = "1"
class << str
def hello
puts "hello"
end
end
str.hello # "hello"
# 注意,如果是通过这种形式,则不是单例方法
class << "1"
def hello
puts "hello"
end
end
"1".hello # errir但猴子补丁的问题是,如果后续增改的方法和已有的方法重名,则会造成未知的影响
动态方法,即将调研的方法名作为一个变量,可以在代码运行期间,真正的调用时才决定需要调用的方法,即动态派发
# ruby 对象中的 send 方法可以将方法名作为参数动态调用
# obj.send(:method, params, ...)
# 还可以带上代码块
# obj.send(:method, params) { |x| ... }
class Example
def echo
puts "send example"
end
end
Example.new.send(echo)
Example.new.send(:echo)除了动态调用以外,还能动态创建方法
# Module#define_method
class Example
=begin
define_method :method do |params|
# do
end
=end
# 注意,define_method 是个类方法
def self.create(name)
# 但创建的是实例方法
define_method name do |*args|
print args
end
end
end
Example.create("test")
Example.new.test(1,2,3)define_method 除了在 Class 内部使用以外,也可以直接在外部创建方法:
chars = %w(a b c)
chars.each_with_index do |char, index|
define_method char.to_sym do
puts index ** 2
end
end
c() # 4当调用对象中并不存在的方法时,会触发method_missing方法。可以通过覆写该方法,动态的自定义的处理缺失的方法
class Example
def method_missing(method, *args)
puts "You just called #{method} with #{args.join(',')}"
end
end
Example.new.not_exist_method 1 # You just called not_exist_method with 1被method_missing方法处理的消息,对于调用者而言,和普通方法相比没有区别,但对于接受者,实际上并没有这些方法,因此被称之为幽灵方法。一个捕获幽灵方法的调用,并把它转发给另一个对象的对象,称之为动态代理
通过method_missing处理的方法,不会相应respond_to?方法,如果需要,则还需覆写respond_to?方法
example = Example.new
example.wow('!')
example.respond_to?(:wow) # false
class Example
def respond_to?(method)
method === :wow || super
end
end
Example.new.respond_to?(:wow) # true当一个幽灵方法和一个真实方法的名称发生冲突时,则会调用真实方法。因此,为了避免莫名的命名冲突问题,可以令类继承自BasicObject而不是Object。或者通过undef_method来清除已有的方法
class Example
instance_methods.each do |m|
# 保留 __ 开头的方法
undef_method m unless m.to_s =~ /^__|method_missing|respond_to?/
end
end在 JavaScript 中可以通过代理类Proxy实现类似method_missing的效果
- Proxy
- Reflect
- Ruby Method Missing In Javascript
- JS Meta Programming
- Does Javascript have something like Ruby's method_missing feature?
class MethodMissing {
constructor(...args) {
console.log(`[MethodMissing:INIT] ${JSON.stringify(args)}`);
const handler = {
get: this._handleMissingMethod.bind(this)
};
return new Proxy(this, handler);
}
_handleMissingMethod(target, name) {
if (Reflect.has(target, name)) {
return Reflect.get(target, name);
}
return (...args) => this.methodMissing(name, ...args);
}
methodMissing(name, ...args) {
throw new Error(`Method ${name} is missing!`);
}
}
class ExampleClass extends MethodMissing {
constructor(name) {
super(name);
this.name = name;
}
methodMissing(name, ...args) {
if (name === 'echo') {
console.log(`Method ${name} cached. Called with args: ${JSON.stringify(args)}`);
} else {
super.methodMissing(name, ...args);
}
}
print(...args) {
console.log(JSON.stringify(args));
}
}
new ExampleClass('example-1').echo(1, 2, 3);
// [MethodMissing:INIT] ["example-1"]
// Method echo cached. Called with args: [1,2,3]
new ExampleClass('example-2').print(1, 2, 3);
// [MethodMissing:INIT] ["example-2"]
// [1,2,3]
new ExampleClass('example-3').puts(1, 2, 3);
// [MethodMissing:INIT] ["example-3"]
// Error:
// Method puts is missing!在方法中,可以通过Kernal#block_given?来判断是否传入了代码块。
当定义了一个块时,它会获取到当前环境中的绑定。当把块传给方法时,它会带着这些绑定一起进入方法内,形成闭包。
def example_method
x = "hello"
yield
end
x = "hi"
example_method do
puts x
end
# hi可以在块内定义额外的绑定,但这些绑定在块结束时就会消失:
def example
yield
end
x = 1
example do
x = 2
y = 1
end
puts x # 2
puts y # error程序会在三个地方关闭前一个作用域,并同时打开新的作用域:
- 类定义
- 模块定义
- 方法
只要程序进入上述三者的定义中,就会发生作用域切换。这三个边界的class, module, def 关键字充当了作用域门的标志。
v1 = 1
class Example # 作用域门:进入 Class
v2 = 2
local_variables # ['v2']
def example_method # 作用域门:进入 def
v3 = 3
local_variables # ['v3']
end # 作用域门:离开 def
local_variables # ['v2']
end # 作用域门:离开 Class
local_variables # ['v1']扁平化作用域,就是让绑定穿越作用域门。在如下的正常情况下,作用域门的内部无法获取到外部的变量:
v1 = 1
class Example
# can not get v1
def echo
puts v1
end
end
Example.new.echo # error, undefined local variable or method `v1'因此,想要达到目的,就不能使用作用域门的class/def/module关键字。
- 使用
Class.new替代class - 使用
Module.new替代module - 使用
Module#define_method替代def
v1 = 1
Example = Class.new do
puts v1
define_method :echo do
puts "echo #{v1}"
end
end
Example.new.echo除此以外,如果想在一组方法之间共享一个变量,则可以把这些方法定义在那个变量所在的扁平作用域中:
def define_methods
shared = 0
Kernel.send :define_method, :counter do
shared
end
Kernel.send :define_method, :inc do |x|
shared += x
end
end
define_methods
counter # 0
inc(4)
counter # 4通过Object#instance_eval方法传入的块被称之为上下文探针,因为块内可以获取到对象内部的上下文。
class Example
def initialize
@v = 1 # 实例属性
end
def echo
puts @v
end
end
obj = Example.new
obj.instance_eval do
@v = 2
end
obj.echo # 2块的延迟执行:通过把块传递给Proc.new/lambda/proc来创建一个Proc对象,之后通过Proc#call进行调用。
inc = Proc.new { |x| x + 1 }
inc.call(2) # 3
dec = lambda { |x| x - 1 }
# lambda 还有简写方法:
# dec = -> (x) { x - 1 }
dec.call(2) # 1
# proc 在 Ruby 1.9 之后仅是 Proc.new 的别名
product = proc { |x| x * 2 }
product.call(2) # 4lambda和proc在使用中有些许不同:
- 在
lambda中,return仅表示从当前lambda中返回 - 在
proc中,return代表从定义这个proc的作用域中返回 lambda严格限制参数数量的对应,即定义时和调用时,传入的参数数目必须一致
类只是一个增强的模块
和方法定义一样,类定义也会返回最后一条语句的值
result = class Example
self
end
result # Example按照之前的知识,想要打开一个已有的类,可以通过class关键字,使用类似类定义的语法。但如果类是一个变量,不知道类名称,则需要使用class_eval(别名为module_eval),在该类的上下文中执行块:
def add_method_to_class(some_class)
some_class.class_eval do
def echo
"hello"
end
end
end
add_method_to_class String
"1".echo # "hello"和instance_eval不同的是,instance_eval仅会修改self,而class_eval则打开了类后,修改self和当前类。
所有的实例变量都属于当前对象 self
class Example
@var = 1 # 实例变量
# 类方法
def self.read
@var
end
# 实例方法
def write
@var = 2
end
# 类方法
def read
@var
end
end
obj = Example.new
# 修改的是当前方法内的实例变量
obj.write
obj.read # 2
Example.read # 1类变量并不真正属于类,而属于类体系结构
# @@var 定义于 main 的上下文,属于 main 的类 Object,所以也属于
# Object 的所有后代
@@var = 1
# Example 继承自 Object,因此也共享了这个类变量
class Example {
@@var = 2
}
puts @@var # 2# 实例的单件方法
str = "123"
def str.echo
"hello"
end
str.echo # "hello"
# 类方法的实质是:它们是一个类的单件方法如果希望类可以暴露属性给外部,则需要定义拟态方法(访问器):读方法和写方法
class Example
# 写方法
def var=(val)
@var = val
end
# 读方法
def var
@var
end
end
obj = Example.new
obj.var = 1
obj.var # 1可以通过Module#attr_*关键字来快速定义访问器:
attr_reader读方法attr_writer写方法attr_accessor读写
类似attr_*这样的方法被称为类宏:本身是方法,但看起来很像关键字,而且只能在类中定义
class Example
def initialize
@a = 1
@b = nil
end
# 只读
attr_reader :a
# 读写
attr_accessor :b
end
obj = Example.new
obj.a # 1
obj.a = 2 # error
obj.b = 3
obj.b # 3目前已经知道,在类中直接include Module,将会增加类的实例方法:
module ExampleModule
def echo
"this is module function"
end
end
class ExampleClass
include ExampleModule
end
obj = ExampleClass.new
obj.echo
ExampleClass.echo # error如果想要利用include增加类方法,则需要:
class << ExampleClass
include ExampleModule
end
ExampleClass.echo除此以外,可以直接通过extend进行类扩展和对象扩展:
class ExampleClass
extend ExampleModule
end
ExampleClass.echo
obj = Object.new
obj.extend ExampleModule
obj.echo通过alias关键字可以给方法定义别名。而定义别名后,重新利用原有的方法名来定义方法,并不会改变旧的方法本身,还是可以通过别名来使用它,这种方式被称为别名环绕
class String
alias :real_length :length
def length
real_length > 5 ? "long" : "short"
end
end
"1".length # short