ruby元编程

作者: 五月的约修亚 | 来源:发表于2017-03-10 11:57 被阅读0次

    对象模型

    所有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
    

    测试

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

    • 测试自己的代码
    • 测试这个代码所生成的代码

    相关文章

      网友评论

        本文标题:ruby元编程

        本文链接:https://www.haomeiwen.com/subject/qcvngttx.html