ruby元编程

对象模型

所有class定义之外的代码默认运行在顶级对象main中。

打开类

rubyclass更像是一个作用于操作符而不是类定义语句,它可以创建一个不存在的类,也可以打开一个已定义的类,然后向内添加新的方法和属性,这种技术称为“打开类”技术。
但是注意,当打开类重新定义新的方法时,如果跟该类已有的方法重名,原来的方法就会被覆盖,这称之为猴子补丁(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(在javaC#中类也是Class类的实例,Class的类也是Class,自我引用)一个对象的方法就是类的实例方法,那么一个类的方法就是Class的实例方法

"hello".class #=> String
String.class #=> Class

所有的类都最终继承于ObjectObject本身又继承于BasicObject(ruby对象体系的根节点)

String.superclass #=> Object
Object.superclass #=> BasicObject
BasicObject.superClass #=> nil

类是增加了三个方法的(new()allocate()superclass())Module

Class.superclass #=> Module
Module.superclass #=> Object
image

调用方法时发生了什么

方法查找

若有如下定义

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方法时会依据下图的祖先链寻找该方法。注意KernelObject中包含的一个Module,所以所有的类都具有这个Module中定义的实例方法(如print
我们也可以利用这种技术给Kernel增加一个方法,这个内核方法就可以为所有对象所用了。

image

self

每一行代码都会在一个对象中被执行——这个对象就是所谓的“当前对象”,当前对象可以用self来表示和访问,所有没有指明接受者的方法都在self上调用。
在类定义中当前对象self是在定义的类,同时self也是当前类,在对象的实例方法中self就是方法的接收者。

class Test
 puts self   #=> Test 当前类
 def foo
   puts self
 end
end
Test.new.foo #=> #<Test:0x007f8bf197b240> 当前对象,方法接收者
代码 图例
image
image

左侧的代码有右侧的祖先链,所以Book.new.print 会调用printable:print方法。每当类包含一个模块时,该模块会被插入到祖先链中,在类的正上方

代码 图例
image
image

Module extend self

extend self的作用就是可以在不include该模块的情况下按如下方式调用
一般extend self的模块会被当做一个只有类方法的工具类来使用

module M
 extend self
 def greet
   puts "hi"
 end
end
M.greet  # =>  hi

总结

image

方法

动态派发

使用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

image

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

测试

相对于测试普通代码,测试元编程代码引入了额外的维度,记住,元编程是编写代码的代码,因此你可能需要在两个层次上测试它

  • 测试自己的代码
  • 测试这个代码所生成的代码
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容

  • 类 方法 代码块 类宏 Eval方法 实例变量、方法、类 实例变量(Instance Variables)是当你使...
    youngiii阅读 1,094评论 0 51
  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,678评论 0 9
  • 01 Ruby元编程介绍和使用场景02 Ruby的类结构03 Singleton Method单例方法以及supe...
    Jayzen阅读 899评论 0 4
  • 什么时候需要读这本书? 扫过一遍基本的 Ruby 语法,自己也写过一些 Ruby 代码,觉得 Ruby 也就是一个...
    Forelax阅读 523评论 0 4
  • 这两天,有三件小事触动了我,内心地震动虽然没有波澜壮阔,却也让我在夜深人静时想起他们!今天我只叙说第一件事,另外两...
    ld熊壮壮阅读 288评论 0 0