lisp 的形参

 函数形参列表

  关于函数名或文档字符串就没有更多可说的了,而本书其余部分将用很多篇幅来描述所有可在一个函数体里做的事情,因此就只需讨论形参列表了。

  很明显,一个形参列表的基本用途是为了声明一些变量,用来接收传递给函数的实参。当形参列表是一个由变量名所组成的简单列表时,如同在verbose-sum里那样,这些形参被称为必要形参。当函数被调用时,必须为它的每一个必要形参都提供一个实参。每一个形参被绑定到对应的实参上。如果一个函数以过少或过多的实参来调用的话,Lisp就会报错。

  但是,Common Lisp的形参列表也给了你更灵活的方式将函数调用实参映射到函数形参。除了必要形参以外,一个函数还可以有可选形参,或者也可以用单一形参绑定到含有任意多个额外参数的列表上。最后,参数还可以通过关键字而不是位置来映射到形参上。这样,Common Lisp的形参列表对于几种常见的编码问题提供了一种便利的解决方案。

  5.3 可选形参

  虽然许多像verbose-sum这样的函数只有必要形参,但并非所有函数都如此简单。有时一个函数将带有一个只有特定调用者才会关心的形参,这可能是因为它有一个合理的默认值。例如一个可以创建按需增长的数据结构的函数。由于数据结构可以增长,那么从正确性角度来说,它的初始尺寸就无关紧要了。那些清楚知道自己打算在数据结构中放置多少个元素的调用者们,可以通过设置特定的初始尺寸来改进其程序的性能,而多数调用者只需让实现数据结构的代码自行选择一个好的通用值就可以了。在Common Lisp中,你可以使用可选形参,从而使两类调用者都满意。不在意的调用者们将得到一个合理的默认值,而其他调用者们有机会提供一个指定的值。

  为了定义一个带有可选形参的函数,在必要形参的名字之后放置符号&optional,后接可选形参的名字。下面就是一个简单的例子:

  (defun foo (a b &optional c d) (list a b c d))

  当该函数被调用时,实参被首先绑定到必要形参上。在所有必要形参都被赋值以后,如果还有任何实参剩余,它们的值将被赋给可选形参。如果实参在所有可选形参被赋值之前用完了,那么其余的可选形参将自动绑定到值NIL上。这样,前面定义的函数会给出下面的结果:

  (foo 1 2) → (1 2 NIL NIL)

  (foo 1 2 3) → (1 2 3 NIL)

  (foo 1 2 3 4) → (1 2 3 4)

  Lisp仍然可以确保适当数量的实参被传递给函数——在本例中是2到4个。而如果函数用太少或太多的参数来调用的话,将会报错。

  当然,你会经常想要一个不同于NIL的默认值。这时可以通过将形参名替换成一个含有名字跟一个表达式的列表来指定该默认值。只有在调用者没有传递足够的实参来为可选形参提供值的时候,这个表达式才会被求值。通常情况只是简单地提供一个值作为表达式:

  (defun foo (a &optional (b 10)) (list a b))

  上述函数要求将一个实参绑定到形参a上。当存在第二个实参时,第二个形参b将使用其值,否则使用10。

  (foo 1 2) → (1 2)

  (foo 1) → (1 10)

  不过有时可能需要更灵活地选择默认值。比如可能想要基于其他形参来计算默认值。默认值表达式可以引用早先出现在形参列表中的形参。如果要编写一个返回矩形的某种表示的函数,并且想要使它可以特别方便地产生正方形,那么可以使用一个像这样的形参列表:

  (defun make-rectangle (width &optional (height width)) ...)

  除非明确指定否则这将导致height形参带有和width形参相同的值。

  有时,有必要去了解一个可选形参的值究竟是被调用者明确指定还是使用了默认值。除了通过代码来检查形参的值是否为默认值(假如调用者碰巧显式传递了默认值,那么这样做终归是无效的)以外,你还可以通过在形参标识符的默认值表达式之后添加另一个变量名来做到这点。该变量将在调用者实际为该形参提供了一个实参时被绑定到真值,否则为NIL。通常约定,这种变量的名字与对应的真实形参相同,但是带有一个-supplied-p后缀。例如:

  (defun foo (a b &optional (c 3 c-supplied-p))

   (list a b c c-supplied-p))

  这将给出类似下面的结果:

  (foo 1 2) → (1 2 3 NIL)

  (foo 1 2 3) → (1 2 3 T)

  (foo 1 2 4) → (1 2 4 T)

  5.4 剩余形参

  可选形参仅适用于一些较为分散并且不能确定调用者是否会提供值的形参。但某些函数需要接收可变数量的实参,比如说前文已然出现过的一些内置函数。FORMAT有两个必要实参,即流和控制串。但在这两个之后,它还需要一组可变数量的实参,这取决于控制串需要插入多少个值。+函数也接受可变数量的实参——没有特别的理由限制它只能在两个数之间相加,它可以对任意数量的值做加法运算(它甚至可以没有实参,此时返回0——加法的底数)。下面这些都是这两个函数的合法调用:

  (format t "hello, world")

  (format t "hello, ~a" name)

  (format t "x: ~d y: ~d" x y)

  (+)

  (+ 1)

  (+ 1 2)

  (+ 1 2 3)

  很明显,也可以通过简单地给它一些可选形参来写出接受可变数量实参的函数,但这样将会非常麻烦,光是写形参列表就已经足够麻烦了,何况还要在函数体中处理所有这些形参。为了做好这件事,还不得不使用一个合法函数调用所能够传递的那么多的可选形参。这一具体数量与具体实现相关,但可以保证至少有50个。在当前所有实现中,它的最大值范围从4096到536 870 911。 汗,这种绞尽脑汁的无聊事情绝对不是Lisp风格。

  相反,Lisp允许在符号&rest之后包括一揽子形参。如果函数带有&rest形参,那么任何满足了必要和可选形参之后的其余所有实参就将被收集到一个列表里成为该&rest形参的值。这样,FORMAT和+的形参列表可能看起来会是这样:

  (defun format (stream string &rest values) ...)

  (defun + (&rest numbers) ...)

  5.5 关键字形参

  尽管可选形参和剩余形参带来了很大的灵活性,但两者都不能帮助应对下面的情形。假设有一个接受四个可选形参的函数,如果在多数的函数调用中,调用者只想为四个参数中的一个提供值,并且更进一步,不同的调用者甚至有可能将分别选择使用其中一个参数。

  想为第一个形参提供值的调用者将会很方便——只需传递一个可选实参,然后忽略其他就好了。但是所有其他的调用者将不得不为所不关心的一到三个形参传递一些值。这不正是可选形参想来解决的问题吗?

  当然是。问题在于可选形参仍然是位置相关的——如果调用者想要给第四个可选形参传递一个显式的值,就会导致前三个可选形参对于该调用者来说变成了必要形参。幸好我们有另一种形参类型,关键字形参,它可以允许调用者指定具体形参相应所使用的值。

  为了使函数带有关键字形参,在任何必要的&optional和&rest形参之后,可以加上符号&key以及任意数量的关键字形参标识符,后者的格式类似于可选形参标识符。下面就是一个只有关键字形参的函数:

  (defun foo (&key a b c) (list a b c))

  当调用这个函数时,每一个关键字形参将被绑定到紧跟在同名键字后面的那个值上。如第4章所述,关键字是以冒号开始的名字,并且它们被自动定义为自求值常量。

  如果一个给定的关键字没有出现在实参列表中,那么对应的形参将被赋予其默认值,如同可选形参那样。因为关键字实参带有标签,所以它们在必要实参之后可按任意顺序进行传递。例如foo可以用下列形式调用:

  (foo) → (NIL NIL NIL)

  (foo :a 1) → (1 NIL NIL)

  (foo :b 1) → (NIL 1 NIL)

  (foo :c 1) → (NIL NIL 1)

  (foo :a 1 :c 3) → (1 NIL 3)

  (foo :a 1 :b 2 :c 3) → (1 2 3)

  (foo :a 1 :c 3 :b 2) → (1 2 3)

  如同可选形参那样,关键字形参也可以提供一个默认值形式以及一个supplied-p变量名。在关键字形参和可选形参中,这个默认值形式都可以引用那些早先出现在形参列表中的形参。

  (defun foo (&key (a 0) (b 0 b-supplied-p) (c (+ a b)))

   (list a b c b-supplied-p))

  (foo :a 1) → (1 0 1 NIL)

  (foo :b 1) → (0 1 1 T)

  (foo :b 1 :c 4) → (0 1 4 T)

  (foo :a 2 :b 1 :c 4) → (2 1 4 T)

  同样,如果出于某种原因想让调用者用来指定形参的关键字不同于实际形参名,那么可以将形参名替换成一个列表,令其含有调用函数时使用的关键字以及用作形参的名字。比如说下面这个foo的定义:

  (defun foo (&key ((:apple a)) ((:box b) 0) ((:charlie c) 0 c-supplied-p))

   (list a b c c-supplied-p))

  可以让调用者这样调用它:

  (foo :apple 10 :box 20 :charlie 30) → (10 20 30 T)

  这种风格在想要完全将函数的公共API与其内部细节相隔离时特别有用,通常是因为想要在内部使用短变量名,而不是API中的描述性关键字。不过该特性不常被用到。

  5.6 混合不同的形参类型

  在单一函数里使用所有四种类型形参的情况虽然罕见,但也是可能的。无论何时,当用到多种类型的形参时,它们必须以这样的顺序声明:首先是必要形参,其次是可选形参,再次是剩余形参,最后才是关键字形参。但在使用多种类型形参的函数中,一般情况是将必要形参和另外一种类型的形参组合使用,或者可能是组合&optional形参和&rest形参。其他两种组合方式,无论是&optional形参还是&rest形参,当与&key形参组合使用时,都可能导致某种奇怪的行为。

  将&optional形参和&key形参组合使用时将产生非常奇怪的结果,因此也许应该避免将它们一起使用。问题出在如果调用者没有为所有可选形参提供值时,那么没有得到值的可选形参将吃掉原本用于关键字形参的关键字和值。例如,下面这个函数很不明智地混合了&optional形参和&key形参:

  (defun foo (x &optional y &key z) (list x y z))

  如果像这样调用的话,就没问题:

  (foo 1 2 :z 3) → (1 2 3)

  这样也可以:

  (foo 1) → (1 nil nil)

  但是这样的话将报错:

  (foo 1 :z 3) → ERROR

  这是因为关键字:z被作为一个值填入到可选的y形参中了,只留下了参数3被处理。在这里,Lisp期待一个成对的关键字/值,或者什么也没有,否则就会报错。也许更坏的是,如果该函数带有两个&optional形参,上面最后一个调用将导致值:z和3分别被绑定到两个&optional形参上,而&key形参z将得到默认值NIL,而不声明缺失了东西。

  一般而言,如果正在编写一个同时使用&optional形参和&key形参的函数,可能就应该将它变成全部使用&key形参的形式——它们更灵活,并且总会可以在不破坏该函数的已有调用的情况下添加新的关键字形参。也可以移除关键字形参,只要没人在使用它们。 一般而言,使用关键字形参将会使代码相对易于维护和拓展——如果需要为函数添加一些需要用到新参数的新行为,就可以直接添加关键字形参,而无需修改甚至重新编译任何调用该函数的已有代码。

  虽然可以安全地组合使用&rest形参和&key形参,但其行为初看起来可能会有一点奇怪。正常地来讲,无论是&rest还是&key出现在形参列表中,都将导致所有出现在必要形参和&optional形参之后的那些值被特别处理——要么作为&rest形参被收集到一个形参列表中,要么基于关键字被分配到适当的&key形参中。如果&rest和&key同时出现在形参列表中,那么两件事都会发生——所有剩余的值,包括关键字本身,都将被收集到一个列表里,然后被绑定到&rest形参上;而适当的值,也会同时被绑定到&key形参上。因此,给定下列函数:

  (defun foo (&rest rest &key a b c) (list rest a b c))

  将得到如下结果:

  (foo :a 1 :b 2 :c 3) → ((:A 1 :B 2 :C 3) 1 2 3)

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值