对象模型
所有class
定义之外的代码默认运行在顶级对象main
中。
打开类
ruby
的class
更像是一个作用于操作符而不是类定义语句,它可以创建一个不存在的类,也可以打开一个已定义的类,然后向内添加新的方法和属性,这种技术称为“打开类”技术。
但是注意,当打开类重新定义新的方法时,如果跟该类已有的方法重名,原来的方法就会被覆盖,这称之为猴子补丁(MonkeyPatch
)
对象中有什么
实例变量
有如下类定义。
class MyClass
def my_method
@v = 1
end
end
与java
这样的静态语言不同,ruby
对象的类和实例变量没有关系,当给实例变量赋值时他们才会生成,因此如果new
一个MyClass
类而没有调用my_method
方法,就不会有@v
实例变量,这可以使用@instance_variables
方法验证
方法
可以通过#methods.grep /my/
来查找匹配my的方法
如果可以撬开ruby
解释器查看某个对象会发现,对象其实并没有包含一组方法,它只包含了实例变量和一个对自身的引用
重访类
重要概念:类自身也是对象
类和其他对象一样也有自己的类,叫Class
(在java
和C#
中类也是Class
类的实例,Class
的类也是Class
,自我引用)一个对象的方法就是类的实例方法,那么一个类的方法就是Class
的实例方法
"hello".class #=> String
String.class #=> Class
所有的类都最终继承于Object
,Object
本身又继承于BasicObject
(ruby
对象体系的根节点)
String.superclass #=> Object
Object.superclass #=> BasicObject
BasicObject.superClass #=> nil
类是增加了三个方法的(new()
、allocate()
、superclass()
)Module
Class.superclass #=> Module
Module.superclass #=> Object
调用方法时发生了什么
方法查找
若有如下定义
Module M
def my_method
puts 'my method'
end
end
Class D
include M
end
Class C < D
end
C.new.my_method #可以使用C.new.ancestors打印祖先链
当调用my_method
方法时会依据下图的祖先链寻找该方法。注意Kernel
是Object
中包含的一个Module
,所以所有的类都具有这个Module
中定义的实例方法(如print
)
我们也可以利用这种技术给Kernel
增加一个方法,这个内核方法就可以为所有对象所用了。
self
每一行代码都会在一个对象中被执行——这个对象就是所谓的“当前对象”,当前对象可以用self
来表示和访问,所有没有指明接受者的方法都在self
上调用。
在类定义中当前对象self
是在定义的类,同时self
也是当前类,在对象的实例方法中self就是方法的接收者。
class Test
puts self #=> Test 当前类
def foo
puts self
end
end
Test.new.foo #=> #<Test:0x007f8bf197b240> 当前对象,方法接收者
代码 | 图例 |
---|---|
左侧的代码有右侧的祖先链,所以Book.new.print
会调用printable:print
方法。每当类包含一个模块时,该模块会被插入到祖先链中,在类的正上方
代码 | 图例 |
---|---|
Module extend self
extend self
的作用就是可以在不include
该模块的情况下按如下方式调用
一般extend self
的模块会被当做一个只有类方法的工具类来使用
module M
extend self
def greet
puts "hi"
end
end
M.greet # => hi
总结
方法
动态派发
使用class#send(:method_name, param)
调用方法的好处是可以在代码运行期知道最后一刻才决定调用那个方法。
动态定义
使用class#define_method(: method_name, proc)
来定义一个方法
class MyClass
define_method :my_method do |arg|
arg * 3
end
endMyClass.new.my_method(3) #=> 9
动态删除
使用class#undef_method
删除方法,从而得到一个白板类。
class MyClass
undef_method puts
puts "hello" #=> 由于删除了puts方法,该句会报错
end
幽灵方法(慎用)
所有在类中没定义的方法都会调用Kernal
中的method_missing()
方法,该方法抛出NoMethodException
异常,可以重写该方法来收集那些“迷路“的方法调用
class MyClass
def method_missing(method, *args)
puts "you call: #{method}(#{args.join(',')})"
end
end
可以定义一个类,只要用等号给它赋值一个属性,该对象就会自动拥有该属性
class No
def initialize
@attr = {}
end
def method_missing(method, *args)
if method =~ /=$/
@attr[method.to_s.chop] = args[0]
else
@attr[method.to_s]
end
end
end
n = No.new
n.name = "lang"
n.age = 27
代码块
yield
可以通过yield
将原代码块执行,并做一些附加操作
class Scanner
def read_file
using(f = File.new) {
f.read(file_path)
f.close
}
end
# 定义using关键词即便出现异常也能自动关闭资源
def using(resource)
begin
yield
rescue
resource.close
end
end
end
作用域
一般情况下
局部变量只能存在当前作用域中,也就是说局部变量无法跨作用域调用,一般情况下程序会在以下三个作用域门关闭前一个作用域,同时打开一个新的作用域
类定义
模块定义
方法定义
实例变量会在实例的生存周期中都可见
扁平作用域
如果想让局部变量(绑定)穿过作用域门,则可以用方法调用来替代作用域门,具体来说就是用Class.new
代替class
关键字,Module.new
代替module
关键字,define_method
代替def
关键字
count = 1
Counter = Class.new do
define_method :get_count do
count
end
end
c = Counter.new
puts c.get_count
#instance_eval
使用该方法传入一个代码块,块的接受者会成为self
,因此代码块中可以访问接受者的私有方法和实例变量
class Test
attr_accessor :name
def initialize
@name = "lang"
end
end
t = Test.new
t.instance_eval do
@name = "zhou"
end
puts t.name #=> zhou
可调用对象
proc对象
尽管ruby中绝大部分东西都是对象,但是块不是,如果想存储一个块供以后使用需要一个对象才能做到,为了解决这个问题,ruby在标准库中提供了Proc
类,可以通过将代码块传给Proc.new()
方法来创建一个proc
,然后调用Proc#call
来执行,这种技术称为延迟执行
#第一种方法
inc = Proc.new {|x| x + 1} #=> Proc对象
inc.call(2) #=> 3
#第二种方法
dec =lambda {|x| x - 1} #=> Proc对象
dec.call(2) #=> 1
给一个方法传入代码块的方式有两种:
第一种,在方法末尾直接声明代码块,然后在方法内部通过yield
调用,但缺点是不能重复使用yield
(因为其是代码块而不是对象),比如再将yield
传给方法内部的方法
def foo(greeding)
puts "#{greeding} #{yield}"
end
foo("hello"){"lang"}
# or
foo("hello", &(Proc.new {"lang"}))
第二种,将Proc
作为参数传入方法中,可以通过&符号将Proc
对象转换为代码块,作为最后一个参数,然后通过Proc#call
调用
&的真正含义是,这是一个Proc
对象,我想把它当做代码块来使用,简单的去掉&操作符,就能得到一个proc
对象
def foo(greeding, &name)
puts "#{greeding} #{name.call}"
end
zhou = Proc.new {"zhou"}
foo("nihao", zhou)
类定义
.class_eval
如果想要在不知道类名的情况下打开类并且使用def定义一个方法,可以使用.class_eval
方法,如果想要打开一个对象(修改self
)可以使用#instance_eval
方法
def def_method_for_class(a_class)
a_class.class_eval do
def say_hi
puts "hi"
end
end
end
def_method_for_class(String)
'abc'.say_hi
类实例变量和类变量
由于类也是对象,所以在类定义的作用域中如果定义@var
(@同self),则该变量是类的实例变量,类实例变量不能在实例方法中调用
class Test
@var = 2 #=> 类的实例变量
def self.read
@var
end
def set_var
@var = 1 #=> 对象的实例变量
end
def get_var
@var
end
end
t = Test.new
t.set_var
puts Test.read #=> 2
puts t.get_var #=> 1
可以使用@@var
定义类变量,类变量可以在实例方法中访问
class Test
@@var = 2 #=> 类的实例变量
def get_var
@@var #=> 2
end
end
单件方法
只给某个对象增加一个方法,则这个方法叫做单件方法
str = "nihao"
def str.title?
if self.upcase == self
end
其实我们日常使用的类方法就是类实例的单件方法,而且其定义方式也跟单件方法一样
def MyClass.singleton_method; end
def str.singleton_method; end
类宏
如果类MyClass中有一批旧方法如 old_method1, old_method2已经弃用,对其的调用希望实际调用新方法new_method1, new_method2,如何优雅的解决?
class MyClass
#def old_method1; puts "old_method1"; end
#def old_method2; puts "old_method2"; end
def new_method1; puts "new_method1"; end
def new_method2; puts "new_method2"; end
def self.replace_method(old_method, new_method)
warn "#{old_method}已弃用,请用新方法#{new_method}"
define_method :old_method do |*args, &block|
send(new_method, *args, &block)
end
end
replace_method :old_method1, :new_method1 #=> self.replace_method(:old_method1, :new_method1)
replace_method :old_method2, :new_metho2d
end
用元编程实现的attr_accessor
class MacroClass
def self.prop(name)
define_method name do
instance_variable_get "@#{name}"
end
define_method name.to_s + "=" do |value|
instance_variable_set "@#{name}", value
end
end
prop :name #=> 等价于attr_accessor
def initialize
@name = "zhou"
end
end
eigenclass
每个eigenclass
只有一个实例并且不能被继承,它是对象的单件方法的存活之处。在调用一个方法时,接收者会先查询eigenclass
中有没有该方法(单件方法),如果有就直接调用,如果没有就沿着祖先链一直向上寻找。
对于类的eigenclass
就是存放类方法的地方
下图阐释了eigenclass
在祖先链的位置 以#开头的为eigenclass
eigenclass
的超类就是超类的eigenclass
,有了这种继承关系,可以在子类中调用父类的类方法(因为#D继承#C)一个对象的
eigenclass
类的超类是这个对象的类,一个类的eigenclass
的超类是该类的超类的eigenclass
打开对象的
eigenclass
定义单件方法
# 打开obj的enginclass,定义一个单件方法a_singleton_method
# 如果把obj换成类名,或在类定义中使用 class << self 则打开该类的eigenclass添加属性
class << obj
def a_singleton_method
'obj#a_singleton_method'
end
end
打开类的eigenclass定义类方法
# 打开obj的enginclass,定义一个单件方法a_singleton_method
# 如果把obj换成类名,或在类定义中使用 class << self 则打开该类的eigenclass添加属性
class C; end
class << C
def class_method
"C.class_method"
end
end
# 或
class C
class << slef
def class_method
"C.class_method"
end
end
end
类扩展和对象扩展Object#extend
extend
关键字可以代替include
关键字,用于将模块混含到类或对象中,唯一不同的是,使用extend
会使模块方法变成对象的单件方法,或成为类的类方法
module MyModule
def say_hi; puts "hi" end;
end
obj = Object.new
obj.extend MyModule
obj.say_hi #=> 成为对象的单件方法
class MyClass
extend MyModule
end
MyClass.say_hi #=> 成为类方法
方法别名alias和环绕别名
可以使用alias
关键字给方法定义别名
module MyModule
def say_hi; puts "hi" end;
alias :hi :say_hi #=> 注意两个方法名之间没有逗号
end
如果想要对某一个我们不能修改的库方法前后增加额外代码,而这个库方法在项目中已经用过无数次,我们不能修改每一处调用该如何处理?
这时可以使用环绕别名的技巧,该技巧的核心是:如果先定义别名再修改方法,则使用别名调用的时候还是调用的老方法,这样我们就可以先用别名把老方法存下来,然后重新定义这个方法,加上额外处理的代码后,再使用别名调用老方法
class String
alias :real_length :length
def length
if self.real_length > 5 #=> 使用原来的方法
"long"
else
"short"
end
end
end
puts "abc".real_length #=> 3
puts "abc".length #=> short
类扩展混入
上文说到,如果想将一个module
中的方法当做类的实例方法包含进来,可以使用include
关键字
如果想当做类方法包含进来,则可以使用extend
关键字,或者将该模块包含到类的eigenclass
中(class << self; ....; end
)
class Myclass
include MyModule #=> 作为实例方法包含进类
extend MyModule #=> 作为类方法包含进类
class << self
include MyModule #=> 作为类方法包含进类
end
end
module MyModule
#...
end
但是如果想部分当做实例方法,部分当做类方法mixin
到类中如何操作呢
在module
中创建一个类ClassMethods
,该类中包含想要定义成类方法的方法
在included
钩子方法中使用extend
方法将ClassMethods
类中的方法包含到包含者的eigenclass
中(成为类方法)
在ClassMethods
类外的方法被include时还是实例方法
将该module include
到类中
class Myclass
include MyModule
end
module MyModule
# 钩子方法,当模块被混含时调用,base为包含模块的类
def self.included(base)
base.extend(ClassMethods) #=> extend方法会把ClassMethod类中的方法包含到base的eigenclass中
end
# 成为包含者的实例方法
def instance_method
end
# 该类中的方法成为包含者的类方法
class ClassMethods
def xxx
#...
end
end
end
测试
相对于测试普通代码,测试元编程代码引入了额外的维度,记住,元编程是编写代码的代码,因此你可能需要在两个层次上测试它
- 测试自己的代码
- 测试这个代码所生成的代码