Ruby元编程 星期四
@ Shen Jianan · Wednesday, Oct 28, 2015 · 7 minute read · Update at Oct 28, 2015

类定义

即将走进"Ruby对象模型最深暗的角落”…和Java不同,在Ruby中,定义类实际上就是在运行代码,这种思想催生了1、可以修改类的类宏 2、可以在其他方法前后封装额外代码的环绕别名。当然,因为类不过是增强的模块,所以这些知识也可以应用于模块。

当前类

方法所属判断是依靠定义方法的所在self判断的,所以对于父类定义的方法,即使在子类中运行使得m2定义语句得到运行,m2依然属于父类。

class C
  def m1
    def m2

    end
  end
end

p C.instance_methods(false)	# => [:m1]

class D<C

end

D.new.m1

p C.instance_methods(false)	# => [:m1, :m2]

p D.instance_methods(false)	# => []

class_eval V.S class关键字

使用class关键字修改类需要指定类名才可以,那假如想要将类名作为参数来动态修改某个类呢?这时候可以用到Module#class_eval(别名为module_eval)方法。

def add_method_to(a_class)
  a_class.class_eval do
    def m
      'Hello!'
    end
  end
end

add_method_to String
p "abc".m			# => "Hello!"

另外,class是一个作用域门,类外的变量对类里面来说是不可见的。而class_eval则使用扁平作用域,因为它归根结底只是一个方法调用。

可以发现class_eval和instance_eval很相似,class_eval也有带参数版本class_exec。instance_eval的重点在于修改对象中变量的当前self,使其在调用处可见,而class_eval的重点在于修改类。

类的实例变量

Ruby的实例变量属于当前self,所以在类中的方法外定义的实例变量属于类这个对象而不是类的实例。

class A
  @var = 1
  def read; @var; end
  def write; @var = 2; end
  def self.read; @var; end
end

class B<A
  def self.read; @var; end
end

obj = A.new
p obj.read      # => nil
p B.read		    # => nil
obj.write
p obj.read      # => 2
p A.read        # => 1

正因为实例变量的归属根据当前self判断,所以子类、实例都不能访问类实例变量,因为这个变量属于“类”这个对象的self。

注意和@@开头的类变量区别开来,它是可以被当前类和子类或实例访问的。正是因为这样,在顶级作用域定义类变量时,所有类都可以访问到它,因为它们都在顶级作用域的覆盖下。

@@var = 1

class A
	@@var = 2
end

@@var = 2

为了避免这样的情况,可以的话最好避免使用类变量,转而使用类的实例变量。

单件方法

Ruby支持给单个对象添加方法:

str = "this is a string"

def str.title?
	self.upcase == self
end

str.singleton_methods  		# => [:title?]

和Java中singleton类只允许生成同一个对象的概念有所不同,这里的singleton方法是说这个方法只属于定义它的对象。

实际上,联想到定义类方法时的方法名前的self,类方法其实就是Class对象的单件方法罢了。

类宏

Ruby对象没有属性,所以需要写类似Java的getter和setter才能访问或修改属性。

class MyClass
  def my_attribute=(value)
    @my_attribute = value
  end

  def my_attribue
    @my_attribute
  end
end

obj = MyClass.new
obj.my_attribute = 1
obj.my_attribute     # => 1

但是这样写很烦,一点也不酷~ 所以Ruby提供了Module#attr_reader、Module#attr_writer和Module#attr_accessor

class MyClass
  attr_accessor :my_attribute
end

obj = MyClass.new
obj.my_attribute = 1
obj.my_attribute     # => 1

类似attr_accessor这样的方法就叫做类宏,他们看起来像是关键字,实际上只是可以用在类里的方法。

假设需要更新API,将旧的方法声明为deprecated,并且自动转到新方法,那么可以使用像下面这样的代码:

class MyClass
  def self.deprecate(old_method, new_method)
    define_method(old_method) do |*args, &block|
      warn "Warinig, #{old_method} is deprecated, use #{new_method} now!"
      send(new_method,*args,&block)
    end
  end

  def old_m
    p "old"
  end

  def new_m
    p "new"
  end

  deprecate :old_m, :new_m
end


obj = MyClass.new
obj.old_m

单件类

Ruby查找方法时先进入接收者的类,然后再向上查找。比如一个MyClass类实例obj调用方法,就会进入obj的类MyClass来查找方法。但是单件方法肯定不在类里,否则所有对象都有这个方法了。而对象本身又不是一个类,所以也不在obj里。那么这个方法在哪里呢?

什么是单件类

当询问对象的类时,Ruby返回的其实不是看到的类,而是对象特有的隐藏类。这个类被称为这个对象的单件类,或者叫元类。

obj = Object.new
singleton_class = class << obj
                    self      # => #<Class:#<Object:0x007fcc711e4278>>
end

p singleton_class

这样我们就可以看到这个单件类的引用(.class方法会包装隐藏单件类),现在Ruby也提供.singleton_class方法返回单件类。每个对象都有自己的单件类,单件类也只有一个实例(有实例说明它的实例也有自己的单件类~循环往复……只是这个单件类的单件类的用途还没有被开发过)。对象的单件方法就放在对应的单件类中。

现在需要更新一下在星期一学习的继承模型了:obj所在的类是obj的单件类,单件类的父类才是MyClass,MyClass类对象的父类是Object。 单件类的方法查找

单件类和继承

假设有如下的类关系,我们可以通过观察继承树输出来了解他们的结构关系

class C
  def self.claz_method

  end
end

class D<C

end

obj = D.new

def obj.singleton_mtd

end


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

单件类的继承关系

instance_eval和单件类的关系

instance_eval方法会把当前类修改为接收者的单件类。

s1 = 'abc'

s1.instance_eval do
	def swoosh!; reverse; end
end

很少会这样用,但这样是可以的。instance_eval的标准含义还是:修改self作用域内容。

类属性

如果想给类创建属性的话,可以使用下面这样的形式:

class MyClass
	class << self
		attr_accessor :c
	end
end

因为如果在单件类中定义了方法,这些方法实际上就会成为类方法。而属性其实就是一对方法,所以这样就达到了类属性的效果。

类扩展/对象扩展

如果想要从一个Module中导入一个方法到类/对象中,可以打开这个类/对象,然后通过include模块来扩展方法。

module MyModule
	def my_method; 'hello'; end
end

class MyClass
	class << self
		include MyModule
	end
end

MyClass.my_method		#完成了类方法的扩展

obj = Object.new
class << obj
	include MyModule
end

obj.my_method				#完成了对象方法的扩展
obj.singleton_methods	# => [:my_method]

除了使用打开类/对象,还可以使用简单的Object#extend方法。

module MyModule
	def my_method; 'hello'; end
end

obj = Object.new
obj.extend MyModule
obj.my_method		# => 'hello'

class MyClass
	extend MyModule
end

MyClass.my_method	# => 'hello'

方法包装器

如果一个不能修改的接口(比如在库中)有很多地方都被使用,而又想要在所有使用这个接口的地方增加新功能,那找到所有使用接口的地方逐条修改就会变成一件痛苦的事情。Ruby为我们提供了方法包装器的技巧来解决这个问题。

方法别名

使用alias可以为Ruby方法起别名,只要不在顶级作用域里面,可以使用Module#alias_method方法,而在顶级作用域里面,只能使用alias关键字。

class MyClass
	def my_method; 'my_method()'; end
	alias_method :m, :my_method
end

obj = MyClass.new
obj.my_method		# => 'my_method()'
obj.m				# => 'my_method'

def my_method2
  'hello'
end

alias :m2 :my_method2

m2					# => 'hello'

如果原方法在设置了别名之后又被修改了,那么别名指向的还是未被修改时的代码。

class String
	alias_method :real_length, :length
	
	def length
		real_length > 5 ? 'long' : 'short'
	end
end

"War and Peace".length 		# => "long"
"War and Peace".real_length 	# => 13

不同的方法包装器

环绕别名

我们可以通过为A方法设置一个别名B,然后重新定义A方法,插入新添加的功能,最后在A中调用B方法(别名了的原A方法,利用了上面说到的“指向未被修改时的代码”的特性)。 但是这种方法有猴子补丁的问题:所有地方的这个方法都被改变了,但是也许我们只需要在某些地方包装这个方法。

细化

refinements 提供一种方法让类的修改只影响某个作用域,而可以避免猴子补丁那样影响全局的问题。看下面的代码,我添加一个camelize方法到String类中,但它只能被Foo使用。

module Camelize
  refine String do
    def camelize
      dup.gsub(/_([a-z])/) {$1.upcase }
    end
  end
end

class Foo
  using Camelize

  def camelize_string(str)
    str.camelize
  end
end

Foo.new.camelize_string('blah_blah_blah')	# => "blahBlahBlah"
'blah_blah_blah'.camelize	# => NoMethodError

当Foo中声明了 refinement 时,我们可以看出它可以使用直接camelize了,但在Foo外却无法使用它。

在refine中使用super可以向方法中包装额外的功能。

module StringRefinement
	refine String do
		def length
			super > 5 ? 'long' : 'short'
		end
	end
end

using StringRefinement

"War and Peace".length			# => "long"

这样可以确保只在需要这个特性的时候才用到,不会在任何地方都曲解String#length的意思导致混乱。

Module#prepend包装

Module#prepend的作用和include类似,只是它把包含的模块插在祖先链的下方,这样的话寻找方法时会先找到被包含的模块中的方法,同时可以通过super调用该类中的原始方法。

module ExplicitString
	def length
		super > 5 ? 'long' : 'short'
	end
end

String class_eval do
	prepend ExplicitString
end

"War and Peace".length 		# => 'long'

这一种方法最为清晰,既避免了猴子补丁,也不用考虑细化的问题。细化会导致很多问题,原来的方法调用可以做到缓存来静态查找,但是现在refine让方法查找必须动态。更糟糕的是,任何一个块被调用的时候都有可能被refine了,这是很恐怖的。

About Me

2018.02至今 杭州嘉云数据 算法引擎

2017.6-2017.12 菜⻦网络-⼈工智能部-算法引擎

2016.09-2018.06 南京大学研究生

2015.07-2015.09 阿里巴巴-ICBU-实习

2012.09-2016.06 南京大学本科