Showing posts with label 方法,Lambda,Closures. Show all posts
Showing posts with label 方法,Lambda,Closures. Show all posts

Monday, October 27, 2008

(8)lambda 和Proc

前面我们一再提到块的概念,你知道块只是一段代码,而不是一个对象。在Ruby中你可以通过Proc或者lambda来描述一个块,这时候这个块就是一个对象了,那么它也就有了方法call.
区分Proc和lambda可以在一个对象上调用lambda?方法,如果返回true就是lambda否则就是Proc
你可以通过a=Proc.new{|x| puts x}的方式定义一个Proc,你可以可以通过y=lambda{|x| x+1}的方式定义一个lambda。这两个的对象的却别在于Proc更像是一个块的定义,而lambda更像是一个方法的定义。比如下面的代码中,同样是return两种对象的行为是不一样的:
def test
  puts "entering method"
  p = Proc.new { puts "entering proc"; return } 
  p.call                 # 这里调用proc会使得方法return
  puts "exiting method"  # 这行永远不会执行
end
test

def test
  puts "entering method"
  p = lambda { puts "entering lambda"; return } 
  p.call                 # 调用lambda不会导致方法return
  puts "exiting method"  #现在这行会执行
end
test
另外这两种方式对参数的要求也是不一样的,比如:
p = Proc.new {|x,y| print x,y }
p.call(1)       # x,y=1:     如果缺少参数就用nil代替,打印 1 和 nil
p.call(1,2)     # x,y=1,2:   打印 12
p.call(1,2,3)   # x,y=1,2,3: 多余的参数被忽略了: 打印 12
p.call([1,2])   # x,y=[1,2]: 数组元素自动分配个每个参数: 打印 12



l = lambda {|x,y| print x,y }
l.call(1,2)     # 这样没有问题
l.call(1)       # 参数个数错误
l.call(1,2,3)   # 参数个数错误
l.call([1,2])   # 参数个数错误
l.call(*[1,2])  # 明确的告诉lambda这是一个数组参数,这可以正常运行。

Sunday, October 26, 2008

(7)方法的块参数

前面我们提到了有一种参数以&开头,这就是块参数,比如下面的代码中doubleplus有一个参数&b,它是一个块参数,块参数通过call方法调用执行:
def doubleplus(x,y,&b)
  c=x+y  
  while (c>1)
    b.call(2*c) #块参数通过call执行块中的内容
    c=c-1
  end
end
doubleplus(4,2){|c|
  puts "c:is #{c}" #我们在这里调用这个方法,并给他传递一个块参数
}
在随后的章节中我们会降到Proc对象,如果是Proc对象的话参数的&符号就可以省略了,因为这是时他是一个对象:
def doubleplus(x,y,b)
  c=x+y  
  while (c>1)
    b.call(2*c)#Proc对象的call方法
    c=c-1
  end
end
b=Proc.new{|x| puts x} #声明一个Proc对象变量
doubleplus(4,2,b)

(6)利用字典对象实现命名参数功能

在其他一些语言中(比如Python)支持方法的命名参数调用,比如你有一个方法有两个参数name和age,并且他们都有缺省值,那么你可以通过指定参数名而只给这个方法传递某一个参数,不幸的是,Ruby不提供这样的语法支持。一个变通的方法(或者说我们玩些小花招),你可以用一个字典对象作为方法的参数,那么你可以通过把字典对象总的元素键当作参数名,把字典对象中的元素值当作参数值的方式来实现这一功能,比如下面的例子:
def sequence(args)
 
  n = args[:n] || 0
  m = args[:m] || 1
  c = args[:c] || 0

  a = []                      
  n.times {|i| a << m*i+c }   
  a                           
end
然后你可以给这个方法传递一个字典对象参数:
sequence({:n=>3, :m=>5})      # => [0, 5, 10]
作为一种特殊的方式Ruby允许你把参数的花括号{}去掉,所以你可以写成这样:
sequence(:m=>3, :n=>5)        # => [0, 3, 6, 9, 12]

# Ruby 1.9 的写法
sequence c:1, m:3, n:5        # => [1, 4, 7, 10, 13]
不过,说实话比起Python的语法格式,这只能算是一种变通的用法,因为,你不能直观的看出这个方法到底有多少个参数,都是什么。

(5)任意参数的方法

有么有想过你的方法可以接受任意参数,如果你是Java程序员你一定记得那个main方法,它的参数是一个字符串数组,所以它的参数是可以有多个的。在Ruby中也是这样的,不过你要在参数前加一个星号*,这样就告诉Ruby你的方法有任意个数的参数,其实这个参数还是一个数组:
def max(first, *rest)  
  max = first 
  rest.each {|x| max = x if x > max } 
  max
end
上面这个方法中*rest是一个数组参数,你可以传递任意数量的参数给他。在Ruby1.8中要求这个带星号的参数必须位于所有参数(缺省参数和带&的参数)之后,也就是说它是最后一个参数。Ruby1.9中没有这样的要求,它不必是方法的最后一个参数,但是它必须位于缺省参数和&前缀的参数之后。
调用这个方法:
data = [3, 2, 1]
m = max(*data)   # first = 3, rest=[2,1] => 3
如果没有这个*那么方法的计算结果可就不同了:
m = max(data)   # first = [3,2,1], rest=[] => [3,2,1]

(4)方法的缺省参数

在Ruby中方法的参数是可以有缺省值的,比如下面的方法中len的值是缺省值,也就是说如果你调用这个方法时,没有给他传值那么就用缺省值:
def prefix(s, len=1)
  s[0,len]
end


prefix("Ruby", 3)    # => "Rub"
prefix("Ruby")       # => "R"
而且,方法的缺省参数可以是任意的表达式,甚至可以是对前一个参数的计算值,比如:
# 第二个参数的缺省值是通过第一个参数的长度值计算得到的。
def suffix(s, index=s.size-1)
  s[index, s.size-index]
end
需要记住的是,方法的缺省值是在方法被调用的时候创建的,而不是给他传值的时候,比如下面的方法中,缺省参数a是在调用方法append的时候就创建了:
def append(x, a=[])
  a << x
end
在Ruby1.8中方法的缺省参数只能出现在其它非缺省参数的后面,在Ruby1.9中没有这个限制,不过,你不能在两个缺省参数中放一个非缺省参数,而且在调用方法时,参数的传递必须是按顺序的。比如你有一个方法有2个参数,他们都存在缺省值,你可以不给它传递参数从而全部使用缺省值,你也可以只传递一个参数,那么方法会对第二个参数使用缺省值,但是,遗憾的是我们没办法只给第二个参数传值而让第一个参数使用缺省值。因为方法的参数传递是按顺序的。

Friday, October 24, 2008

(3)方法的括号

在Ruby中方法的括号是可选的,比如下面的两行代码是一样的效果:
puts "Hello World"
puts("Hello World")
Ruby中没有属性的概念,你要访问一个对象必须通过方法,比如下面的代码中length实际上是一个方法而不是属性:
greeting = "Hello"
size = greeting.length
不仅是方法调用,方法声明是也可以不加括号,比如:
def hello lname,fname
  puts lname
  puts fname
end
有时候加上括号可以使得代码更清楚,比如:
puts(sum 2,2)   # 这段代码的含义到底是 puts(sum(2,2)) 还是 puts(sum(2), 2)?
这种情况下,没有括号容易让代码引起混淆。需要注意的一点是,方法后的括号应该紧跟方法名,否则他们的含义是不一样的,比如:
square(2+2)*2    # square(4)*2 = 16*2 = 32
square (2+2)*2   # square(4*2) = square(8) = 64

(2)方法的命名

Ruby中的方法命名没有强制的要求,但是按照惯例,方法以小写字母开头,如果以大写字母开头通常会被任务是一个常量;如果方法中包含多个单词,那么每个单词之间用下划线分隔,而不是传统的驼峰式写法,比如:按惯例方法名写成like_this而不是likeThis。但是记住这只是一种约定,Ruby语法解析器没有强制要求你这样做。
另外要说到的是Ruby中方法可以以等号=,问号?和叹号结尾。以等号结尾的方法它的参数可以通过赋值的方式传递,这个我们之前就提到过。按照约定,以问号结尾的方法一般是告诉调用者,这个方法返回的是一个布尔值,比如Array的empty?用于判断数组是否为空,以!结尾的方法提醒程序员这个方法会更高对象内部的状态,比如Array的sort方法用于排序数组内的元素,返回的是一个新的数组拷贝,但是sort!方法是将当前数组重新排序并返回数组本身,一般Ruby的核心类库中都会提供两同名方法,一个有!结尾一个没有。但是记住这只是一种约定,Ruby语法解析器没有强制要求你这样做。
符号方法
Ruby中所有的操作符号都定义成了方法,比如+,-,*,/ []等等,所以你可以通过重载自定义符号方法。比如:
def +(other)               # 定义二进制的加法操作: x+y is x.+(y)
  self.concatenate(other)
end
但是值得注意的是,你只能使用我们以前讲到的Ruby中操作符中定义的操作符,也就是说你不能发明一个这些操作符以为的操作符。
方法别名
有时候我们用的方法有一个很长的名字,如果我们多次调用它时不得不一次次重复的写这个冗长的名字,幸运的是Ruby对方法提供了一个别名操作符alias,你可以通过这种方式给一个方法取另外一个别名:
alias aka also_known_as   # 给方法起个别名
 
看到这个你想到了什么?我想到了Python中的import some.module.clzz as clz,是不是很像?
执行了这个方法后aka就相当于also_known_as方法。这种语法结构让Ruby程序代码看起来更“清晰”,比如Range对象有一个方法用于判断某个元素是否位于其中,include?还记得吧,如果你的Range是某州类型的集合,那么另一个别名方法member?看起来让代码更容易阅读。
另外一个有用的功能是,你可以通过这种方式给一个已经存在的方法动态的增加新的功能,比如下面这样:
def hello                       # 定义一个简单的方法
  puts "Hello World"            # 假设现在我们想扩展它的功能...
end

alias original_hello hello      # 给已经存在的方法取一个别名

def hello                       # 现在我们用之前的那个方法名定义一个新的方法
  puts "Your attention please"  # 加入新的打印功能
  original_hello                # 调用之前的方法
  puts "This has been a test"   # 加入另一个功能
end
现在你在执行hello方法得到的结果将是:
Your attention please
Hello World
This has been a test
Ruby的方法名是不能重复的,在一些静态语言中,比如Java,你可以拥有相同方法名的方法,他们的参数个数和类型不同,这称之为方法重载,这在Ruby中是不允许的。
不过Ruby中不要求定义参数的类型,所有者意味着你可以传递任意的参数给一个方法,而且Ruby的方法参数可以有缺省值,你甚至可以不给已经有缺省值的参数传值。我们后面的章节中将详细讲解这部分。

(1)Ruby中的方法

前面我们就已经接触到了Ruby中的方法,这一节我们将详细讲解方法的定义、调用等细节。
首先说,从语法上来讲,方法以def 关键字开头后面是方法名、0到多个参数,方法以end关键字结尾,方法的参数可以当作变量在方法体内使用。下面是一个例子:
# 定义一个名为 'factorial' 的方法,它只有一个参数 'n'
def factorial(n)
  if n < 1                # 校验参数的合法性
    raise "argument must be > 0"
  elsif n == 1            # 如果参数为 1
    1                     # 那么参数的返回值就是 1
  else                    # 否则计算阶乘
    n * factorial(n-1)    # 计算n-1的阶乘
  end
end
上面的方法只有一个参数,用于计算整数的阶乘,你可以看到这是一个递归调用的方法。
方法都有返回值,返回值就是方法的最后一个表达式的值,如果一个方法中没有表达式那么返回值就是nil。像上面的factorial方法,如果正常执行,返回值就是1或者n*factorial(n-1)。
当然,你还可以用return 关键字来强制返回一个值,这种方式一般出现在两种情况:

  1. 你需要在方法的最后一行之前返回一个值。
  2. 你的方法有多个返回值,如果有多个返回值,那么最终的返回值是一个数组。
下面是一些例子:
def factorial(n)
  raise "bad argument" if n < 1
  return 1 if n == 1
  n * factorial(n-1)
end

#返回多个值时要使用关键字return
def polar(x,y)
  return Math.hypot(y,x), Math.atan2(y,x)
end

# 将最后一行的多个表达式值,转换成一个数组就可以省略return
def cartesian(magnitude, angle)
  [magnitude*Math.cos(angle), magnitude*Math.sin(angle)]
end
这种返回多个值得方法,通常被用于平行赋值:
distance, theta = polar(x,y)
x,y = cartesian(distance,theta)

Ruby是一个完全面向对象的语言,那么所有的方法都是通过对象调用的,即使是我们在一个脚本中写的一个单一的方法,其实底层实现中它实际上是Object类的私有方法。如果你定义了一个类并在类中定义了方法,那么和其他语言类似,Ruby中对象的方法通过点号访问(.),self关键字表示调用方法那个对象本身的引用。如果你使用过Java,那么这个self相当于Java中的this。对了,还记得我们以前讲过的异常处理吗,那么说rescue要存在于begin和end块之间,如果你是在一个方法中使用rescue,可以直接放在方法体中。
前面我们给出的方法都是“全局”方法,后面我们介绍类的时候还会介绍类的方法,然而,我们还可以定义另一种方法,假如现在你有一个对象,你只想在当前这个对象上定义一个方法,那么你需要使用def关键字并且方法名是一个对象名加点号,后面再跟方法名的方式,我们称这种方法为单一方法:
o = "message"    # 一个字符串对象
def o.printme    # 为这个对象定义一个单一方法
  puts self
end
o.printme        # 调用这个方法
我们除了可以定义一个方法还可以“解除定义”一个已经存在的方法,方法是使用关键字undef:
def sum(x,y); x+y; end      # 定义一个方法
puts sum(1,2)               # 使用它
undef sum                   # 解除定义

undef并不常用,不过有一种情况可能有用,比如对于子类方法访问的限制,举个例子:类A定义了方法m,B是A的子类,那么按照继承的原则,B当然拥有方法m,但是可能出于某种原因你不希望使用B的人调用方法m,那么你可以在类B中undef m。不过通常的方式我们是通过在B中重载m的方式来完成的。对于类和模块,还有一个私有方法undef_method,你可以把你不需要的一系列方法通过它来“解除定义”。