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'

上一章:輸入與輸出 目錄 下一章:程式