Ruby盲点梳理

前两天被小伙伴问到ruby的一些常用概念,觉得自己对某些方法和概念还是理解的不够清晰,想起之前写gem的时候,一度掉进了delegator的坑里,对某些类的使用,依然停留在相识未深知的阶段,于是刷了点文档,梳理下一些概念。

说三个:benchmark,new & allocate, delegator。

Benchmark

Benchmark是一个module,提供了诸多方法,用于获取ruby代码的执行时间。

比较常用的三个方法:

  • benchmark(caption = “”, label_width = nil, format = nil, *labels) { |report| … }
  • bm(label_width = 0, *labels) { |report| … }
  • bmbm(width = 0) { |job| … }

bm可以看作是benchmark方法的一个简洁版,省去了参数caption,format的设置,这里,caption用于设置输出的标题内容,而format则用于设置时间的输出样式。通常可采用默认设置CAPTION,FORMAT,不过使用CAPTION,FORMAT常量时,记得加上include Benchmark

bmbm其实是执行了两次bm(看它名字就知道了,double bm : P ),这么做是为了获取更为精准的时间统计结果。因为有的时候,由于垃圾回收和内存分配的不同,可能会导致早期代码的执行时间与后期代码的执行时间存在差异,当然执行两次,并不代表结果一定不受到GC或者其他因素的影响,只是说会得到更为精准的结果。【希望我的理解没有偏……】

看例子感受下三者:

require 'benchmark'
include Benchmark

n = 500000

Benchmark.benchmark(CAPTION, 6, FORMAT) do |x|
  tf = x.report("for: ") {for i in 1..n; a = "1" ; end }
  tt = x.report("times: ") { n.times do; a = "1"; end }
  tu = x.report("upto: ") {1.upto(n) do ; a = "1" ; end }
end

Benchmark.bm(6) do |x|
  tf = x.report("for: ") {for i in 1..n; a = "1" ; end }
  tt = x.report("times: ") { n.times do; a = "1"; end }
  tu = x.report("upto: ") {1.upto(n) do ; a = "1" ; end }
end


Benchmark.bmbm(6) do |x|
  tf = x.report("for: ") {for i in 1..n; a = "1" ; end }
  tt = x.report("times: ") { n.times do; a = "1"; end }
  tu = x.report("upto: ") {1.upto(n) do ; a = "1" ; end }
end

输出的结果:

这么看,感觉times也没有特别比for快多少……

new & allocate

两者都用于实例化类,不同之处在于是否调用了initialize。

allocate是为类的对象分配空间,创建实例,但是不调用initialize方法,new也用于实例化,但是它调用了initialize,new可以看成是先调用allocate,再调用了initialize。

new对应的sourcecode如下:

static VALUE
rb_class_s_new(int argc, const VALUE *argv, VALUE klass)
{
    VALUE obj;

    obj = rb_class_alloc(klass);
    rb_obj_call_init(obj, argc, argv);

    return obj;
}

new的实现中,先是调用了rb_class_alloc【查看allocate的源码,会发现这正是allocate方法的实现】,然后调用了rb_obj_call_init

看个例子感受下两者的差异:

### 根据官方文档稍稍改编了下
klass = Class.new do
  def initialize(*args)
    @initialized = true
  end

  def initialized?
    @initialized || false
  end
end

klass.allocate #=> #<#<Class:0x00007fdb1f004a40>:0x00007fdb1f004978>
klass.allocate.initialized? #=> false
klass.new #=> #<#<Class:0x00007fdb1f004a40>:0x00007fdb1f004770 @initialized=true>
klass.new.initialized? #=> true

Delegator

Delegator是一个类,提供了三种不同的方法用于代理对象的方法调用,最简单的方法是使用SimpleDelegator。其次是使用DelegateClass,通过类继承来代理,第三种是继承Delegator这个abstract class,然后自己定制代理机制。

先来看看SimpleDelegator。

它有两个较为常用的实例方法:__setobj__ 用来更改代理对象,__getobj__ 用来获取代理对象。

看个例子:

class Stats
  def initialize
    @source = SimpleDelegator.new([])
  end

  def stats(records)
    p "before setobj: #{@source.size}"
    @source.__setobj__(records)
    p "after setobj: #{@source.size}"
  end
end

s = Stats.new

s.stats(%w{Hello ruby world})
#=> "before setobj: 0"
#=> "after setobj: 3"

稍稍解释一下。这里调用stats方法时,@source最开始是SimpleDelegator的一个实例,它的代理对象是[],所以输出before setobj 中,执行@source.size 就是在执行[].size, 随后使用__setobj__将代理对象更改成了records,这里传递过去的参数是数组%w{Hello ruby world},随后执行@source.size,就是在执行[“Hello”, “ruby”, “world”].size,所以输出结果为0,3.

第二种,通过继承来代理,跟祖先链很类似,略过,看第三种,定制代理。

custom delegator这种操作,涉及到module Forwardable和SingleForwardable,Forwardable是针对object level,而SingleForwardable则是object,class,module通吃。

两者主要的原理其实是一样的,通过在类,对象或者模块中使用extend来为其拓展单件方法,调用诸如def_delegate, def_delegates, def_instance_delegator等method来实现定制化代理。

先看个Forwardable的例子:

require 'forwardable'

class RecordCollection
  attr_accessor :records
  extend Forwardable
  def_delegator :@records, :[], :record_number
  def_delegators :@records, :size, :<<, :map
end

obj = RecordCollection.new
obj.records = [1,2,4]
obj.record_number(0) #=> 1, 相当于执行[1,2,4](0)
obj.size #=> 3 , 相当于执行[1,2,4].size
obj << 5 #=>[1, 2, 4, 5], 相当于执行[1,2,4]<< 5
obj.map { |x| x + 1} #=>[2, 3, 5, 6], 相当于执行[1,2,4,5].map{|x| x + 1}

这里 def_delegators :@records, :size, :<<, :map相当于:

def_delegator :@records, :size
def_delegator :@records, :<<
def_delegator :@records, :map

再来看一个SingleForwardable的例子:

class A
  def self.hi
    puts "hello"
  end
end

module B
  extend SingleForwardable
  def_delegator :A, :hi
end

B.hi #=> hello

这里的B.hi ,使用代理,执行的其实是A.hi ,故输出为hello.

看到这里你也许要问,如果当前对象本身有一个方法method A,代理方法里面也有一个同名的method A,会调用哪一个?

当然是调用自己的,自己没有才会调用代理的

以上面的例子为例:

class A
  def self.hi
    puts "hello"
  end
end

module B
  extend SingleForwardable
  def_delegator :A, :hi

  def self.hi
    puts "world"
  end

end

B.hi #=> world

题外

看Date部分的时候,发现ruby cal.rb -c it 10 1582 时[此处it 指的是Italy] 的输出结果很是诡异,自己在本机执行,同样得到了奇葩的结果:

莫名其妙少了10天,什么情况?!刚开始还以为是bug,Google下才知道是不同历法的切换导致, wiki上对于1582年,是这么说的:

However, this year saw the beginning of the Gregorian Calendar switch, when the Papal bull known as Inter gravissimas introduced the Gregorian calendar, adopted by Spain, Portugal, the Polish–Lithuanian Commonwealth and most of present-day Italy from the start. In these countries, the year continued as normal until Thursday, October 4. However, the next day became Friday, October 15 (like a common year starting on Friday), in those countries (France followed two months later, letting Sunday, December 9 be followed by Monday, December 20).

这种操作真6,老祖宗们会玩。

参考

ruby 2.5

1582

谢谢D推荐的devdocs, 用来阅读文档真的是超赞!推荐使用devdocs:P