Ruby元编程笔记——类定义

本章内容理解:讲了Ruby对象模型,并且介绍了几种依赖于此模型的技术。

在Java和C#等语言中,直到你创建了该类的一个对象,然后调用对象的方法才会有实际的工作。

在Ruby中,类的定义有所不同。当使用class关键字时,并非是在指定对象未来的行为方式,相反,实际上是在运行代码。

Ruby对象模型介绍以及七条规则:

1.只有一种对象——要么是普通对象,要么是模块。

类就是对象,是class类的一个实例。类也是一个增强的模块,比模块多了new、allocate、superclass三个方法。

普通对象不可以使用new方法再建立一个实例,模块也是。

2.只有一种方法——它存在于一个模块中,通常是一个类中。

无论是类或模块的实例方法,类方法,都存在于类或模块中。单件方法比较特殊,但也存在于单件类中。

3.只有一种模块——可以是一个普通模块、一个类或者单件类

模块可以是普通模块,可以是一个类,即增强的模块。同时一个普通的模块无法被继承。

1
2
3
4
5
6
7
8
9
10
module M
M = 1
end
module D < M
D = 1
end
SyntaxError: (irb):15: syntax error, unexpected '<' module D < M
# 一个普通的模块无法被继承。

4.每个对象都有自己”真正的类”,要么是一个普通类,要么是一个单件类。

前面好理解,至于”要么是一个单件类”这句话,可以通过这个例子理解一下:

instance_eval方法把当前类改成了接收者s1的单件类。只有s1自己一个对象可以使用。

1
2
3
4
5
6
7
s1, s2 = "abc", "def"
s1.instance_eval do
def swoosh!; reverse; end
end
s1.swoosh! #=> "cba"
s2.respond_to?(:swoosh!) #=> false

5.除了BasicObject,任何类只有一条向上的、直到BasicObject的祖先链。

6.一个(普通)对象(区别于类这种对象)的单件类的超类是这个对象的类,一个类的单件类的超类是这个类的超类的单件类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class C
class << self
def a_class_method
'C#a_class_method()'
end
end
end
class D < C; end
obj = D.new
# 一个(普通)对象的单件类的超类是这个对象的类
puts obj.singleton_class.superclass #=> D
# 一个类的单件类的超类是这个类的超类的单件类
puts C.singleton_class.superclass #=> #<Class:Object>
puts C.superclass.singleton_class #=> #<Class:Object>

7.调用一个方法时,Ruby先向右迈一步进入接受者真正的类,然后向上进入祖先链。这就是Ruby查找方法的方式。

如果它有单件类(规则4),查找会从单件类开始。

为什么Ruby要以这种方式来组织对象模型?——这样就能在子类中调用父类的类方法。

1
2
3
4
5
6
7
8
9
10
class C
class << self # 这行代码表示开始为C定义类方法
def a_class_method
'C#a_class_method()'
end
end
end
class D < C; end
puts D.a_class_method #=> C#a_class_method() #=>在子类D中调用父类C的类方法。

依赖Ruby对象模型而来的技术概念:

1.在不知道类名的情况下打开一个类:
1
2
3
4
5
6
7
8
9
10
def add_method_to(a_class)
a_class.class_eval do
def m;
'Hello!';
end
end
end
add_method_to String
"abc".m # "Hello!"

这里给String类增加了一个m方法。

class_evalclass的区别:

class_eval使用扁平作用域,当前的绑定依然可见。

class则打开一个新的作用域,当前的绑定不可见。

class_evalinstance_eval的区别:

一般用instance_eval方法打开非类的对象,用class_eval方法打开类定义。

当前类

不管Ruby程序处于哪个位置,总存在一个当前对象:self。同样,也总有一个当前类或当前模块的存在,定义一个方法时,那个方法将成为当前类的一个实例方法。

2.类实例变量和类变量

类实例变量是Class类的实例——即继承Class的一般类的实例变量。一般类相对于Class类来说也就是个普通对象。它定义于一般类充当self的时刻,所以只能被它本身的类访问,不能被它的实例或者子类访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class C
@@v1 = 1 # 类变量以@@开头
@v2 = 2 # 这是类实例变量
def my_method1;
puts @@v1
end
def my_method2;
puts @v2
end
end
class D < C
def my_method1;
puts @@v1
end
def my_method2;
puts @v2
end
end
obj1 = C.new
obj1.my_method1 #=> 1
obj1.my_method2 #=> nil # 类实例变量不能被它的实例或者子类访问
obj2 = D.new
obj2.my_method1 #=> 1
obj2.my_method2 #=> nil # 类实例变量不能被它的实例或者子类访问
obj1.my_method1 #=> 3
# @@v1定义于main的上下文,属于main的类object
# 所以也属于object所有的后代
# 所以在C中定义的@@v1因为D#my_method1被改为3
# 也因为这个特性,类变量可以在别处被修改,使用它的情况很少,使用类实例变量较多。
3.类宏

类宏可以为任何ruby对象创建属性。

在给对象创造属性的时候用Module#attr_ accessor(),给类创造属性时用单件方法。

属性实际上只是一对方法,如果在单件类中定义了这些方法,那么他实际上会成为类方法。

Ruby对象并没有属性。如果希望有一些像属性的东西,就得定义两个拟态方法:一个读方法和一个写方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# class_definitions/attr.rb
class MyClass
def my_attribute=(value)
@my_attribute = value
end
def my_attribute
@my_attribute
end
end
obj = MyClass.new
obj.my_attribute = 'x'
obj.my_attribute # "x"
  • 写这样的方法(也叫访问器)很快就让人感到枯燥。可以通过Module#attr_ accessor()则可以生成读写方法:
1
2
3
class MyClass
attr_accessor :my_attribute
end
  • 为普通Ruby对象创建属性:
1
2
3
4
5
6
7
class MyClass
attr_accessor :b
end
obj = MyClass.new
obj.a = 2
puts obj.a #=> 2
  • 为类创建属性
1
2
3
4
5
6
7
8
class MyClass; end
class Class
attr_accessor :a
end
MyClass.b = 42
puts MyClass.b #=> 42

但这样做会让所有继承Class类的类都拥有这个属性。

1
2
3
# 接上段代码
MyClass1.b = 42
puts MyClass1.b #=> 42

所以考虑下一种做法:

  • 将类属性创建在该类的单件类中
1
2
3
4
5
6
7
8
9
10
11
12
class MyClass
class << self
attr_accessor :c
end
end
MyClass.c = 'It works!'
puts MyClass.c #=> 42
MyClass1.c = 'It works!'
puts MyClass1.c #=> 42
# => undefined method `c=' for MyClass1:Class (NoMethodError)
4.单件类

每个单件类只有一个实例,而且不能被继承。单件类也是一个对象单件方法的存活之所。

  • 单件方法适合在什么情况下使用

​ 仅仅针对于个别使用次数很少的对象。也就是用来增强某个类。

​ 类方法的实质是一个类的单件方法。

​ 类方法的实质就是:它们是一个类的单件方法。为什么要是单件方法呢?——只是这一个对象(类)能用它。

  • 怎样进入单件类?
1
2
3
class << an_object
# ...
end

如果对象有单件类,Ruby不是从它所在的类开始查找,而是从对象的单件类开始查找方法。

在单件类中,有一种运用情况是类方法,下面来看看类方法。

5.三种定义类方法的方法:
类扩展

通过向类的单件类中增加模块来定义方法。

  • 直接打开类
1
2
3
4
5
6
7
8
9
10
11
module MyModule
def my_method; "Hello"; end
end
class MyClass
class << self
include MyModule
end
end
puts MyClass.my_method #=> "Hello"
  • include方法扩展:
1
2
3
4
5
6
7
8
9
10
11
12
module MyModule
def my_method; "Hello"; end
end
obj = Object.new
class << obj
include MyModule
end
puts obj.my_method #=> "Hello"
puts obj.singleton_methods #=> "[:my_method]"
  • extend方法扩展:
1
2
3
4
5
6
7
8
9
10
11
12
13
module MyModule
def my_method; "Hello"; end
end
obj = Object.new
obj.extend MyModule
puts obj.my_method #=> "Hello"
class MyClass
extend MyModule
end
puts MyClass.my_method #=> "Hello"
6.方法包装器

有一个不能直接修改的方法,而我希望为这个方法包装额外的特性,这样所有的客户端都能自动获得这个额外特性。要明白方法包装器,先明白方法别名:

  • 方法别名

有点绕,先看较简单的初级形式,它只输出一个字符串:

1
2
3
4
5
6
7
8
9
10
11
12
class MyClass
def my_method
'my_method()'
end
alias_method :m, :my_method
alias_method :m2, :m
end
obj = MyClass.new
puts obj.my_method
puts obj.m
puts obj.m2

先给方法命名一个别名,然后重定义了它:

1
2
3
4
5
6
7
8
9
10
class String
alias_method :real_length, :length
def length
real_length > 5 ? 'long' : 'short'
end
end
puts "War and Peace".length
puts "War and Peace".real_length

当调用real_length时,它会调用原来的length并输出结果。

调用length时,它会通过real_length与5比较,然后得到结果。同样的,调用real_length时,它会调用原来的length并输出结果。

  • 定义环绕别名

​ 1.给方法定义一个别名

​ 2.重定义这个方法

​ 3.在新的方法中调用老的方法

​ 它的作用本质是往老方法中加入一些代码。它是一种方法包装器,下面来看看更多的方法包装器:

  • 用细化封装器代替环绕别名
1
2
3
4
5
6
7
8
9
10
module StringRefinement
refine String do
def length
super > 5 ? 'long' : 'short'
end
end
end
using StringRefinement
puts "War and Peace".length #=> "long"

​ 这里的只用一个super关键字调用原来的方法并与5比较得出结果。

​ 细化封装器的作用范围只到文件末尾处,比环绕别名安全,因为环绕别名是全局性的。

​ 不过还有一种方法包装的技术,使用module_prepend方法:

  • 下包含包装器:
1
2
3
4
5
6
7
8
9
10
11
module ExplicitString
def length
super > 5 ? 'long' : 'short'
end
end
String.class_eval do
prepend ExplicitString
end
puts "War and Peace".length #=> "long"

​ 它也是一种局部化的方法包装器,不过一般认为它比细化包装器和环绕别名都更清晰。