在类定义中,Java 与 Ruby 有着极大的不同,在 Java 中类定义就好像你对编译器说:这是我希望对象的行为,但在对象创建前或者使用方法前,什么也不会发生。而 Ruby 的类定义不仅仅是规定对象的行为方式,实际上也是运行代码。
这种思想催生出两种法术:类宏可以来修改类,环绕别名可以在其他方法前后封装额外的代码,为了最大程度使用这些法术,我们将介绍单件类
学习之前我们需要提醒,类不过是增强的模块,所有关于类定义的,对模块也同样适用
类中不仅能定义方法,也可以放入任何代码进行执行
class MyClass
puts "Hello" # => Hello
end
和方法和块类似,类定义也会返回最后一条语句的值
result = class MyClass
self
end
p result # => MyClass
定义类或模块儿时,类本身充当当前对象 self 的角色,因为类和模块儿也是对象,所以可以充当 self,这里我们引入一个相关的概念:当前类
至今为止,有几个概念混杂在一起,当前对象,当前类,当前作用域
无论程序在哪个位置,都会有一个当前对象 self,同样也总是有一个当前类或模块儿的存在,定义一个方法时,这个方法将成为当前类的一个实例方法。
self 可以获取当前对象,但是 Ruby 中并没有相应的方法获取当前类的引用,我们这里有几个规则
# 这里定义的是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是一个私有方法
class User
# 这里一旦执行,当前类为User
def one
# 这里定义的函数生效,并属于User,
def two
end
end
end
obj = User.new
obj.one
p User.instance_methods(false) # => [:one, :two]
如果我们想将类为参数,动态的给类添加一个实例方法,我们该如何操作
def add_method_to(a_class)
# TODO : 在 a_class上定义方法 m()
end
这里我们引入 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
Module#class_eval 会同时修改 self 和当前类,所以可以定义类的实例方法
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 方法打开类的定义,然后使用 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 一起祖先链的图,先不考虑单件类和模块
单件类的超类是什么?
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 为何这样设计?
因为这样就可以在子类中调用父类的类方法
我们再来画一下有了单件类后的祖先链和方法查找
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
修改 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
这称为对象扩展
类扩展,对象扩展 因为用的很多,所以 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"
重定义方法时,并不是修改这个方法,而是定义一个新的方法,并将之前存在的方法名从新绑定,只要老方法还存在一个绑定,就仍可调用,这种先定义别名再重新定义方法的思想是一种有趣技巧的基础,我们举例说明
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 中的规则简单,小巧,易操作
这些规则同时适用于类和模块