Ruby Ruby 元编程(第二版)--- 对象模型

qinsicheng · 2023年01月27日 · 最后由 irritatingdish 回复于 2024年11月20日 · 866 次阅读

元编程是什么?

​ 元编程是编写能在运行时操作语言构件的代码

​ 在我们使用编辑器编写代码后,代码按照我们所写的内容进行编译,运行。在启动后我们无权再对代码进行干涉,而在一些业务场景中,我们想在不修改源代码的前提下,对一个类进行增强,这在 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 类的实例。

image-20230124214842752

常量

​ 任何大写字母开头的引用,都代表着常量,而常量一般指不会修改的东西,在 Ruby 中常亮也可以看做是变量,而当你修改,编译器会发出警告,但仍然会进行修改。常量与变量最大的区别在于作用域的不同。

module MyModule
  # 外部的MyConstant
    MyConstant = "Outer constant"
    class MyClass
    # 内部的MyConstant
        MyConstant = "Inner constant"
    end
end
p MyModule::MyConstant
p MyModule::MyClass::MyConstant

​ 我们以树形结构为例:

image-20230124220328845

​ 这里内部的 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

调用方法时发生了什么?

  1. 找到这个方法定义
  2. 执行这个方法,Ruby 中需要借助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]

Kernel 模块儿

​ 在 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

​ 在定义类或模块儿时(且在任何方法定义之外),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

​ 细化只在两种场合有效:

  1. refine代码块内部
  2. 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 方法,但不能在类中调用这个方法

对象模型总结

  • 对象由一组实例变量和类的引用组成
  • 对象的方法存在于对象所属的类中(对类来说是实例方法)
  • 类本身是 Class 类的对象,类的名字只是一个常量
  • Class 类是 Module 的子类,一个模块儿基本上就是由一组方法组成的包,类除了具有模块儿的特性以外,还可以被实例化(使用 new 方法),或者按照一定的层次结构来组织(使用 superclass 方法)
  • 常量像文件系统一样,是按照树形结构组织的,其中模块儿和类的名字扮演目录的校色,其他普通常量扮演文件的校色
  • 每个类都有一个祖先链,这个链从每个类自己开始(pretend Module 会在类本身之前),向上直到 BasicObject 类结束
  • 调用方法时,Ruby 首先向右找到接受者所属的类,然后向上查找祖先链,直到找到该方法或达到链的顶端
  • 在类中包含一个模块儿,使用include方法时,模块插入祖先链中,位置在类的正上方,使用 prepend 方法包含一个模块儿时,这个模块儿也会被插入祖先链中,位置在类的正下方
  • 调用一个方法时,接受者会扮演 self 的对象
  • 定义一个模块儿(或类)时,该模块儿扮演 self 对象
  • 实例变量永远被认定为 self 的实例变量
  • 没有明确指定接收者的方法调用,都被当做是调用 self 的方法
  • 细化像是在原有的类上添加了一块儿补丁,并且会覆盖正常方法的方法查找,并且细化只在部分区域生效。

弱弱地问一个,头像是本尊吗?

xiaox 回复

不是不是 😂

需要 登录 后方可回复, 如果你还没有账号请 注册新账号