一直感觉arity这个属性的算法提及的很少(虽然arity是个数学概念,但是简单的数学概念显然无法满足Ruby的复杂的需求),即使在Ruby-doc的MethodProc中也是如此。

Returns an indication of the number of arguments accepted by a method. Returns a nonnegative integer for methods that take a fixed number of arguments. For Ruby methods that take a variable number of arguments, returns -n-1, where n is the number of required arguments. For methods written in C, returns -1 if the call takes a variable number of arguments.

这篇Ruby 2.0的文档里面仅仅提到了几种最简单的情况,还远远算不上复杂,现在我这里列出了比文档里更多的例子,以此来分析arity的行为。

测试代码是这样的:

p proc {}.arity
p proc {||}.arity
p proc {|a|}.arity
p proc {|a,b|}.arity
p proc {|a,b,c|}.arity
p proc {|*a|}.arity
p proc {|a,*b|}.arity
p proc {|a,*b, c|}.arity
p proc {|x = 0, *args|}.arity
p proc {|x=0, y=0, *args|}.arity
p proc {|x, y=0, *args|}.arity
p proc {|(x, y), z=0, *args|}.arity
p proc {|a, b = 1, *c, d|}.arity
p proc {|a = 1, b = 1, *c, d|}.arity

p proc   { |x = 0| }.arity
p lambda { |a = 0| }.arity
def f(a = 0) end; p method(:f).arity
p proc   { |x=0, y| }.arity
p lambda { |x=0, y| }.arity
def f(x = 0, y) end; p method(:f).arity
p proc   { |x=0, y=0| }.arity
p lambda { |x=0, y=0| }.arity
def f(x=0, y=0) end; p method(:f).arity
p proc   { |x, y=0| }.arity
p lambda { |x, y=0| }.arity
def f(x, y=0) end; p method(:f).arity
p proc   { |(x, y), z=0| }.arity
p lambda { |(x, y), z=0| }.arity
def f((x, y), z=0) end; p method(:f).arity

然后是结果对比:

code ruby-1.9.3 ruby-2.0.0
p proc {}.arity 0 0
p proc {||}.arity 0 0
p proc {|a|}.arity 1 1
p proc {|a,b|}.arity 2 2
p proc {|a,b,c|}.arity 3 3
p proc {|*a|}.arity -1 -1
p proc {|a,*b|}.arity -2 -2
p proc {|a,*b, c|}.arity -3 -3
p proc {|x = 0, *args|}.arity -1 -1
p proc {|x=0, y=0, *args|}.arity -1 -1
p proc {|x, y=0, *args|}.arity -2 -2
p proc {|(x, y), z=0, *args|}.arity -2 -2
p proc {|a, b = 1, *c, d|}.arity -3 -3
p proc {|a = 1, b = 1, *c, d|}.arity -2 -2
p proc { |x = 0| }.arity 0 0
p lambda { |a = 0| }.arity 0 -1
def f(a = 0) end; p method(:f).arity -1 -1
p proc { |x=0, y| }.arity 0 -1
p lambda { |x=0, y| }.arity 0 -2
def f(x = 0, y) end; p method(:f).arity -2 -2
p proc { |x=0, y=0| }.arity 0 0
p lambda { |x=0, y=0| }.arity 0 -1
def f(x=0, y=0) end; p method(:f).arity -1 -1
p proc { |x, y=0| }.arity 1 1
p lambda { |x, y=0| }.arity 1 -2
def f(x, y=0) end; p method(:f).arity -2 -2
p proc { |(x, y), z=0| }.arity 1 1
p lambda { |(x, y), z=0| }.arity 1 -2
def f((x, y), z=0) end; p method(:f).arity -2 -2

由这一对比结果,可以清楚的看到:

  • 对于proc而言,如果参数中没有可变长参数(比如*args之类的),则在1.9.3中,arity取决于从左往右没有默认值的参数的数量。2.0.0中取决于整个参数列表中没有默认值的参数数量。
  • 如果有可变长参数,则如果n为整个参数列表中没有默认值的参数数量,arity为-n - 1。
  • 对于lambda而言,可以看到,在1.9.3中情况与proc总是一致的。而在2.0.0中,如果存在有默认值的参数,则如果n为整个参数列表中没有默认值的参数数量,arity = -n -1。否则就等于参数数量。
  • 对于method而言,无论1.9.3还是2.0.0,均与2.0.0中的lambda一致(准确的应该说是lambda在2.0.0中与method保持一致)。
  • 额外的&block不影响到arity的值。

大致结果就是如此了,如果有误,请务必留言告知,谢谢。

这篇文章只是随手写写玩玩的,我对Ruby的Block和Lambda其实理解并不怎么深刻,知识现在够用就差不多了吧。

第一种方法最传统了:

def f1(a)
  yield a
end

f1(1){|a| a.to_s} # "1"
f1(1, &:to_s) # "1" 

这种方法要求传入一个block,但是不传入也可以,可以用block_given?方法判断是否被传入了block。在第二个示例中虽然看上去要求是一个参数,但是再传入一个block并不会出错,&:to_s是{|a| a.to_s}这个block的简写

下面是第二种方法:

def f2(a, &b)
  b[a]
end

f2(1){|a| a.to_s} # "1"
f2(1, &:to_s) # "1"

这种方法和前一种其实效果一致,虽然看上去要求两个参数,但是block依旧是可选的,如果不传也不会出错,一样可以用block_given?方法判断是否被传入了block。唯一的区别就是这种方法可以为传入的block赋个值,以便于在再传给另外一个方法作为参数。

下面是第三种方法:

def f3(a, b)
  b[a]
end

f3(1, lambda{|a| a.to_s}) # "1"
f3(1, :to_s.to_proc) # "1"

这种方法虽然和第二种方法只有一个字符之差,但其实天差地远,b不能被传入一个block,无论是{|a| a.to_s}还是&:to_s都将被视为错误,它必须被传入一个lambda。因此在示例中我用两种方法创建了lambda,注意第二个示例其实是第一个示例的简写,在Ruby中&:to_s只能创建block,而:to_s.to_proc则可以创建proc对象(这里有件更加神奇的事,可以通过hack to_proc修改这个方法的返回值,但是依然要求必须是Proc对象,并且无法通过hack这个Proc对象的call方法或是[]方法修改它的执行行为,并且写在这个Proc中的return,next,break的语意也只按proc的语意处理,下面会提及)。至于proc,lamdba和block三者的差异?lamdba和proc其实差别很小,主要差异在binding和参数传递上,不过lambda {|a| a.to_s}其实也返回一个Proc对象,而lambda和block的区别主要是返回方法不一致,这个在很多Ruby书中都有详细介绍。但是它们其实可以相互转换,看下列代码:

def f4(a, &b)
  f3(a, b)
end

f4(1, &:to_s) # "1"

def f5(a, b)
  f1(a, &b)
end

f5(1, :to_s.to_proc) # "1"

可以看到在第一个示例中,一个方法调用时传入的block参数在方法内部会被当作成proc对象,因而f3也可以接受。而在第二个示例中,一个proc对象前面加上&符号跟在方法后面就被当作block,很有意思吧。更有意思的是,似乎无论怎么转换,代码块中return,next,break语句的语意似乎并不改变,几次试验下来都是如此,这点我至今还是没有想通。例如:

def f6(&b)
  puts "class of b is: #{b.class.inspect}"
  3.times {b.call}
end

def f7(b)
  puts "class of b is: #{b.class.inspect}"
  3.times {b.call}
end

f6 {puts 1; break}
# output:
# class of b is: Proc
# 1

f7 lambda {puts 1; break}
# class of b is: Proc
# 1
# 1
# 1

可以看到虽然都是在方法中调用代码块,并且在方法中b都是proc对象,代码内容也完全一致,但是由于f6传入的block形式而f7中传入的lambda形式,因此最后的结果存在差异。证明了break的语意并没有因为都转换成了proc对象而发生转变。