像App Inventor這類的編程語言通常會提供一組基本的內置功能,對于app inventor來說,就是一組基本塊。編程語言還提供一種功能擴展的方法,即,向語言中添加新的子程序(塊)。【在計算機科學中,子程序(英語:Subroutine, procedure, function, routine, method, subprogram),是一個大型程序中的某一部份代碼,由一個或多個語句塊組成。它負責完成某項特定任務,而且與其他代碼相比,具備相對的獨立性。——譯者注】在App Inventor中,通過定義過程(procedure),即,命名一些順序執行的塊,來實現功能的擴展。應用中可以像調用App Inventor中的預定義塊一樣,調用這些過程。本章中你將看到,創建這樣抽象的過程的能力對于解決復雜問題是非常重要的,這是創建真正好應用的基石。

當家長對孩子說“睡覺前去刷牙”時,他們的實際含義是“從架子上拿起牙刷牙膏,向牙刷上擠一點牙膏,在每顆牙齒上刷10秒鐘(哈哈!)”,等等。“刷牙”就是一種抽象:為一系列的低級指令起一個公認的名稱。此處,家長要求孩子完成他們已經認可了的“刷牙”的一系列指令。
你也可以在編程中創建這樣的有名字的一系列指令,有些編程語言稱之為函數(function)或子程序(subprogram),在App Inventor中,被稱為過程(procedure)。過程就是一組順序執行的有名字的塊,在應用中可以隨時隨地調用它。
圖21-1就是一個過程的例子,它的功能是以英里為單位,計算兩個GPS坐標之間的距離。

**圖 21-1 計算兩點間距離的過程**
不必急于探究這個過程中的內部構件,只要知道對于你所使用的編程語言來說,這樣的過程擴展了它的功能。如果每個家長每天晚上都要向他的孩子解釋“刷牙”的步驟,那么這個孩子到了五年級可能還是不會刷牙。說“刷牙”是一種更有效的方式,而且每個人都會在睡覺之前去刷牙。
同樣的道理,在設計或編寫一個大型應用時,一旦定義好了distanceBetweenPoints這個過程,你就會忽略它的內部實現細節,而只是簡單地使用(或調用)它的名字。這種抽象能力對于解決大型問題來說是至關重要的,可以將大型的軟件項目分解成若干個便于管理的代碼塊。
過程還可以有助于減少錯誤,因為它們可以省去很多冗余的代碼:只要在一處定義了過程,應用中就可以隨處調用它。因此,假如應用中要計算你的當前位置與其他10個點之間的最近距離,你不必拷貝粘貼10次圖21-1中的塊,相反,你只需要定義這個過程,并在需要時調用它即可。此外,那種拷貝粘貼塊的方法還非常容易引入錯誤,因為一旦你想修改程序,就必須找到所有的拷貝,并逐個以相同的方式修改它們。想象一下,你試圖在一個有1000行或塊的代碼中,找到5-10個曾經粘貼過的代碼塊!與其被迫地拷貝粘貼這寫塊,不如用過程在一處將代碼塊封裝起來。
最后,過程將有助于建立代碼庫,讓這些代碼在其他應用中可以被重用。即便是創建一個非常具體的應用,有經驗的程序員總會在必要時設法考慮重用其他應用中的部分代碼。有些程序員從未創建過應用,他們只是專注與創建可重用的代碼庫,以便其他程序員以此來創建他們自己的應用。
## **消除冗余**
看一下圖21-2中的代碼塊,能否發現其中的冗余。



**圖 21-2 "隨手記"應用中的冗余代碼**
這里的冗余代碼指與foreach塊有關(實際上是整個foreach塊以及它上面的"set NotesLabel.Text to"塊),例子中的三個foreach的作用都是顯示筆記列表,只是使用的場合有所不同:當添加新項、刪除某一項,以及應用啟動從數據庫加載列表時。
作為一個有經驗的程序員,一旦看到這樣的代碼,腦子里會立即敲響警鐘,甚至不必等到開始拷貝粘貼第一段程序中的代碼,他們知道最好是將這些冗余的代碼封裝在一個過程里,這樣既保證程序有很好的可讀性,也可以使后來的修改變得容易。
因此,有經驗的程序員會創建一個過程,將冗余代碼塊放在其中,并在原來使用冗余代碼的地方調用這一過程。應用的執行結果完全一樣,但更易于維護,也讓其他程序員更容易地加以利用。這種代碼(塊)的重新整理的過程成為重構。
## **定義過程**
我們來創建一個過程,實現圖21-2中那些冗余代碼的功能。在App Inventor中,定義過程幾乎與定義變量一樣簡單:從Procedures抽屜中拖出一個“to procedure”塊或“to procedure result”塊。如果過程需要通過計算返回一個結果,則使用后者(我們將在本章稍后的部分討論它)。
在拖出“to procedure”塊后,可以修改過程名稱:點擊默認名稱“procedure”并輸入新名稱。由于冗余代碼塊的作用是顯示筆記列表,因此重構時將過程名設為“displayList”,如圖21-3所示。

**圖 21-3a 點擊默認名稱“procedure”**

**圖 21-3b 將過程名改為“displayList”**
下一步是向過程中添加塊,此時就用現有的冗余塊,將它們從事件處理程序中拖出并放在displayList塊中,如圖21-4所示。

**圖 21-4 封裝了冗余代碼的過程displayList**
現在我們可以用過程來顯示筆記列表了,在應用的任何一處,都可以很容易地調用它。
## **調用過程**
像“displayList”和“刷牙”這樣的過程是一個包含了某種功能的實體,它們只有在被調用時,才能體現出這種功能。因此,以上我們只是創建了過程,卻并沒有調用它。調用它意味著要運行它,或者說來實現它。
在App Inventor中,可以從Procedures抽屜中拖出一個以“call”開頭的塊來調用一個過程。每當定義了一個新的過程,procedures抽屜中就會顯示一個新的塊,即定義一個過程,就是向Procedures抽屜中添加一個新塊,如圖21-5所示。

**圖 21-5 定義好一個過程后,Procedures抽屜中就會出現一個新的“call” 塊**
你一直都在用“call”塊來調用App Inventor中的預定義函數,如Ball.MoveTo以及Texting.SendMessage。當你定義了一個過程,就相當于創建了自己的塊,也相當于你擴展了App Inventor語言,新的“call”塊讓你可以使用自己的創造。
在“隨手記”的例子中,三次拖出“call displayList”塊來取代三個事件處理程序中的冗余代碼,如,ListPicker1.AfterPicking事件處理程序(刪除一條筆記)修改的結果如圖21-6所示。

**圖 21-6 使用“call displayList”來調用放在過程中的那些塊**
## **程序計數器**
要理解“call”塊的運行機制,要想象應用中有一個指針,它隨著塊的運行而移動。在計算機科學中,這個指針被稱作程序計數器。
程序計數器隨著事件處理程序中的塊的運行而移動,當它遇到一個“call”塊時,它會跳到所遇到的過程中,并開始隨著過程中的塊的執行而移動,;當過程執行完成,程序計數器再跳回到此前的位置(“call”塊處),并從此處開始繼續移動。以“隨手記”為例,“remove list item”塊執行完成后,程序計數器跳到displayList過程中,并隨過程中的塊(設置NotesLabel.Text屬性為空,以及foreach循環)移動;最后程序計數器在回到TinyDB1.StoreValue塊。
## **為過程添加參數**
過程displayList將冗余代碼重整到一處,這使得程序更加容易理解,你可以在更高層次上理解這些事件處理程序,而忽略掉如何顯示列表的細節。這樣做的另一個好處是,如果想要修改列表的顯示方式,就只需修改一處代碼(而不是三處)。
就過程的通用性而言,displayList是有局限的,因為該過程是針對特定的列表(notes)而設定的,而且用指定的label(NotesLabel)來顯示列表內容,它不能用于顯示其他列表,比如應用的用戶列表,因為過程中的要素定義的過于具體。
App Inventor以及其他編程語言都提供了一種稱為參數的機制,用于構造更為通用的過程。過程為了實現它的預設功能所必須的信息就由參數來提供,以睡前刷牙為例,有可能將牙膏的類型和刷牙時間設定為刷牙過程的參數。
通過點擊過程塊左上角的藍色標記,就可以為過程設定參數。對于displayList過程,我們定義了一個名為“list”的參數,如圖21-7所示。

**圖 21-7 在過程中引入了list作為參數**
即使是定義了參數,但foreach塊中仍然直接引用特定列表“notes”(插入到foreach塊的“in list”插槽中)。而我們希望在過程中使用我們傳遞的參數list,因此將對“global notes”的引用替換成對“get list”的引用。如圖21-8所示。

**圖 21-8 現在foreach中使用了傳遞來的參數“list”**
新版本的過程更加通用:在調用displayList時,無論傳入什么樣的列表,displayList都能顯示它。在向過程添加參數時,App Inventor會自動為“call”塊添加一個對應的插槽,因此當displayList添加了參數list之后,“call displayList”塊就變成圖21-9中的樣子。

**圖 21-9 現在調用displayList時,需要指明要顯示的列表**
過程定義中引入的參數list被稱為“形式參數”,而“call”塊中與之相對應的插槽被稱為“實際參數”。當在應用中的某處調用過程時,必須為過程中的每個“形式參數”提供一個“實際參數”。
對于“隨手記”的應用來說,將列表“notes”作為實際參數添加到“call”塊的list插槽中。ListPicker.AfterSelection的修改結果如圖21-10所示。

**圖 21-10 在調用displayList時,將notes作為實際參數傳入**
現在當displayList被調用時,列表notes被傳遞到過程中,來取代形式參數list。此時,程序計數器隨著過程中的每個塊的運行,它的指向是參數list,而實際上處理的是變量notes。
由于有了參數,過程displayList可以用于處理任何列表,而不僅僅是notes。例如,如果“隨手記”應用可以在一組用戶中共享,而你想查看一下用戶列表,就可以調用displayList并傳入userList參數。如圖21-11所示。

**圖 21-11 過程displayList可用于顯示任何列表,而不僅僅是notes**
## **過程的返回值**
關于過程displayList的可重用性,還有一個問題需要討論——你能猜到是什么嗎?如前所述,它可以顯示任何數據列表,但也只能在標簽NotesLabel中顯示。如果你想用其他的界面元素(如另一個label)來顯示列表(如userList),該如何是好呢?
一個方法就是重構過程——將它的功能從“用指定label顯示列表”改為“只返回一個文本對象,它可以被顯示在任何地方”。為此,需要使用“procedure result”塊來取代“procedure”塊,如圖21-12所示。

**圖 21-12 “procedure result”塊**
你會發現與“procedure”塊相比,“procedure result”塊的底部有一個額外的插槽,將一個變量放入插槽,這個變量將被返回給調用者。因此,正如調用者可以向過程以參數的方式傳入數據一樣,過程也可以以值得方式將數據返回給調用者。
圖21-13顯示了上述過程的改寫版本,現在使用的是“procedure result”塊。注意,由于過程的作用變了,因此名稱也由displayList改為convertListToText(將列表轉換為文本)。

**圖 21-13 過程convertListToText返回一個文本對象,調用者可以將其放在任何一個label中**
在圖21-13所示的塊中,變量text用來保存foreach循環中通過遍歷列表而生成的文本。用text變量取代之前使用的過于具體的NotesLabel組件。在foreach執行完畢后,變量text包含了列表中的所有項,而且項之間以換行符“\n”分隔(即“item1\nitem2\nite3”)。最后,將變量text插入return插槽,返回給調用者。
在定義“procedure result”時,與“procedure”相比,對應的“call”塊看起來略有不同,如圖21-14中所做的比較。

**圖 21-14 下面的有返回值的“call”必須插入到某個插槽中**
不同的是在“call convertListToText”塊的左側有一個插頭,這是因為當“call”塊運行時,過程在執行一系列指令后將向“call”塊返回一個值,必須有某個插槽可以接收這個返回值。
在這種情況下,調用塊“call convertListToText”的返回值可以插入到任何一個label的Text屬性中,以notes列表為例,需要顯示列表的三個事件處理程序都可以調用這一過程,如圖21-15所示。

**圖 21-15 將列表notes的內容轉換為文本,并用NotesLabel顯示出來**
更重要的是,由于過程的定義更具通用性,不需要引用任何特定list或label,因此應用中可以使用convertListToText在任何一個label蒸南瓜顯示任何一個列表。像圖21-16中的例子那樣。

**圖 21-16 這一過程再也不必與一個特定的Label組件捆綁在一起**
## **在應用中重用塊**
通過過程的方式實現代碼的重用不必只限于單獨的應用,有許多過程,如convertListToText,可以用在你創建的任何應用中。事實上,有許多組織和編程社區都在為他們感興趣的領域創建過程代碼庫,例如動畫過程的代碼庫。
通常編程語言會提供一個“import(導入)”功能,可以在任何應用中引入其他的代碼庫。App Inventor目前沒有這項功能,不過正在開發之中。同時,也可以在一個特定的“庫應用”中創建一些過程,并復制該應用的代碼,作為一個新建項目的基礎代碼。
## **第二個例子:求兩點間距離**
在displayList(convertListToText)例子中,我們將過程定義描述為一種消除冗余代碼的方法:你開始寫代碼,隨后發現代碼存在冗余,于是整理代碼消除冗余。無論如何,一個軟件的開發人員或開發團隊在應用開發的初期都會創建很多過程,同時也考慮到要重用部分代碼。這樣的規劃可以在項目過程中節省大量時間。
考慮一項應用:確定離某人當前位置最近的本地醫院,某些東西在緊急情況下會派上用場的。以下是這個應用的高層設計描述:
**應用啟動時,以英里為單位計算兩點之間的距離,起點是當前所在位置,終點是發現的第一家醫院。然后再尋找第二家醫院,以此類推。在求得若干個距離后,判斷最短距離的醫院,并顯示它所在位置的地址。**
從以上描述中,你能斷定應用中需要什么樣的過程嗎?
通常,一段描述中的動詞提示了所需的過程。重讀一遍描述,正如“等等”所提示的,這是另一個線索。這種情況下,“求出兩點之間的距離”與“判斷這些距離中最短的”成為兩個必需的過程。
現在考慮設計一個過程distanceBetweenPoints(兩點間距離)。在設計過程時,首先要確定過程的輸入及輸出:調用者需要向過程傳遞實現過程的功能所需的參數,而過程要向調用者返回執行結果。在這里,調用者需要向過程傳遞兩個點的經度及緯度值,如圖21-17所示;而過程的任務是以英里為單位返回兩點之間的距離。

**圖 21-17 調用者想過程傳遞了4個參數,并收到一個距離**
圖21-18中顯示了我們在本章開始時提到的那個過程,使用公式求得兩個GPS坐標點之間的近似英里數。

**圖 21-18 過程distanceBetweenPoints**
圖21-19顯示了對上述過程的兩次調用,每次都會求出當前位置與指定醫院之間的距離。

**圖 21-19 兩次調用distanceBetweenPoints過程**
第一次調用中,起點為用戶當前所在位置的LocationSensor(位置傳感器)讀數,終點是St. Mary's hospital(圣瑪利亞醫院),計算的結果保存在變量distanceStMarys中;第二次調用也類似,只是將終點的數據改為CPMC Hospital(加州太平洋醫療中心醫院)的經緯度。
接下來程序比較兩個距離并返回最近的醫院。但是如果還有更多的醫院,那就需要在一個距離列表中進行比較,并找到最小值。依你所學,你能寫出這個過程嗎?將其命名為findMinimum,接受一個數值列表作為參數,并返回最短距離在列表中的索引值。
## **小結**
像App Inventor這樣的編程語言提供了一個內置功能的基本集,而過程是一種新功能的提取,它擴充了app inventor語言。App Inventor不提供顯示列表的塊,于是由你來做;那么是否需要一個計算兩個GPS坐標間距離的塊呢?答案是靠我們自己來創造。
想要建造大型的、可維護的軟件,以及在解決復雜問題時免于不斷地糾纏于細節之中,則定義高級過程的能力是至關重要的。過程是將代碼塊封裝起來,并起一個名字。在編寫過程時,你會關注這些塊的細節,但對程序的其他部分而言,這個過程只是一個抽象的名字,你可以在更高層次上來引用它。
- 簡介
- 序言
- 前言
- 第 1 章 Hello 貓咪
- 第 2 章 油漆桶
- 第 3 章 打地鼠
- 第 4 章 開車不發短信
- 第 5 章 瓢蟲快跑
- 第 6 章 巴黎地圖旅游
- 第 7 章 安卓,我的車在哪?
- 第 8 章 總統測驗
- 第 9 章 木琴
- 第 10 章 出題及答題
- 第 11 章 廣播中心
- 第 12 章 遙控機器人
- 第 13 章 亞馬遜掌上書店
- 第 14 章 理解應用的結構
- 第 15 章 軟件工程與應用調試
- 第 16 章 應用中的存儲
- 第 17 章 創建動畫應用
- 第 18 章 程序中的決策:條件塊
- 第 19 章 數據列表編程
- 第 20 章 循環
- 第 21 章 定義過程
- 第 22 章 數據庫
- 第 23 章 傳感器
- 第 24 章 與Web API通信