Ruby Ruby 元编程(第二版)--- 类定义

qinsicheng · 2023年01月28日 · 最后由 Mark24 回复于 2023年09月25日 · 660 次阅读

类定义

​ 在类定义中,Java 与 Ruby 有着极大的不同,在 Java 中类定义就好像你对编译器说:这是我希望对象的行为,但在对象创建前或者使用方法前,什么也不会发生。而 Ruby 的类定义不仅仅是规定对象的行为方式,实际上也是运行代码。

​ 这种思想催生出两种法术:类宏可以来修改类,环绕别名可以在其他方法前后封装额外的代码,为了最大程度使用这些法术,我们将介绍单件类

​ 学习之前我们需要提醒,类不过是增强的模块,所有关于类定义的,对模块也同样适用

深入类定义

​ 类中不仅能定义方法,也可以放入任何代码进行执行

class MyClass
  puts "Hello"  # => Hello
end

和方法和块类似,类定义也会返回最后一条语句的值

result = class MyClass
    self
end
p result  # => MyClass

​ 定义类或模块儿时,类本身充当当前对象 self 的角色,因为类和模块儿也是对象,所以可以充当 self,这里我们引入一个相关的概念:当前类

当前类

至今为止,有几个概念混杂在一起,当前对象,当前类,当前作用域

​ 无论程序在哪个位置,都会有一个当前对象 self,同样也总是有一个当前类或模块儿的存在,定义一个方法时,这个方法将成为当前类的一个实例方法。

​ self 可以获取当前对象,但是 Ruby 中并没有相应的方法获取当前类的引用,我们这里有几个规则

  1. 在程序的顶层,当前类为 Object,这是 main 对象所属的类 (这就是在顶层定义方法会成为 Object 实例方法的原因)
# 这里定义的是private实例方法,当前类为Object,所以子类也会继承到这个方法
def say_hello
    p "Hello World"
end

class User
    def hi
        say_hello
    end
end

obj = User.new
# 这里是可以调用成功的
obj.hi  
say_hello
obj.send :say_hello
# 这里无法调用
obj.say_hello  # 因为say_hello是一个私有方法
  1. 在一个方法中,当前类就是当前对象的类,比如我们在一个函数中定义另一个函数,这个内部定义的函数属于当前对象的类
class User
    # 这里一旦执行,当前类为User
    def one 
        # 这里定义的函数生效,并属于User,
        def two
        end
    end
end
obj = User.new
obj.one
p User.instance_methods(false)  # => [:one, :two]
  1. 当使用 class 或者 module 打开一个类时,这个类成为当前类

如果我们想将类为参数,动态的给类添加一个实例方法,我们该如何操作

def add_method_to(a_class)
  # TODO : 在 a_class上定义方法 m()
end

这里我们引入 class_eval 方法

class_eval 方法

Module#class_eval方法会在一个已存在类的上下文中执行一个块儿。这听起来和obj.instance_eval很像。

def add_method_to(a_class)
  a_class.class_eval do
    def m; 'Hello'; end
  end
end
add_method_to String
"abc".m
  1. Module#class_eval 会同时修改 self 和当前类,所以可以定义类的实例方法

  2. Object#instance_eval 只修改 self(这并不绝对的,我们后面会讲)

Module#class_eval功能和class关键字类似,但更强大,因为 class 关键字传入 常量,而 Module#class_eval,只要是代表类的变量即可使用。比我们想在运行期决定具体的类

class也是作用域门,会切换作用域,而Module#class_eval则是扁平作用域可以引入外部变量

Module#class_eval 也有 class_exec 可以接收额外的代码块作为参数

instance_eval 和 class_eval 方法该如何选择

​ 这取决于两者的特点,instance_eval 方法打开非类的对象,而用 class_eval 方法打开类的定义,然后使用 def 定义方法

类实例变量

Ruby 解释器假定所有的实例变量都属于当前对象 self,在类定义时也是这样

class MyClass
  @my_var = 1
end

​ 这里在 MyClass 中定义@my_var,self 为 MyClass,所以@my_var归属 MyClass,也就是类实例变量

这里需要声明,类实例变量 和 类实例化对象的实例变量是不同的

class MyClass
    # 这里当前类为MyClass,self也为MyClass,这里定义@my_var实例变量,所属MyClass
    @my_var = 100
    # 定义MyClass的read方法,一个指向MyClass的类方法,访问@my_var是可以的
    def self.read; @my_var; end
    # 定义MyClass的实例方法write,这里的@my_var 和 外面的@my_var 并不是一个变量,作用域不同
    def write; @my_var = 2; end
    # 这里一样,是访问不到外部的@my_var,除非调用write方法,给类的对象创建一个@my_var
    def read; @my_var; end
end
obj = MyClass.new 
p obj.read  # nil
obj.write # 定义 @my_var = 2
p obj.read  # @my_var = 2
p MyClass.read # @my_var = 100

​ Ruby 解释器假定所有的实例变量都属于当前对象 self,在类定义时也如此。

一个类实例变量只可以被类本身所访问,而不能被类的实例或子类所访问到

类变量

​ 如果想在类中定义变量,可被子类或者实例对象访问到,可以使用类变量,它更像是 Java 中的静态变量。

class C
    @@var = 1
end
class D < C
    def hi
        @@var
    end
end
obj = D.new
p obj.hi

需要注意一点,盲目的使用类变量也会有问题

​ 不允许在顶级上下文中定义类变量,因为 main 对象所属 Object 类,定义类变量,则所有 Object 子类都会继承这个类变量,也就有修改类变量的可能,在最新的 Ruby 编译器中已经对这个行为禁止,并爆出错误

@@var = 1
class User
    @@var = 2
end
p @@var

这里再回顾一下 Ruby 中的操作符

p false || true  # 一个为真则为真
p false && true  # 一个为假都为假
p nil || "a"     # 除了 nil 和 false,其他都为真
p "a" || nil     # || 遇到真则返回
p "a" || 'b'

p nil && "a"     # && 遇到假则返回
p "a" && nil
p "a" && "b"

类对象是否可以访问到类实例变量?

class MyClass
    # 这里@var 属于 MyClass,因为MyClass也是一个对象
    @var = 1
    def get
        # 这里访问,self为MyClass.new,作用域分离
        @var
    end
    def self.get
        @var
    end
end
p MyClass.new.get  # 无法访问到
p MyClass.get      # 正常输出 1

obj.instance.eval 改变 obj 为 self,如果在 Block 内定义实例变量,则该实例变量属于 obj

Class.class_eval 改变 Class 为 self,同时改变当前类,定义实例变量属于这个类

class MyClass
    def self.get
        @var 
    end

    def get
        @var
    end
end

MyClass.class_eval do 
    @var = 1
end

p MyClass.get  # => 1
p MyClass.new.get  # => nil

单件方法

我们现在想要修改一个类的实例方法有三种办法

class MyClass
end
# 1.0 猴子补丁
class MyClass
  def one
  end
end
# 2.0 细化
module MyClass_Plus
  refine MyClass do
        def two
        end
    end
end
# 3.0 单件方法
obj = MyClass.new
def obj.tree
  # 方法体
end
obj.tree

​ 单件方法我们可以看到是在对象上操作,定义的函数也只针对这个对象,其他对象并没有这个方法,所以叫做单件方法,语法:def obj.method_name

类方法的真相

​ 类方法的实质是一个类的单件方法,因为类也是一个对象,给类定义单件方法,就是类方法。

类宏

​ Ruby 中的对象是没有属性的,对外只提供方法。所以在最初我们访问对象的实例变量时,可以写 get,set 方法,但是这会很麻烦,所以我们使用Module#attr_accessor :var访问器,这也叫做类宏,所属于Module#attr_*,类宏看起来很像关键字,实际上只是普通的方法,只不过可以在类定义中使用

使用类宏

我们原有的 Book 类中有名为:GetTitle , title2 , LEND_TO_USER,但是按照 Ruby 的惯例,他们应该分别命名为:get_title,title.lend_to_user,不过其他项目也在使用 Book 类,而我们不能修改那些项目,如果简单修改方法名,就会破坏其他的调用者

我们可以使用类宏声明这些旧方法名已被弃用,这样就可以修改方法名了

class Book
  def title ; end
  def subtitle ; end

  def self.deprecate(old_method,new_method)
    define_method(old_method) do |*args,&block|
      warn "Warning: #{old_method} is deprecated , Use #{new_method}"
      send(new_method,*args,&block)
    end
  end

  deprecate :GetTitle, :title
  deprecate :LENT_TO_USER, :lent_to
  deprecate :title2, :subtitle
end

b = Book.new
b.LENT_TO_USER("Bill")

单件类

提问:单件方法,类方法的信息是保存在哪里?

​ 首先不在对象中,因为只有类和模块儿可以定义方法

​ 其次也不在类中,因为无法在类的对象中共享,它们就好像是一个独立个体,存在与某个与当前类有关的地方,这个地方就是单件类,负责存储单件方法。

那我们该如何访问到单间类内?如何看到它?

两种方式:

class MyClass
end
obj = MyClass.new
single_class = class << obj
    # 返回单件类
    self
end
p single_class  # => #<Class:#<MyClass:0x0000000108beb5c8>>
p single_class.class # => Class
class MyClass
end
obj = MyClass.new
# 访问对象所属单件类,每个对象的单件类都不同
# #<Class:#<MyClass:0x00000001051f3a78>>
p obj.singleton_class
other = MyClass.new
# #<Class:#<MyClass:0x00000001051f3578>>
p other.singleton_class

​ 单件类只有一个实例,且无法被继承,单件方法就定义在单件类中

补充方法查找

class C
  def a_method
    'C#a_method'
  end
end

class D < C ; end

obj = D.new
p obj.a_method

我们画出 obj 一起祖先链的图,先不考虑单件类和模块

image-20230127215743350

单件类 和 方法查找

单件类的超类是什么?

class MyClass
end
obj = MyClass.new
# #<Class:#<MyClass:0x00000001051f3a78>>
p obj.singleton_class
# 对象的单件类的超类 就是 对象的所属类
p obj.singleton_class.superclass  # => MyClass

单件类是否在祖先链中,因为这涉及到方法的查找

​ 单件类是存在于祖先链中的,而且单件类的超类为对象的所属类,所以在祖先链中排在当前类之右边。方法查找也是按照这个顺序进行查找的。所以对象访问方法时,是先在单件类中访问,然后再去当前类中访问。

类的单件类的超类就是超类的单件类

class D
end
class E < D
end

p D.singleton_class   # => #<Class:D>
p E.singleton_class   # => #<Class:E>
p D.singleton_class.superclass  # => #<Class:Object>
p E.singleton_class.superclass  # => #<Class:D>

上面的定义看起来有点儿绕,Ruby 为何这样设计?

​ 因为这样就可以在子类中调用父类的类方法

image-20230103164540176

我们再来画一下有了单件类后的祖先链和方法查找

image-20230127220319776

七条规则

  1. 对象:要么是普通对象,要么是模块儿
  2. 模块:可以是普通模块,一个类或一个单件类
  3. 方法:存在与一个模块中,通常定义在类中
  4. 对象都有自己真正的类,要么是普通类,要么是单件类
  5. 除了 BasicObject 没有超类,其他的类都有一个祖先
  6. 一个对象的单件类的超类,就是这对象的类
  7. 一个类的单件类的超类,就是这个类的超类的单件类
  8. 调用一个方法时,Ruby 先找到接收者真的类,再向上进入祖先链

类方法的语法

class MyClass
end
# 1
def MyClass.one ; end
# 2
class MyClass
    def self.two ; end
end
# 3
class MyClass
    class << self
        def three ;end
    end
end

单件类 和 instance_eval 方法

​ 之前我们说instance_eval修改 self,实际上也修改当前类为接收者的单件类。

s1 = "abc"

s1.instance_eval do
  # 这里的swooh!self对象为“abc”的单件方法
    def swoosh!
        reverse
    end
end

p s1.swoosh!   # => cba
s2 = 'qsc'  
p s2.respond_to?(:swoosh!)  # => false

类属性

我们知道,使用Module#attr_accessor可以为对象创建属性

class MyClass
    attr_accessor :name
end

obj = MyClass.new
obj.name = "张三"
p obj.name 

如果我们想给类创建对象怎么办?可以在 Class 中定义类宏,每个类实际上是 Class 的实例对象,这样也就拥有了自己的属性

class MyClass
end

class Class
    attr_accessor :name
end

MyClass.name = "张三"
p MyClass.name

但是这样每个类都拥有了属性,我们只希望 MyClass 中拥有属性

class MyClass
    class << self
        attr_accessor :name
    end
end

MyClass.name = "张三"
p MyClass.name

模块儿的麻烦

我们试图在模块中定义模块的类方法,然后在一个类中引用该模块,试图将模块的类方法转为类的类方法

module MyModule
    def self.my_method; "Hello" end
end

class MyClass
    include MyModule
end

MyClass.my_method  # => 报错,因为MyModule中的my_method为一个单件方法,不能被触碰

我们看看我们能解决这个问题吗?将模块中的方法,转为类的类方法

module MyModule
    def my_method; "Hello" end
end

class MyClass
    class << self
        include MyModule
    end
end

MyClass.my_method  # => "Hello"

​ my_method 方法是 MyClass 的单件类的一个实例方法,这样也就是 MyClass 的类方法,这种技巧叫做类扩展,同样的技巧也适用于对象,毕竟类也是一个对象

对象扩展

module MyModule
    def my_method()
        p "Hello World"
    end
end
class MyClass
end
obj = MyClass.new
class << obj
    # 单件类所属obj,所以引入的方法,会作为对象的单件方法
    include MyModule
end
obj.my_method

​ 这称为对象扩展

Object#extend

类扩展,对象扩展 因为用的很多,所以 Ruby 提供了Object#extend方法

module MyModule
    def my_method()
        p "Hello World"
    end
end
class MyClass
    extend MyModule
end
MyClass.my_method

obj = MyClass.new
obj.extend MyModule
obj.my_method

方法包装器

如何在原有函数不修改的前提下,对方法做增强,在此之前我们介绍一些新的东西

方法别名

alias_method :new_method_name , :old_method_name 对方法起一个别名

class MyClass
    def one
        p "Hello one"
    end
    alias_method :two,:one
end

obj = MyClass.new
obj.one  # => “Hello one”
obj.two  # => “Hello one”

​ 别名在 Ruby 中几乎随处可见,例如String#size 就是 String#length方法的别名,Inteager 有一个方法有至少五种别名

如果先给一个方法起别名,又重新定义这个方法,我们看看会发生什么?

class MyClass
    def one
        p "Hello one"
    end
    alias_method :two,:one
    def one
        p "good morning"
    end
end

obj = MyClass.new
obj.one  # => "good morning"
obj.two  # => "Hello one"

​ 重定义方法时,并不是修改这个方法,而是定义一个新的方法,并将之前存在的方法名从新绑定,只要老方法还存在一个绑定,就仍可调用,这种先定义别名再重新定义方法的思想是一种有趣技巧的基础,我们举例说明

环绕别名 (类似于动态代理的效果)

  1. 给方法定义一个别名
  2. 重定义这个方法
  3. 新方法中调用老的方法
class MyClass
    def get_users
        p "从数据库中获取数据"
    end
end
# 现在我们想对 MyClass#get_users做一些增强处理
class MyClass
    alias_method :get_users_origin,:get_users

    def get_users
        p "检查用户权限"
        p "开启事务"
        get_users_origin
        p "提交事务"
    end
end

obj = MyClass.new
obj.get_users

环绕别名的一个缺点在于它污染了你的类,添加了一个额外的名字,如果想解决这个问题,可以在添加别名之后,想办法把老版本的方法变成私有的,Ruby 中 公有 和 私有 实际上是针对的方法名,而不是方法本身

环绕别名的另一个缺点与加载有关,不要尝试加载(load)两次环绕别名,这里留给你自己思考

环绕别名的最主要的问题在于它是一种猴子补丁,它有可能破坏已有的代码,Ruby2.0 增加了两种额外的方式来为已有方法保证新的功能

更多方法包装器

细化:使用细化,可以从新定义方法,如果定义重名方法,使用 super 则可调用到原先的内容,叫做:细化封装器

class MyClass
    def get_users
        p "从数据库中获取数据"
    end
end
# 现在我们想对 MyClass#get_users做一些增强处理
module MyClassRefinement
    refine MyClass do
        def get_users
            p "检查用户权限"
            p "开启事务"
            super
            p "提交事务"
        end
    end
end

using MyClassRefinement

obj = MyClass.new
obj.get_users

Module#prepend:因为会将引入 module 放入当前类的祖父链位置的前面,所以也会覆盖掉当前类中定义的方法,使用 super 则可调用到原先的内容,这种技术称为:下包含包装器

class MyClass
    def get_users
        p "从数据库中获取数据"
    end
end
# 现在我们想对 MyClass#get_users做一些增强处理
module ExplicitMyClass
    def get_users
        p "检查用户权限"
        p "开启事务"
        super
        p "提交事务"
    end
end
class MyClass
    prepend ExplicitMyClass
end
obj = MyClass.new
obj.get_users

测试:打破数据规律

让 1 + 1 = 3

绝大部分 Ruby 操作符实际上是方法,例如整数的 + 只是名为 Fixnum#+ 方法的语法糖,编写 1+1 时。实际上为:1.+(1)

class Fixnum
    alias_method :old_plus , :+

    def +(value)
        self.old_plus(value).old_plus(1)
    end
end
p 1+1

务必要慎用这种能力,实际上我们发现 Ruby 中的规则简单,小巧,易操作

总结:

  • 类定义对 self 和当前类的影响
  • 熟悉单件方法单件类,重新认识对象模型和方法查找
  • 学习了类实例变量,类宏和下包含包装器

这些规则同时适用于类和模块

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