像App Inventor這類的編程語言通常會提供一組基本的內(nèi)置功能,對于app inventor來說,就是一組基本塊。編程語言還提供一種功能擴(kuò)展的方法,即,向語言中添加新的子程序(塊)?!驹谟?jì)算機(jī)科學(xué)中,子程序(英語:Subroutine, procedure, function, routine, method, subprogram),是一個大型程序中的某一部份代碼,由一個或多個語句塊組成。它負(fù)責(zé)完成某項(xiàng)特定任務(wù),而且與其他代碼相比,具備相對的獨(dú)立性?!g者注】在App Inventor中,通過定義過程(procedure),即,命名一些順序執(zhí)行的塊,來實(shí)現(xiàn)功能的擴(kuò)展。應(yīng)用中可以像調(diào)用App Inventor中的預(yù)定義塊一樣,調(diào)用這些過程。本章中你將看到,創(chuàng)建這樣抽象的過程的能力對于解決復(fù)雜問題是非常重要的,這是創(chuàng)建真正好應(yīng)用的基石。
當(dāng)家長對孩子說“睡覺前去刷牙”時,他們的實(shí)際含義是“從架子上拿起牙刷牙膏,向牙刷上擠一點(diǎn)牙膏,在每顆牙齒上刷10秒鐘(哈哈?。保鹊?。“刷牙”就是一種抽象:為一系列的低級指令起一個公認(rèn)的名稱。此處,家長要求孩子完成他們已經(jīng)認(rèn)可了的“刷牙”的一系列指令。
你也可以在編程中創(chuàng)建這樣的有名字的一系列指令,有些編程語言稱之為函數(shù)(function)或子程序(subprogram),在App Inventor中,被稱為過程(procedure)。過程就是一組順序執(zhí)行的有名字的塊,在應(yīng)用中可以隨時隨地調(diào)用它。
圖21-1就是一個過程的例子,它的功能是以英里為單位,計(jì)算兩個GPS坐標(biāo)之間的距離。
圖 21-1 計(jì)算兩點(diǎn)間距離的過程
不必急于探究這個過程中的內(nèi)部構(gòu)件,只要知道對于你所使用的編程語言來說,這樣的過程擴(kuò)展了它的功能。如果每個家長每天晚上都要向他的孩子解釋“刷牙”的步驟,那么這個孩子到了五年級可能還是不會刷牙。說“刷牙”是一種更有效的方式,而且每個人都會在睡覺之前去刷牙。
同樣的道理,在設(shè)計(jì)或編寫一個大型應(yīng)用時,一旦定義好了distanceBetweenPoints這個過程,你就會忽略它的內(nèi)部實(shí)現(xiàn)細(xì)節(jié),而只是簡單地使用(或調(diào)用)它的名字。這種抽象能力對于解決大型問題來說是至關(guān)重要的,可以將大型的軟件項(xiàng)目分解成若干個便于管理的代碼塊。
過程還可以有助于減少錯誤,因?yàn)樗鼈兛梢允∪ズ芏嗳哂嗟拇a:只要在一處定義了過程,應(yīng)用中就可以隨處調(diào)用它。因此,假如應(yīng)用中要計(jì)算你的當(dāng)前位置與其他10個點(diǎn)之間的最近距離,你不必拷貝粘貼10次圖21-1中的塊,相反,你只需要定義這個過程,并在需要時調(diào)用它即可。此外,那種拷貝粘貼塊的方法還非常容易引入錯誤,因?yàn)橐坏┠阆胄薷某绦?,就必須找到所有的拷貝,并逐個以相同的方式修改它們。想象一下,你試圖在一個有1000行或塊的代碼中,找到5-10個曾經(jīng)粘貼過的代碼塊!與其被迫地拷貝粘貼這寫塊,不如用過程在一處將代碼塊封裝起來。
最后,過程將有助于建立代碼庫,讓這些代碼在其他應(yīng)用中可以被重用。即便是創(chuàng)建一個非常具體的應(yīng)用,有經(jīng)驗(yàn)的程序員總會在必要時設(shè)法考慮重用其他應(yīng)用中的部分代碼。有些程序員從未創(chuàng)建過應(yīng)用,他們只是專注與創(chuàng)建可重用的代碼庫,以便其他程序員以此來創(chuàng)建他們自己的應(yīng)用。
看一下圖21-2中的代碼塊,能否發(fā)現(xiàn)其中的冗余。
圖 21-2 "隨手記"應(yīng)用中的冗余代碼
這里的冗余代碼指與foreach塊有關(guān)(實(shí)際上是整個foreach塊以及它上面的"set NotesLabel.Text to"塊),例子中的三個foreach的作用都是顯示筆記列表,只是使用的場合有所不同:當(dāng)添加新項(xiàng)、刪除某一項(xiàng),以及應(yīng)用啟動從數(shù)據(jù)庫加載列表時。
作為一個有經(jīng)驗(yàn)的程序員,一旦看到這樣的代碼,腦子里會立即敲響警鐘,甚至不必等到開始拷貝粘貼第一段程序中的代碼,他們知道最好是將這些冗余的代碼封裝在一個過程里,這樣既保證程序有很好的可讀性,也可以使后來的修改變得容易。
因此,有經(jīng)驗(yàn)的程序員會創(chuàng)建一個過程,將冗余代碼塊放在其中,并在原來使用冗余代碼的地方調(diào)用這一過程。應(yīng)用的執(zhí)行結(jié)果完全一樣,但更易于維護(hù),也讓其他程序員更容易地加以利用。這種代碼(塊)的重新整理的過程成為重構(gòu)。
我們來創(chuàng)建一個過程,實(shí)現(xiàn)圖21-2中那些冗余代碼的功能。在App Inventor中,定義過程幾乎與定義變量一樣簡單:從Procedures抽屜中拖出一個“to procedure”塊或“to procedure result”塊。如果過程需要通過計(jì)算返回一個結(jié)果,則使用后者(我們將在本章稍后的部分討論它)。
在拖出“to procedure”塊后,可以修改過程名稱:點(diǎn)擊默認(rèn)名稱“procedure”并輸入新名稱。由于冗余代碼塊的作用是顯示筆記列表,因此重構(gòu)時將過程名設(shè)為“displayList”,如圖21-3所示。
圖 21-3a 點(diǎn)擊默認(rèn)名稱“procedure”
圖 21-3b 將過程名改為“displayList”
下一步是向過程中添加塊,此時就用現(xiàn)有的冗余塊,將它們從事件處理程序中拖出并放在displayList塊中,如圖21-4所示。
圖 21-4 封裝了冗余代碼的過程displayList
現(xiàn)在我們可以用過程來顯示筆記列表了,在應(yīng)用的任何一處,都可以很容易地調(diào)用它。
像“displayList”和“刷牙”這樣的過程是一個包含了某種功能的實(shí)體,它們只有在被調(diào)用時,才能體現(xiàn)出這種功能。因此,以上我們只是創(chuàng)建了過程,卻并沒有調(diào)用它。調(diào)用它意味著要運(yùn)行它,或者說來實(shí)現(xiàn)它。
在App Inventor中,可以從Procedures抽屜中拖出一個以“call”開頭的塊來調(diào)用一個過程。每當(dāng)定義了一個新的過程,procedures抽屜中就會顯示一個新的塊,即定義一個過程,就是向Procedures抽屜中添加一個新塊,如圖21-5所示。
圖 21-5 定義好一個過程后,Procedures抽屜中就會出現(xiàn)一個新的“call” 塊
你一直都在用“call”塊來調(diào)用App Inventor中的預(yù)定義函數(shù),如Ball.MoveTo以及Texting.SendMessage。當(dāng)你定義了一個過程,就相當(dāng)于創(chuàng)建了自己的塊,也相當(dāng)于你擴(kuò)展了App Inventor語言,新的“call”塊讓你可以使用自己的創(chuàng)造。
在“隨手記”的例子中,三次拖出“call displayList”塊來取代三個事件處理程序中的冗余代碼,如,ListPicker1.AfterPicking事件處理程序(刪除一條筆記)修改的結(jié)果如圖21-6所示。
圖 21-6 使用“call displayList”來調(diào)用放在過程中的那些塊
要理解“call”塊的運(yùn)行機(jī)制,要想象應(yīng)用中有一個指針,它隨著塊的運(yùn)行而移動。在計(jì)算機(jī)科學(xué)中,這個指針被稱作程序計(jì)數(shù)器。
程序計(jì)數(shù)器隨著事件處理程序中的塊的運(yùn)行而移動,當(dāng)它遇到一個“call”塊時,它會跳到所遇到的過程中,并開始隨著過程中的塊的執(zhí)行而移動,;當(dāng)過程執(zhí)行完成,程序計(jì)數(shù)器再跳回到此前的位置(“call”塊處),并從此處開始繼續(xù)移動。以“隨手記”為例,“remove list item”塊執(zhí)行完成后,程序計(jì)數(shù)器跳到displayList過程中,并隨過程中的塊(設(shè)置NotesLabel.Text屬性為空,以及foreach循環(huán))移動;最后程序計(jì)數(shù)器在回到TinyDB1.StoreValue塊。
過程displayList將冗余代碼重整到一處,這使得程序更加容易理解,你可以在更高層次上理解這些事件處理程序,而忽略掉如何顯示列表的細(xì)節(jié)。這樣做的另一個好處是,如果想要修改列表的顯示方式,就只需修改一處代碼(而不是三處)。
就過程的通用性而言,displayList是有局限的,因?yàn)樵撨^程是針對特定的列表(notes)而設(shè)定的,而且用指定的label(NotesLabel)來顯示列表內(nèi)容,它不能用于顯示其他列表,比如應(yīng)用的用戶列表,因?yàn)檫^程中的要素定義的過于具體。
App Inventor以及其他編程語言都提供了一種稱為參數(shù)的機(jī)制,用于構(gòu)造更為通用的過程。過程為了實(shí)現(xiàn)它的預(yù)設(shè)功能所必須的信息就由參數(shù)來提供,以睡前刷牙為例,有可能將牙膏的類型和刷牙時間設(shè)定為刷牙過程的參數(shù)。
通過點(diǎn)擊過程塊左上角的藍(lán)色標(biāo)記,就可以為過程設(shè)定參數(shù)。對于displayList過程,我們定義了一個名為“l(fā)ist”的參數(shù),如圖21-7所示。
圖 21-7 在過程中引入了list作為參數(shù)
即使是定義了參數(shù),但foreach塊中仍然直接引用特定列表“notes”(插入到foreach塊的“in list”插槽中)。而我們希望在過程中使用我們傳遞的參數(shù)list,因此將對“global notes”的引用替換成對“get list”的引用。如圖21-8所示。
圖 21-8 現(xiàn)在foreach中使用了傳遞來的參數(shù)“l(fā)ist”
新版本的過程更加通用:在調(diào)用displayList時,無論傳入什么樣的列表,displayList都能顯示它。在向過程添加參數(shù)時,App Inventor會自動為“call”塊添加一個對應(yīng)的插槽,因此當(dāng)displayList添加了參數(shù)list之后,“call displayList”塊就變成圖21-9中的樣子。
圖 21-9 現(xiàn)在調(diào)用displayList時,需要指明要顯示的列表
過程定義中引入的參數(shù)list被稱為“形式參數(shù)”,而“call”塊中與之相對應(yīng)的插槽被稱為“實(shí)際參數(shù)”。當(dāng)在應(yīng)用中的某處調(diào)用過程時,必須為過程中的每個“形式參數(shù)”提供一個“實(shí)際參數(shù)”。
對于“隨手記”的應(yīng)用來說,將列表“notes”作為實(shí)際參數(shù)添加到“call”塊的list插槽中。ListPicker.AfterSelection的修改結(jié)果如圖21-10所示。
圖 21-10 在調(diào)用displayList時,將notes作為實(shí)際參數(shù)傳入
現(xiàn)在當(dāng)displayList被調(diào)用時,列表notes被傳遞到過程中,來取代形式參數(shù)list。此時,程序計(jì)數(shù)器隨著過程中的每個塊的運(yùn)行,它的指向是參數(shù)list,而實(shí)際上處理的是變量notes。
由于有了參數(shù),過程displayList可以用于處理任何列表,而不僅僅是notes。例如,如果“隨手記”應(yīng)用可以在一組用戶中共享,而你想查看一下用戶列表,就可以調(diào)用displayList并傳入userList參數(shù)。如圖21-11所示。
圖 21-11 過程displayList可用于顯示任何列表,而不僅僅是notes
關(guān)于過程displayList的可重用性,還有一個問題需要討論——你能猜到是什么嗎?如前所述,它可以顯示任何數(shù)據(jù)列表,但也只能在標(biāo)簽NotesLabel中顯示。如果你想用其他的界面元素(如另一個label)來顯示列表(如userList),該如何是好呢?
一個方法就是重構(gòu)過程——將它的功能從“用指定label顯示列表”改為“只返回一個文本對象,它可以被顯示在任何地方”。為此,需要使用“procedure result”塊來取代“procedure”塊,如圖21-12所示。
圖 21-12 “procedure result”塊
你會發(fā)現(xiàn)與“procedure”塊相比,“procedure result”塊的底部有一個額外的插槽,將一個變量放入插槽,這個變量將被返回給調(diào)用者。因此,正如調(diào)用者可以向過程以參數(shù)的方式傳入數(shù)據(jù)一樣,過程也可以以值得方式將數(shù)據(jù)返回給調(diào)用者。
圖21-13顯示了上述過程的改寫版本,現(xiàn)在使用的是“procedure result”塊。注意,由于過程的作用變了,因此名稱也由displayList改為convertListToText(將列表轉(zhuǎn)換為文本)。
圖 21-13 過程convertListToText返回一個文本對象,調(diào)用者可以將其放在任何一個label中
在圖21-13所示的塊中,變量text用來保存foreach循環(huán)中通過遍歷列表而生成的文本。用text變量取代之前使用的過于具體的NotesLabel組件。在foreach執(zhí)行完畢后,變量text包含了列表中的所有項(xiàng),而且項(xiàng)之間以換行符“\n”分隔(即“item1\nitem2\nite3”)。最后,將變量text插入return插槽,返回給調(diào)用者。
在定義“procedure result”時,與“procedure”相比,對應(yīng)的“call”塊看起來略有不同,如圖21-14中所做的比較。
圖 21-14 下面的有返回值的“call”必須插入到某個插槽中
不同的是在“call convertListToText”塊的左側(cè)有一個插頭,這是因?yàn)楫?dāng)“call”塊運(yùn)行時,過程在執(zhí)行一系列指令后將向“call”塊返回一個值,必須有某個插槽可以接收這個返回值。
在這種情況下,調(diào)用塊“call convertListToText”的返回值可以插入到任何一個label的Text屬性中,以notes列表為例,需要顯示列表的三個事件處理程序都可以調(diào)用這一過程,如圖21-15所示。
圖 21-15 將列表notes的內(nèi)容轉(zhuǎn)換為文本,并用NotesLabel顯示出來
更重要的是,由于過程的定義更具通用性,不需要引用任何特定list或label,因此應(yīng)用中可以使用convertListToText在任何一個label蒸南瓜顯示任何一個列表。像圖21-16中的例子那樣。
圖 21-16 這一過程再也不必與一個特定的Label組件捆綁在一起
通過過程的方式實(shí)現(xiàn)代碼的重用不必只限于單獨(dú)的應(yīng)用,有許多過程,如convertListToText,可以用在你創(chuàng)建的任何應(yīng)用中。事實(shí)上,有許多組織和編程社區(qū)都在為他們感興趣的領(lǐng)域創(chuàng)建過程代碼庫,例如動畫過程的代碼庫。
通常編程語言會提供一個“import(導(dǎo)入)”功能,可以在任何應(yīng)用中引入其他的代碼庫。App Inventor目前沒有這項(xiàng)功能,不過正在開發(fā)之中。同時,也可以在一個特定的“庫應(yīng)用”中創(chuàng)建一些過程,并復(fù)制該應(yīng)用的代碼,作為一個新建項(xiàng)目的基礎(chǔ)代碼。
在displayList(convertListToText)例子中,我們將過程定義描述為一種消除冗余代碼的方法:你開始寫代碼,隨后發(fā)現(xiàn)代碼存在冗余,于是整理代碼消除冗余。無論如何,一個軟件的開發(fā)人員或開發(fā)團(tuán)隊(duì)在應(yīng)用開發(fā)的初期都會創(chuàng)建很多過程,同時也考慮到要重用部分代碼。這樣的規(guī)劃可以在項(xiàng)目過程中節(jié)省大量時間。
考慮一項(xiàng)應(yīng)用:確定離某人當(dāng)前位置最近的本地醫(yī)院,某些東西在緊急情況下會派上用場的。以下是這個應(yīng)用的高層設(shè)計(jì)描述:
應(yīng)用啟動時,以英里為單位計(jì)算兩點(diǎn)之間的距離,起點(diǎn)是當(dāng)前所在位置,終點(diǎn)是發(fā)現(xiàn)的第一家醫(yī)院。然后再尋找第二家醫(yī)院,以此類推。在求得若干個距離后,判斷最短距離的醫(yī)院,并顯示它所在位置的地址。
從以上描述中,你能斷定應(yīng)用中需要什么樣的過程嗎?
通常,一段描述中的動詞提示了所需的過程。重讀一遍描述,正如“等等”所提示的,這是另一個線索。這種情況下,“求出兩點(diǎn)之間的距離”與“判斷這些距離中最短的”成為兩個必需的過程。
現(xiàn)在考慮設(shè)計(jì)一個過程distanceBetweenPoints(兩點(diǎn)間距離)。在設(shè)計(jì)過程時,首先要確定過程的輸入及輸出:調(diào)用者需要向過程傳遞實(shí)現(xiàn)過程的功能所需的參數(shù),而過程要向調(diào)用者返回執(zhí)行結(jié)果。在這里,調(diào)用者需要向過程傳遞兩個點(diǎn)的經(jīng)度及緯度值,如圖21-17所示;而過程的任務(wù)是以英里為單位返回兩點(diǎn)之間的距離。
圖 21-17 調(diào)用者想過程傳遞了4個參數(shù),并收到一個距離
圖21-18中顯示了我們在本章開始時提到的那個過程,使用公式求得兩個GPS坐標(biāo)點(diǎn)之間的近似英里數(shù)。
圖 21-18 過程distanceBetweenPoints
圖21-19顯示了對上述過程的兩次調(diào)用,每次都會求出當(dāng)前位置與指定醫(yī)院之間的距離。
圖 21-19 兩次調(diào)用distanceBetweenPoints過程
第一次調(diào)用中,起點(diǎn)為用戶當(dāng)前所在位置的LocationSensor(位置傳感器)讀數(shù),終點(diǎn)是St. Mary's hospital(圣瑪利亞醫(yī)院),計(jì)算的結(jié)果保存在變量distanceStMarys中;第二次調(diào)用也類似,只是將終點(diǎn)的數(shù)據(jù)改為CPMC Hospital(加州太平洋醫(yī)療中心醫(yī)院)的經(jīng)緯度。
接下來程序比較兩個距離并返回最近的醫(yī)院。但是如果還有更多的醫(yī)院,那就需要在一個距離列表中進(jìn)行比較,并找到最小值。依你所學(xué),你能寫出這個過程嗎?將其命名為findMinimum,接受一個數(shù)值列表作為參數(shù),并返回最短距離在列表中的索引值。
像App Inventor這樣的編程語言提供了一個內(nèi)置功能的基本集,而過程是一種新功能的提取,它擴(kuò)充了app inventor語言。App Inventor不提供顯示列表的塊,于是由你來做;那么是否需要一個計(jì)算兩個GPS坐標(biāo)間距離的塊呢?答案是靠我們自己來創(chuàng)造。
想要建造大型的、可維護(hù)的軟件,以及在解決復(fù)雜問題時免于不斷地糾纏于細(xì)節(jié)之中,則定義高級過程的能力是至關(guān)重要的。過程是將代碼塊封裝起來,并起一個名字。在編寫過程時,你會關(guān)注這些塊的細(xì)節(jié),但對程序的其他部分而言,這個過程只是一個抽象的名字,你可以在更高層次上來引用它。
更多建議: