BOO大全/函式

维基教科书,自由的教学读本

上一章:输入与输出 目录 下一章:程式


函式[编辑]

Boo 的函式以 def 关键字定义,而回传值则以 return 表示传回。

def sqr(x as double):
	return x*x

与 Python 最大的不同在于通常都要指定引数的型别,这点很重要,通常这被用来认定是否为静态型别(也有效率)语言,下面是以 C# 所写的代码,目的与上面一样。(译注:引数型别可以不用指定)

double sqr(double x) {
	return x*x;
}

正如变数型别可以由初始运算式推导而出一样,函式的回传型别通常也可以由回传值运算式的型别推导而得。(例外状况可以参考:递回函式)

部分语言允许引数也可推导而出,但 Boo 需要指定引数型别。如果你不指定,那么型别隐含地会是 Object,在后续处理上,你可能需要作必要的转型。指定引数型别之所以好,是因为你可以很清楚地明白引数的型别,也可以在多型的情况下,很清楚的明白是使用了哪一个函式。强型别语言本身已经说明了很多事情,在动态语言里,往往很多事情都需要放在程式注解里。

不像是 C# 或 Java,Boo 函式可以独立宣告,也不一定要定义在变数宣告之后。这应该会让你很惊讶,因为这几乎是一个全然自由的 Python 环境了。(不过,Boo并不是Python)

引数传递到函式时,是传递值,但是记住,当处理物件时,是传递参考。(参考与物件).

>>> res
(20, 2.4)
>>> def Fun(x as (double)):
... 	x[0] = 1.0
... 
>>> Fun(res)
>>> res
(1, 2.4)

函式也是一个物件,所以你可以将他们指派给变数 (类似 C 的函式指标)。其型别为 CallableTypes。

def yes():
	print "yes was entered"
	
def no():
	print "no was entered"

line = prompt("enter yes or no:")
if line == "yes":
	fn = yes
else:
	fn = no
fn()

递回函式[编辑]

Boo 的递回函式比较特别的是,他们通常需要明确地指定回传型别,因为这很难由型别推导中得到。下面是一个很经典的阶乘递回函式:

def fact(n as int) as int:
	if n < 2:
		return 1
	else:
		return n*fact(n-1)

fact 并不是个很有用或很有效率的函式,但它却很适合用来表达我想说的。

下面这个或许比较有用。目录树很自然地就是递回结构,目录包含目录...等。FilesInFolder 函式回传一个包含目录下所有符合特定样式档案的串列。首先它建立一个串列用以包含目录下所有档案,然后再继续访问其下的目录,把档案加入串列。到最后没有档案可加了,就会终止。

import System.IO
def FilesInFolder(path as string, mask as string) as List:
	files = [file for file in Directory.GetFiles(path,mask)]
	for dir in Directory.GetDirectories(path):
		files.Extend(FilesInFolder(dir,mask))
	return files
	
res = FilesInFolder(Directory.GetCurrentDirectory(),"*.boo")
print join(res,'\n')

下面函式 Reverse 只能运作在 List 上,它以递回的方式将串列的顺序颠倒﹔ ls[0:1] 是个新的串列,里面包含 ls 的第一个元素,而 ls[1:] 则包含剩余的元素。 (在要复制串列时,建议使用 ls[:] 进行。)

def Reverse(ls as List) as List:
	if len(ls) < 2:
		return ls[:]
	else:
		return Reverse(ls[1:])+ls[0:1]

Boo并不是Python[编辑]

Boo 使用 Python 的语法,同时加入许多很酷的 Python 特性,像字串与阵列的slicing、串列...等等。但是它是个非常不同的语言,就像 Java 明显地是以C++语法为基础但却不是 C++ 一样。所以语法本身并不重要,它只是语言的表面。Python 是个动态型别的语言,Boo 则全然是个静态型别的语言 (但是,请参考 DuckTyping,只是在很多地方使用了型别推导来避免明确地指定型别。

函式多载[编辑]

很多时候,函数会有相同的意思,但却针对不同的型别提供不同的实作。举例来说,System.Max就针对各种数值型别提供了不同的实作。这里Square函数就有两个多载:

def Square(n as int):
	return n*n
	
def Square(x as double):
	return x*x
	
assert Square(10).GetType() == int
assert Square(2.3).GetType() == double

虽然不为 int 或其他数执行别进行多载,也可以运行,但是结果却会是 double。在当我们真的需要传回值型别与引数型别相同时,进行多载就非常的重要了。不过,这样看来很多余,甚至写习惯 Python 的人也不觉得有什么。(这样的多载其实可以使用泛型(Generic)来作,但是我们得等等)

有一个引数以上的函式,一般对额外的引数都会提供预设值。Boo 无法只传部分引数,所以多载是一个让函式使用上更友善的方法。

def DrawBox(pos as Point, size as Size, colour as Color):
	....
	
def DrawBox(pos as Point, size as Size):
	DrawBox(pos,size,Color.Black)
	
def DrawBox(pos as Point):
	DrawBox(pos,Size(1,1),Color.Black)
與這很接近的,Boo 可以讓你在建構物件時就指定公開屬性的值,例如:btn = Button(Text : "Hello", Location : Position(100,100))

根据守则,如果方法的多载有相同数目的引数时,引数的型别应该尽可能地清楚。如果没指定型别,可能会造成 Boo 不知道该呼叫哪一个。

>>> def f(x as single):
... 	return x
... 
>>> def f(i as int):
... 	return i
... 
>>> f(2.3)
-----^
ERROR: Ambiguous reference 'f': Input43Module.f(System.Single), Input44Module.f(System.Int32).

如果多载函式的引数是数值型别的话,一定要加上 double 的版本,因为编译器可以轻易地在 double 与其他数值型别间作转换。

传递多个值[编辑]

函式可以接收不定个数的引数。下面的函式就可以接受不定个数的浮点数。你可以看到,它使用 '*' 运算子将引数指定为阵列。你也可以传递浮点数阵列给 Max,但要记得加上展开运算子 '*':

>>> def Max(*x as (double)):
... 	ret = double.MinValue
... 	for val in x:
... 		ret = System.Math.Max(ret,val)
... 	return ret
... 
>>> Max(2.4,2,4,5,4)
5
>>> arr = (1.0,5,6,4,2)
>>> Max(*arr)
6

在函式里面,引数就是被当作阵列。事实上,这也说明了这个特性是怎么被实作出来的,它就跟 Max( (2.0, 4.3, 2, 0) ) 一样。

函式当然也可以有其他正常的引数,不过他们必须被放在前面:

import System
def PrintNumbers(s as string, *numbers as (double)):
	Console.Write("{0}: ",s)
	for x in numbers:
		Console.Write("{0} ",x)
	Console.WriteLine()

PrintNumbers("Some numbers",2,3,6,2,1)

回传多个值[编辑]

有两种方法可以让你从 Boo 函式里传出多个值。第一种方法是使用参考引数:

def Modifies(ref a as int, ref x as double):
	a = 20
	x = 2.4
	
k = 1
z = 1.2
Modifies(k,z)
print k,z

输出结果

20 2.4

这与 C# 的 ref 一样﹔但在传递引数时,不需要使用 ref(译注:C# 要传引数到进去时,需明确指定 ref,例如:Method(ref val); )。这意味着除非看代码,否则你无法知道哪个变数被函式改变了。这很糟,所以使用这个特性时要小心。

另外一种方法是以阵列传回。

>>> def ReturnsTwo():
... 	return (20,2.4)
... 
>>> res = ReturnsTwo()
>>> res
(20, 2.4)
>>> x1,x2 = ReturnsTwo()
>>> print x1,x2
20 2.4

我们可以看到,阵列可以被解封为变数,这让语法看来十分地优雅。如果阵列里的元素型别不同的话,会是一个 object 型别的阵列,解封也仍然可以运作。

>>> def ReturnsAMixedBag():
... 	return ("a string",20,2.4)
... 
>>> s,i1,x1 = ReturnsAMixedBag()

匿名函式[编辑]

匿名函式一般被称作 closures,有两个好处:

  • 你不用取名字
  • closures 包含当前的状态

在 Boo 里的重要性远比 Python 来的高,因为在 Boo 里并不是区域的 Nested function

让我们拿前面的例子作点修改:

line = prompt("enter yes or no:")
if line == "yes":
	fn = def():
		print "yes was entered"
else:
	fn = def():
		print "no was entered"
		
fn()

closures 跟一般函数一样也可以有引数:

if argv[0] == "sqr":
	fn = def(x as double):
			return x*x
elif argv[0] == "cube":
	fn = def(x as double):
			return x*x*x

也可以用来作为某个函数的快捷用法:

getline = def():
	return System.Console.In.ReadLine()
while line = getline():
	print line

另一种用法是使用大括号 { }。这里隐藏了一个 return 述句,不过你应该很轻易就能看出他的语意。

getline = { System.Console.In.ReadLine() }
while line = getline():
	print line

map 内建函式可以将序列里的每个元素传入指定的函式来进行处理。下面就是将序列里每个元素都作平方的例子:

def sqr(x as double):
	return x*x

iter = map(range(0,10),sqr)
print join(iter)

你可以写的更短:

iter = map(range(0,10)) def(x as double):
	return x*x
for n in iter:
	print n

或者干脆用大括号 { }

iter = map(range(0,10),{x as double | x*x})
for n in iter:
	print n

该用哪种语法呢?虽然你可以把超过一行的述句放到 { } (通常被称作行内closure)里面,但它并不是个好的用法。这个语法只适用于传递值的简单函式,这种情况下够清楚。但遇到较为复杂的动作时,使用区块的语法会比较好。

closure 可以存取范围外的任何变数。

list = [1,2,3,4,5]
sum = 0.0
l2 = map(list) def(i as int):
	sum += i
	return sum
print join(l2)

输出结果

1 3 6 10 15

callable型别[编辑]

前面我们提到可以将函式指派给变数。但这是什么型别呢?我们可能会需要在函式宣告用到这型别的正确名称,事实上,的确我们会很常会要把函式当作一个引数传给另一个函式。

callable DoubleFun(x as double) as double

def Dump(fn as DoubleFun):
  for i in range(0,10):
    print i,fn(i)

Dump(System.Math.Sin)

关键字callable建立一个新的 callable 型别,之后则是他的命名。这与 C 使用 typedef 定义函式指标的作用非常类似,但不一样的是 callable 建立新型别,而不是别名。Boo 如果在遇到型别相近的情况时,会自动作适当的转换。下面的例子演示了引数为整数的函式也能传入 Dump 里面。

def isqr(i as int):
  return i*i

Dump(isqr)

试着传递另一个宣告不同的函式,将会得到编译时期错误:

def dbl(s as string):
	return s+s
Dump(dbl)
*** Dump(DoubleFun) is not compatible with the argument list
*** '(callable(System.String) as System.String)'

在遇到某些不知道函式宣告的情况,事实上 callable 相当宽容,举例来说,当写作 Windows forms 程式时,可以使用closure作为按键的处理动作:

btn.Click += def(o as object, e as EventArgs):
  print "I was clicked"

但很多情况下,宣告了引数却不一定会使用到,所以其实可以省略,Boo允许你这样写:

btn.Click += def():
     print "That's better!"

ICallable界面[编辑]

通常 Boo 允许你呼叫 callable型别,不过你也可以让你的类别可以被当作函式呼叫。你只要让你的类别实作ICallable界面即可:

class Fred(ICallable):
  def Call(args as (object)) as object:	
    return (args[0] as string) + (args[1] as string)
	
f = Fred()
assert f('one','two') == 'onetwo'

上一章:输入与输出 目录 下一章:程式