編寫Hello World應用程式通常被認為,是學習任何程式設計語言的第一步。在這一章,你将建立iOS版的Hello World應用程式作為起步,快速了解Xcode這個開發iOS應用程式的主要工具。
下一步,你将學習Objective-C的基礎知識。在此基礎之上,将探索類(class)與對象(object)的知識,它們是建構應用程式的主要基石。與此同時,你将建立CarValet應用程式,練習一些類的編寫,并學習屬性(property)的知識。在本章末尾,你将在指導下完成程式設計挑戰題以探索子類擴充的知識。當學完本章時,你将獲得CarValet應用程式的初始版本,以及足夠完成本書學習的Objective-C知識。
2.1 使用模闆建立Hello World 應用程式
建立Hello World示例應用程式的最簡單方法,就是使用Xcode的預設模闆。在接下來的步驟中,你将建立一個新的項目,然後修改它,使之能列印“Hello World”,并能在iOS模拟器上運作。在建立第一個Xcode項目時,你将學到關于Xcode這一關鍵工具的總體介紹,以及如何建立和操縱項目。
2.1.1 建立Hello World 項目
啟動Xcode,你将看到如圖2-1所示的Xcode歡迎頁面(如果不小心關閉了這個頁面,可通過選擇Window | Welcome to Xcode(歡迎使用Xcode)或按下Cmd+Shift+1組合鍵來重新顯示)。單擊Create a New Project。也可以在Xcode中選擇File | New | Project(或按下Cmd+Shift+N組合鍵)。之後将會出現如圖2-2所示的模闆選擇視窗。預設情況下,模闆選擇視窗被嵌套在一個新的、名為工作區(workspace)的大視窗中。這個單獨的工作區視窗中包含Xcode全部的編輯器和檢查器(inspector)特性。
圖2-1 Xcode 的歡迎頁面
在圖2-2所示的模闆選擇視窗中,選擇左邊的iOS菜單下的Application,然後選擇SingleView Application(單視圖應用程式),單擊Next按鈕。
圖2-2 Xcode 模闆選擇視窗
警告:截圖和功能可能會有所不同
Xcode 會定期更新,這意味着以上操作指令可能會與Xcode 中或蘋果公司網站上的實際界面元素有細微不同。當遇到這種情況時,通常可以找到一些顯而易見且能完成相同指令的辦法,比如可以在不同的位置查找同名的控件或元件,此外還可以在類似的區域尋找類似的名稱。如果以上方法都行不通,請檢視本書配套網址(www.infomit.com/title/9780321862969)上的更新内容,如果還是找不到,請發送回報資訊。
接下來顯示的是Xcode項目選項面闆(project options panel),如圖2-3所示。在這個視窗中,請遵循下列步驟:
(1) 在Product Name中輸入HelloWorld。
(2) 将Company Identifier(公司辨別符)設定為你所在的公司,使用反向域名命名法。例如,Maurice的公司辨別符是com.mauricesharp或com.klmapps,Rod的公司辨別符是com.strougo,而Erica的公司辨別符是com.sadun。如果沒有公司辨別符,蘋果公司建議使用edu.self。
(3) 單擊Next按鈕。
圖2-3 Xcode 項目選項面闆 Class Prefix(類字首)可以讓你指定包含在類名中的字首,Xcode會在定義類時,自動将其添加在類名前。使用字首是一種好的方式,能有效地避免類名與現有或将來的iOS系統類發生命名沖突。例如,如果建立了類SlideMenuViewController,而某次iOS系統更新時增加了一個同名的類,那麼至多可能是應用程式無法編譯。更糟糕的情況是,應用程式說建立的執行個體是同名系統類的執行個體,不是自己所寫的類的執行個體,這會帶來非常糟糕的bug。但如果指定字首為“MS”,那麼Xcode會建立名為MSSlideMenuController的檔案和類,進而避免潛在的沖突。
在本書中,為了讓命名更簡短,并未使用字首,但是你應當根據自己的開發者名稱或公司名稱設定字首。
專家提示:字首格式
最典型的類字首是一小串大寫字母(蘋果公司推薦使用3 個字母),通常是你或你公司名稱的單詞首字母。在公司内部項目或開源項目中,這種格式很常見,但可讀性并不是最好。以下是一個滑動菜單類的各種不同的名稱,這并不是我實際編寫過的一個類,是我想出來的。但不妨試着觀察一下清單中的這些類名條目,按照你浏覽代碼時的方式,看看哪一個更容易快速識别:
SlideMenuViewController
MSSlideMenuViewController
MS_SlideMenuViewController
msSlideMenuViewController
ms_slideMenuViewController
極其可能的是,全部大寫且沒有分隔符的版本最難識别。小寫的稍稍容易點,但難以識别的程度僅次于前者,更容易識别的是那些帶有分隔符的版本。你應當選擇能與符号名稱簡易搭配使用的類字首。長遠來看,你将節省大量的開發與調試時間。
項目選項面闆中的其他條目包括以下這些:
• Product Name (産品名稱)——應用程式的名稱,它會被顯示到裝置螢幕上。如你在第15 章“部署應用程式”中将要看到的,後續還可以對其修改。
• Organization Name (組織名稱)——作為自動生成的源檔案頭部注釋的一部分,用作版權屬性。
• Devices(裝置)——應用程式的目标裝置平台,可以是iPhone、iPad 或Universal(通用應用程式)——可以同時運作在這兩種平台上的應用程式。
上一個Xcode視窗面闆詢問你Hello World項目的存儲位置。如果願意的話,Xcode還可以為項目建立本地的git倉庫。針對目前這個Hello World示例,将Source Control(源碼控制)複選框保持為未選中狀态。選擇一個檔案夾并單擊建立,如圖2-4所示。
在單擊Create按鈕之後,你應該能看到在Xcode中,自己的Hello World新項目已被打開,它帶有一些Xcode自動生成的檔案。至此,你得到一個看似不怎麼有趣,但是功能完備的應用程式。如果立刻單擊Run(運作)按鈕,你的app會顯示一個頂部帶有狀态條的白色螢幕。
圖2-4 Xcode 項目檔案夾位置面闆 2.1.2 Xcode 項目界面快速導航
開發應用程式時,你将在Xcode中花費大量時間。圖2-5顯示了展開項目檔案并選擇ViewController.m檔案後的Xcode界面。
圖2-5 Xcode 項目界面的組成部分
以下是對Xcode界面的快速導航。接下來的幾章将探讨Xcode界面中不同元件的更多細節知識。下面的數字與圖2-5中顯示的數字一一對應:
(1) 單擊Run按鈕(在左邊),編譯應用程式,并在模拟器或裝置中啟動應用程式。單擊并保持(長按)Run按鈕,你會看到額外的選項,比如用于測試或分析項目的選項。右側的按鈕可用于停止正在運作的應用程式、正在執行的建構或已發起的其他動作。
(2) 在這個分為兩段的彈出式菜單按鈕中,左段用于選擇和編輯方案(Scheme),也就是關于你想運作什麼以及如何運作該應用程式。右段用于選擇在哪兒運作應用程式:是在某種模拟器中還是在某個已連接配接的裝置上。
(3) Status(狀态)區域向你展示上一動作的執行結果或目前動作的進度。動作包括建構應用程式、運作應用程式或是下載下傳應用程式到裝置上。
(4) Editor(編輯器)按鈕用于配置編輯器(圖2-5中标為7的區域)中顯示的内容。左側按鈕在圖中已被選中,隻顯示一件東西,即主編輯區。中間的Assistant(輔助編輯)按鈕将編輯區劃分為兩塊,在右側會顯示與主編輯器相關的檔案,在右側通常情況下會顯示頭檔案。最後一個按鈕用于顯示源代碼差異,并且可以檢視檔案的不同版本。這一點在使用源代碼倉庫跟蹤代碼變動時非常有用。注意,不能同時顯示輔助編輯和源代碼視圖。
(5) 這個區域控制Xcode中工作區視圖的總體布局。第一個按鈕顯示或隐藏Navigation(導航器,圖2-5中的區域6)。第二個按鈕控制Debugger(調試器,區域10)。最後一個按鈕顯示Utilities(區域8和9)。
(6) Navigator(導航器)可以顯示項目的不同視圖。頂部的圖示控制視圖的類型。在圖2-5中,被選中的是檔案夾圖示,是以這個區域顯示的是檔案以及基于組(group)的項目導航器。其他圖示是:項目符号導航器,用于在全項目範圍内進行搜尋的放大鏡圖示,用于檢視建構問題的警告三角形圖示,還有單元格測試導航器,調試導航器,斷點清單,最後是日志視圖。容易混淆的一點是,檔案群組導航器中顯示的檔案結構和Finder中的檔案結構,這兩者可以是不同的。在導航器中,組被顯示為檔案夾圖示,但這些與Finder中的檔案夾是不同的。如果想讓組和Finder檔案夾保持一緻,就需要在Finder中添加這些檔案夾,并将源檔案儲存在正确的位置。
(7) Editor(編輯器)是你将在Xcode中花費大多數時間的地方。目前,它顯示的是源碼編輯器。它還可以顯示Interface Builder(界面生成器)、資料模型編輯器或運作時調試測量儀器。
(8) 工具區域顯示了不同種類的工具,包括檔案資訊、快速幫助(目前被選中的内容)、資料模型細節檢視器、視圖屬性(比如顔色、按鈕标題以及尺寸,包括螢幕大小和限制)。
(9) 工具區域的底部為庫區域。在此處你能夠找到Interface Builder中構造應用程式所需的可拖曳元件、代碼片段,甚至包括圖檔和其他媒體檔案。注意在工具區域的底部可能隻看到含有庫選擇圖示的一行。要展開庫,請單擊并向上拖動選擇器工具欄。
(10) 調試區域包含三個主要部分。頂部工具欄的左邊包含用于在運作中暫停以及單步執行的控件。右側包含目前選中的檔案。底部被分割成兩個主要區域,一個用于檢查變量,另一個用于打開控制台。在整本書中,你将學習到有關的更多具體知識,特别是在第14章“Instruments和調試”中。現在你對Xcode已有一定了解,該建立第一個應用程式了。
2.1.3 添加Hello World 标簽
預設情況下,Xcode建立設定為運作在iPhone或iPad上的項目;當建立一個項目時,下一個項目使用前一次的設定。新項目一開始就包含你建立自己的應用程式所需的所有類和支援檔案。當運作一個新項目時,它會顯示一個空白的白色螢幕。
在螢幕上看到Hello World的最快方法是往項目裡添加标簽。保持項目打開,執行以下步驟(見圖2-6):
(1) 通過觀察方案彈出菜單的右側(圖2-5中的區域2),确認項目被設定為運作在模拟器中。如果右側沒有顯示“iPhone Retina(4-inch)”,單擊方案彈出菜單的相應一邊并且選擇這一條。注意下拉菜單包含兩部分:左側是方案彈窗,右側允許你選擇應用程式在哪裡運作。你應隻将Hello World标簽添加到iPhone故事闆中,于是,應用程式在iPhone而不是iPad上運作是非常重要的。
(2) 在Xcode項目導航器中單擊并選擇Main_iPhone.Storyboard檔案。Interface Builder會在編輯區域打開。在非常罕見的情況下,Interface Builder并沒有打開。這時你要檢查并确定沒有單擊顯示源代碼差異的第三個按鈕(參見圖2-5中的區域4)。如果源代碼控制按鈕未選中,嘗試選擇一個.m或.h檔案,然後再次選擇故事闆檔案。如果還是不管用,那麼退出Xcode并重新開機。
(3) 在右下方的查找面闆中,輸入label并将這個标簽對象拖曳到故事闆的畫布中。
(4) 在Xcode工作區左側在Utility區域中,在Attributes檢查器的文本框内輸入Hello World,将這個标簽調整得好看一些。圖2-6顯示這個标簽在視圖控制器(view controller)的頂部水準居中。
(5) 單擊Run按鈕,編譯并運作Hello World應用程式。
圖2-6 通過Xcode 的界面生成器添加Hello World 标簽 當運作Hello World應用程式時,可以看到iPhone模拟器,記得使用方案彈出菜單,将運作的目的地設定為iPhone模拟器。
這就是建立簡單Hello World應用程式所需的所有步驟。你甚至不需要編寫任何代碼。在下一節,你将學習一些Objective-C基礎知識,回到這個Hello World應用程式,并添加一些代碼以練習Objective-C。
圖2-7 運作在iPhone 模拟器中的Hello World 應用程式 使模拟器适應你的螢幕
圖2-7 所示的模拟器是全尺寸的、4 英寸Retina 顯示屏,并且可能非常大,特别是當在筆記本電腦螢幕上工作時。可以使用Simulator Window | Scale 菜單更改顯示屏的顯示大小。
2.2 Objective-C 新兵訓練營
要成為熟練的iOS開發者,你需要學習Objective-C,它是iOS和Mac的主要程式設計語言。Objective-C是一種強大的面向對象程式設計語言,允許你利用蘋果公司的Cocoa和Cocoa Touch架構,建構應用程式。在這一章,你将學習基本的Objective-C技巧,開始iOS程式設計。你将學到接口、方法以及更多其他知識。要想完成這些學習,必須往Hello World應用程式中添加一些自定義的類,練習所學的知識。
注意:
這一節涵蓋的Objective-C 知識足以讓你了解本書所展示的代碼,并實作大多數章末尾的挑戰題。然而,Objective-C 有太多的内容,我們沒法在這麼小的一節裡邊全部涵蓋,而這些沒有涵蓋的内容中,有一些對于編寫産品級品質的應用程式是非常重要的。學習Objective-C的一份重要材料是Learning Objective-C 2.0:A Hands-on Guide to Objective-C for Mac and iOS Developers,2nd edition,作者是Robert Clair。還可以使用Objective-C Programming:The Big Nerd
Ranch Guide,作者是Aaron Hillegass,這本書涵蓋了更進階的知識。
2.2.1 Objective-C 程式設計語言
Objective-C是ANSI C的嚴格超集。C語言20世紀70年代早期由AT&T開發的一種面向過程的編譯型語言。Objective-C由Brad J. Cox在20世紀80年代早期開發,向C語言增加了面向對象的特性。它将C語言的組成部分與Smalltalk-80中産生的概念加以混合。
Smalltalk是最老和最有名的面向對象程式設計語言之一,是由Xerox PARC開發的一種動态類型的互動式語言。Cox将Smalltalk的對象和消息傳遞系統層疊到标準C語言之上,進而建構了一種新的語言。這種方法允許應用程式員在繼續使用熟悉的C語言進行開發的同時,在那種語言内部使用基于對象的特性。在20世紀80年代後期,Objective-C被史蒂夫·喬布斯的計算機創業公司Next的NeXTStep作業系統選作主要開發語言。NeXTStep成為OS X直到iOS的精神和字面意義上的先驅。
Objective-C 2.0在2007年10月随着OS X Leopard一起釋出,它引入了許多新特性,如屬性和快速疊代。在2010年,蘋果公司更新了Objective-C語言,增加了Block這個C語言擴充,它提供了匿名函數,并允許開發者将Block作為對象進行處理(你将在第13章中學習更多内容)。在2011年夏,蘋果公司引入了自動引用計數(Automatic Reference Counting,ARC),這個擴充大大簡化了開發,它允許應用程式員将注意力集中在應用程式語義上,而不用擔心記憶體管理(準确地說,ARC是編譯時擴充而非語言擴充)。最近,Objective-C被擴充為支援字面量(定義靜态對象的方式)以及索引(通路數組和字典中元素的方式)。蘋果公司持續改進Objective-C語
言,是以要注意新的iOS和Xcode更新版本的釋出說明。面向對象程式設計使用了ANSI C沒有的表特性。對象是一種資料結構,它關聯了一組公開聲明的函數調用。Objective-C中的每個對象包含一些執行個體變量(instance variable),也就是這種資料結構的資料域(或字段);還包含一些方法(method),也就是該對象所能執行的函數調用。面向對象的代碼使用這些對象、變量和方法來引入一些程式設計抽象來增加代碼的可讀性和可靠性。你有時候可能會看到執行個體變量被縮寫為iVar,方法被稱作消息(message)。
對象使用類(class)進行定義。可以将類認為是決定對象最終長什麼樣的模闆:如何檢視狀态(執行個體變量),以及支援什麼行為(消息)。
類本身通常不做太多事情。它們的主要用途是建立功能完整的對象。對象被稱作執行個體(instance),也就是基于類所提供模闆的起作用的實體。之是以命名為“執行個體變量”,是因為它們隻存在于類的執行個體中,而不是類自身的内部。當往第4步的文本框中輸入“Hello World”時,實際上也就設定了一個UILabel對象的text執行個體變量的值。UILabel類本身并沒有text變量可被設定。所有這些事情,以及建立标簽執行個體的代碼已經自動完成了。
面向對象程式設計讓你建構可重用的代碼并從面向過程開發的正常控制流中解耦。面向對象的應用程式圍繞着對象和它們的方法所提供的自定義資料結構來開發。iOS的Cocoa Touch以及Mac OS X的Cocoa提供了一個包含大量這種自定義對象的倉庫。Objective-C解鎖了那個倉庫,并允許你用最少的努力和代碼,基于蘋果公司的工具箱建立有效而強大的應用程式。
注意:
iOS 的Cocoa Touch 中以NS 開頭的類名,例如NSString 和NSArray,可追溯到NeXT公司。NS 代表NeXTStep,運作在NeXT 計算機之上的作業系統。蘋果公司于1996 年收購了NeXT。
調用函數:也就是消息傳遞
Objective-C是C語言的超集,正因為如此,它沿用了大多數相同的文法。讓大多數Objective-C的學習者感到困惑的一點,在于消息傳遞的文法——用于調用或運作類執行個體所實作的方法。不像函數調用時使用的“函數名(參數清單)”文法,傳遞消息給對象時要使用方括号。
一條消息讓一個對象執行一個方法。實作這個方法,産生一個結果,是這個對象的職責。方括号中的第一個元素是消息的接收者,也就是實作這個方法的對象;第二個元素是方法名稱以及可能會有的傳給那個方法的一些參數,它們一起定義了你想要發送的消息。在C語言中,你可能會這麼寫:
<span style="font-size:14px;">printCarInfo(); // This function prints out the info on the default car</span>
但是在Objective-C裡,你這麼寫:
<span style="font-size:14px;">[self printCarInfo]; // This method prints out the info on the default car</span>
在C語言中,想要運作這個函數的目标對象被假定為目前的對象。在某些語言中,你可能看到this.printCarInfo()。在Objective-C中,self代表目前對象,大體上與this類似。
在其他語言中,你可能會使用類似someOtherObject.printCarInfo()的代碼,在另一個對象上調用方法,假設someOtherObject擁printCarInfo()函數。在Objective-C中,可以使用以下代碼:
<span style="font-size:14px;">[someOtherObject printCarInfo]; // This method prints out the info on the default car</span>
盡管文法不同,但方法(method)基本上講就是在對象上執行的函數(function)。除了Objective-C的類型之外,方法中的類型可以使用标準C語言中同樣的類型。不像函數調用,Objective-C限制了可以實作和調用方法的主體。方法屬于類,并且類的接口定義了哪些方法是公開的,或者說,是面向外部世界的聲明。
當函數包含一個或多個參數時,代碼就開始看起來不一樣了。假定必須将汽車對象myCar 傳遞給printCarInfo()函數。在C語言中,你會這麼寫:
printCarInfo(myCar); // Print the info from the myCar object
在Objective-C中,你會這麼寫:
<span style="font-size:14px;">[self printCarInfo:myCar]; // Objective-C equivalent, but with poor method name</span>
在Objective-C中,你被鼓勵依次放置方法名和參數,是以極其可能将printCarInfo方法重命名為:
<span style="font-size:14px;">[self printCarInfoWithCar:myCar]; // More clear as to which car it will print out</span>
現在将示例再深入推進一步,假定必須傳遞顯示資訊時的字型大小。在C語言中,你會這麼寫:
<span style="font-size:14px;">printCarInfo(myCar,10); // Print the info using a font size of 10</span>
在Objective-C中,你會使用如下代碼:
<span style="font-size:14px;">[self printCarInfoWithCar:myCar withFontSize:10]; // Print using a font size of 10</span>
通讀代碼時可以立刻發現Objective-C清晰了不少。讓我們再深入一步。現在假定你有三個參數:汽車對象,資訊的字号,還有表示文字是否需要粗體顯示的布爾值。在C語言中,你會使用如下代碼:
<span style="font-size:14px;">printCarInfo(myCar, 10, 1); // Using 1 to represent the value of true in C</span>
在Objective-C中,你會這麼寫:
<span style="font-size:14px;">[self printCarInfoWithCar:myCar withFontSize:10 shouldBoldText:YES];</span>
注意:
在Objective/Cocoa 中,布爾值由BOOL 類型提供。BOOL 類型的标準值是YES/NO而不像C 語言中的true/false。盡管也可以引入C 标準庫,并且使用C 語言的Boolean 類型,但不推薦。
方法名與參數依次交錯放置,能有效地讓Objective-C 的消息傳遞變得容易閱讀和了解。在C 語言和其他語言中,你不得不時常參考函數定義以确定每個參數是什麼以及參數的順序。在Objective-C 中,這些都很清晰,就在你面前。在使用一些含有5 個或更多個參數的UIKit 方法調用時,你會特别明顯地體會到這一點。
在Objective-C中,方法的參數由冒号(:)字元進行分隔,在參數值之間帶有方法名稱中的一部分。你的方法可能傳回一個值或對象,同樣C語言也可以傳回一個值。在C語言中,你會使用以下代碼:
<span style="font-size:14px;">float mySpeed = calculateSpeed(100,10); // returns the speed based on distance / time</span>
在Objective-C中,你的方法調用看起來如下所示:
<span style="font-size:14px;">float mySpeed = [self calculateSpeedWithDistance:100 time:10];</span>
注意:
如果Objective-C 的方法聲明和消息傳遞文法對你來說還是不清楚,不要擔心。在本章的下一節,你将有充分機會加以練習。
蘋果公司提供圖2-8 所示的文字來闡述Objective-C 方法調用的組成部分。要看到更多資訊,
請看https://developer.apple.com/library/iOS/#referencelibrary/GettingStarted/RoadMapiOS/Languages/
WritingObjective-CCode/WriteObjective-CCode/WriteObjective-CCode/html。
圖2-8 Objective-C 方法調用的組成部分 除了讓參數更清晰,方法還引入了一個更加強大的特性。方法可以通路類中定義的所有東西。換句話說,可以通路執行個體變量以及任意類執行個體中實作的方法。在這種意義上,方法如何運作對于調用者對象是透明的。某個特定方法的實作代碼甚至是整個類可以完全改變而不需要将别的任何地方修改。這在更新或替換應用程式中的特性時是非常有用的:可能是讓它們更有效地更新新的硬體特性,甚至徹底替換通信的處理方式。
2.2.2 類和對象
對象是面向對象程式設計的核心。可以通過建構類定義對象,類即為建立對象的模闆。在Objective-C中,類定義描述了如何建構屬于這個類的新對象。例如,要建立汽車對象,需要定義Car類,并且在需要時用這個類建立新的對象。與C語言類似,在Objective-C中實作類需要分兩處進行:頭檔案和實作檔案。頭檔案規定了外部世界如何與這個類互動:執行個體變量及其類型,方法及其參數和傳回值類型。就像契約,頭檔案承諾類的執行個體如何與别的對象對接。
實作檔案的内容即為類如何提供執行個體變量的值,以及方法被調用時如何響應。除了頭檔案中定義的公有變量和方法;在實作檔案中也可以定義變量與方法,并且實作檔案通常會包含私有的變量和方法。
每個類使用标準的C.h約定,在頭檔案中列出執行個體變量和方法。例如,你可能會像代碼清單2-1那樣定義SimpleCar對象。此處所示的Car.h頭檔案包含聲明SimpleCar對象結構的接口。
代碼清單2-1 聲明SimpleCar 接口(SimpleCar.h)
<span style="font-size:14px;">#import <Foundation/Foundation.h>
@interface SimpleCar : NSObject {
NSString *_make;
NSString *_model;
int _year;
}
@property float fuelAmount;
- (void)configureCarWithMake:(NSString*)make
model:(NSString*)model
year:(int)year;
- (void)printCarInfo;
- (int)year;
- (NSString*)make;
- (NSString*)model;
@end</span>
在Objective-C中,類、變量和方法采用駝峰式命名法。在Objective-C中,你會使用identifiersLikeThis而不是identifiers_like_this。類名首字母大寫,而其他的名稱首字母小寫。可以在代碼清單2-1中看到。類名SimpleCar以大寫首字母開頭。執行個體變量fuelAmount采用駝峰式命名法,但是以小寫字母開頭。示例如下:
- (void) printCarInfo
在Objective-C中,@符号用在特定的一些關鍵字中。此處展示的兩個元素(@interface和@end)劃分了類接口定義的開頭與結尾。類定義描述了一個含有5個方法與4個執行個體變量的對象。
在這4個變量中,隻有fuelAmount是公有的(public),意思是它在SimpleCar類的外部可以使用。花括号中的其他三個變量隻能被SimpleCar及其子類使用。這三個變量也可以定義在.m實作檔案中,但那樣的話它們将隻對SimpleCar類可見。如果想讓子類(比如ElectricCar)共享這些變量的話,這就成問題了。
私有變量中有兩個(_make和_model)是字元串類型。Objective-C通常使用基于NSString對象的類,而不是基于位元組(byte)的用char *聲明類型的C字元串。正如在這本書中到處可見的,NSString提供的功能遠遠多于C字元串。對于這個類,可以找出字元串的長度,查找和替換子字元串,颠倒字元串,提取檔案擴充名,以及更多。這些特性都被編寫到iOS(和Mac OS)的對象庫裡。私有的_year變量和公有的fuelAmount變量都屬于簡單類型。前者是int類型,後者是float類型。
使用前置的下劃線字元(_)是Objective-C中區分執行個體變量和getter方法的一般做法。使用x=_year是直接從執行個體變量獲得值,而x=[self year]是調用getter方法-(int)year,可以做一些必要的計算或者在傳回之前臨時計算值。setter方法類似getter,隻不過是用于設定執行個體變量的值。
重複一下,使用setter可以執行一些額外的工作,比如更新螢幕上的計數器。你将在以下内容中以及全書中學習建立與使用getter和setter。
第一個公有方法如下所示:
<span style="font-size:14px;">configureCarWithMake:model:year:</span>
這個完整的包含三段的聲明(包括冒号),才是該方法的名稱或選擇器(selector,有時也稱為簽名)。那是因為Objective-C在方法名中交替放置參數,使用冒号分隔每個參數。在C語言中,可以使用setProperties(char *c1,char *c2,int i)這樣的函數。Objective-C的方式,盡管更繁重,但提供更好的清晰性和不言自明的文檔。你不需要猜測c1和c2的作用,因為它們的用途直接在方法名稱裡聲明了:
<span style="font-size:14px;">[myCar configureWithMake:c1 model:c2 year:i];</span>
每個方法都有傳回參數。printCarInfo傳回void,year傳回int,而make和model都傳回NSString*類型。與C一樣,這些代表方法傳回的資料類型。void代表這個方法不傳回任何東西。在C語言中,與printCarInfo和year方法等價的函數聲明是void printCarInfo()和int year()。
使用Objective-C的“方法名分散在參數中”的方式,對新應用程式員來說可能看起來很奇怪,但是很快就會變成你非常喜愛的特性。當方法名告訴你什麼什麼參數該在哪兒時,就不需要猜測該傳遞什麼參數。你會在iOS程式設計中多次見到這個,特别是當使用respondsToSelector:這個方法調用時,這個方法讓你在運作時檢查對象是否能響應特定的消息。
注意代碼清單2-1中的頭檔案使用#import加載頭檔案,而不是#include。當導入(import)頭檔案時,Objective-C自動跳過已經被添加了的檔案。是以可以往各種各樣的頭檔案中添加@import指令,而不會有任何損失。
1. 定義實作檔案
.h檔案告訴外界如何與類的對象互動。.m檔案或實作檔案包含了賦予對象生命與力量的代碼。代碼清單2-2展示了SimpleCar類的某種實作。
代碼清單2-2 SimpleCar 類的實作檔案(SimpleCar.m)
<span style="font-size:14px;">#import "SimpleCar.h"
@implementation SimpleCar
- (void)configureCarWithMake:(NSString*)make
model:(NSString*)model
year:(int)year {
_make = [make copy];
_model = [model copy];
_year = year;
}
- (void)printCarInfo {
NSLog(@"--SimpleCar-- Make: %@ - Model: %@ - Year: %d - Fuel: %0.2f",
_make, _model, _year, [self fuelAmount]);
}
- (int)year {
return _year;
}
- (NSString*)make {
return [_make copy];
}
- (NSString*)model {
return [_model copy];
}
@end</span>
實作檔案通常與頭檔案配對,是以第一件事情即為導入那個頭檔案。在此處是SimpleCar.h。大多數類會導入其他頭檔案,并且可能聲明常量或做其他事情。類實作的主要部分位于@implementation和@end之間。
configureCarWithMake:model:year為每個私有執行個體變量設定值。除了fuelAmount之外,不能為目前的汽車對象單獨設定某個值。使用通路器方法(access method)讀取任何單個元素的值是可行的,例如代碼清單2-2底部定義的-(int)year。因為頭檔案為fuelAmount使用了@property,是以setter和getter方法,以及下劃線版本的變量已經為你建立好了。你将在本章後邊的“2.3.2節“屬性”中看到相關更多内容。
第一個方法設定三個非公有執行個體變量的值。printCarInfo将所有執行個體變量的目前值列印到日志中。最後三個方法是私有執行個體變量的getter方法。你可能注意到的一點是,配置方法和getter方法都與字元串的副本打交道。這是一種普遍的防禦性實踐,避免代碼意外地修改字元串的值。但當你意識到每個變量是一個指向NSString對象的指針時,就會了解了。如果将這個指針指派給字元串參數,那麼它将與這個字元串的擁有者指向相同的記憶體位置。如果原始擁有者改變了字元串的值,汽車對象會得到新的值,因為它指向相同的記憶體位址。
指派并傳回字元串的副本,會得到不同記憶體區域的新對象。這些副本可以被修改,而不會改變目前汽車對象的make和model。注意,你唯一需要注意的是,這隻适用于長期存在的執行個體變量。臨時字元串和對象不需要被拷貝。
2. 建立對象
你已經學到,類定義一個或更多個對象。類在運作時如何變成對象?要建立對象,你需要讓類為新對象配置設定足夠的記憶體,并且傳回一個指向這塊記憶體的指針。然後讓新對象初始化自己。你通過調用alloc方法處理記憶體配置設定,并且初始化發生在調用init時。如果正在建立SimpleCar對象,那麼可以使用如下兩行代碼:
<span style="font-size:14px;">SimpleCar *myCar = [SimpleCar alloc];
[myCar init];</span>
盡管看起來沒有多少代碼,但這将是你時常需要輸入的。幸運的是,Objective-C支援嵌套消息發送。這意味可以使用一個方法的結果作為另一個方法的接收者。一組嵌套消息的傳回值來自最後一條消息。
在之前的代碼中,第一行為汽車對象配置設定記憶體并且傳回一個指向那個對象的指針。第二行拿到了配置設定好的汽車對象并且加以初始化。init方法傳回初始化之後的對象。因為myCar已經指向正确的對象,而第二行不需要使用這個傳回值。使用嵌套可以将這兩行縮短為一行:
<span style="font-size:14px;">SimpleCar *myCar = [[SimpleCar alloc] init];</span>
此處,将消息alloc發送到SimpleCar對象的源代碼得到一個新的汽車對象,然後将消息init發送到新配置設定好的SimpleCar對象并傳回初始化後的汽車對象。這種嵌套在Objective-C中是很典型的。
你在此處看到的“配置設定後緊跟init”的模式是執行個體化對象的最常見方式。SimpleCar類指向alloc方法。它配置設定足夠存儲類定義中所有執行個體變量的新記憶體塊,将所有執行個體變量清理為0或nil,并傳回指向這個記憶體塊開始位置的指針。新配置設定的塊是執行個體,代表記憶體中單獨的對象。某些類,比如視圖,使用自定義的初始化方法,例如initWithFrame:。如你在本章後邊将看到的,可以編寫自定義的初始化方法,比如initWithMake:model:year:fuelAmount:。這種緊随記憶體配置設定進行初始化的模式廣泛存在。你在記憶體中建立這個對象,然後預設所有關鍵的執行個體變量。
3. 繼承方法
對象在繼承執行個體變量的同時會繼承方法實作。SimpleCar是一種NSObject,是以所有NSObject能夠響應的消息Simple Car也能夠響應。這就是myCar可以使用alloc和init進行初始化的原因。這兩個方法是由NSObject定義的,可用于建立和初始化任何SimpleCar執行個體,因為它繼承自NSObject類。Objective-C中的所有類都最終繼承自NSObject,NSObject處于它們繼承樹的頂端。
提示:繼承的方法
如果檢視Hello World 項目的AppDelegate 或ViewController 類的.h 檔案,就會看到AppDelegate 繼承自UIResponder,并且ViewController 繼承自UIViewController,而UIViewController 接下來繼承自UIResponder。如果選擇并右擊UIResponder,選擇Jump to Definition(跳到定義),那麼Xcode 會為你顯示UIResponder 的聲明,在那裡可以看到,它也繼承自NSObject。
作為另一個示例,當應用程式中有數組時,你有可能會使用NSArray或NSMutableArray—— 一種允許你增删元素的NSArray。所有數組方法都可以被可修改的數組以及它們的子類使用。可以統計數組中元素的個數,根據索引數字取出對象等。
警告:
有些類對“子類化”(subclassing)并不友好。它們是作為類簇(class cluster)來實作的;也就是,類自身會根據一些标準,建立一些其他類的對象。NSArray 和NSString 都是類簇的示例。它們以最能有效利用記憶體的方式,使用不同的類建立對象。所有這些類都在文檔中做了清晰标記。在子類化系統類之前,要仔細檢查一下其是否為類簇。
子類可以實作與超類具有相同選擇器的方法。在子類對象上調用此方法将執行新的方法。這取決于這個方法如何實作,要麼特殊化,要麼覆寫超類行為。特殊化(Specializing)的意思是(執行新邏輯的同時)還讓超類方法運作,方法是将消息發送到super對象,super是代表超類的特殊辨別符。覆寫(Overriding)的意思是并不将消息發送到超類,超類的行為從不執行。一個不錯的示例就是初始化方法。需要確定繼承鍊中的每一個類都有計劃初始化自身。不過方法隻需要記得調用自身的超類。初始化方法總是包含以下形式的一行:
<span style="font-size:14px;">self = [super init];</span>
這會将目前對象的執行個體設定為超類建立的執行個體。接下來就是初始化對象的剩餘部分。
警告:先初始化超類
非常重要的是,要在做任何特定于類的事情之前調用超類初始化。如果試着首先操作對象,此時任何超類,包括NSObject,所提供的一切東西都沒有建立起來。盡管執行個體變量可以傳回一個值,并且方法調用可能順利進行,但是這個對象将處于一種未定義的狀态。
最好的情況是,在初始化時遇到一次應用程式崩潰(crash),而更有可能的是,在之後某一天将遇到一些随機性的崩潰或者奇怪的行為。你将在後邊學到如何編寫正确的init 方法。
4. 指向對象
你已經了解到,使用類建立對象是非常簡單的事情。當擁有一個新建立(配置設定)和初始化的對象時,下一步就是引用并使用這個新對象。在Objective-C中,你使用*字元表示變量是指向對象的指針,這在代碼清單2-2中的_make和_model變量聲明處可以看到。_make和_model變量都指向對象(NSString)并且變量名前邊必須有*。
其他變量屬于原始類型,不是對象。變量自身,或更準确說記憶體位址,儲存的是值而不是對象的位址。_year變量是原始類型(int)的示例,是以不需要*字元。
當向對象發送消息時,要去掉*字元。在以下代碼片段中,myCar對象被建立為SimpleCar類的執行個體,并且printCarInfo方法被調用:
<span style="font-size:14px;">SimpleCar *myCar = [[SimpleCar alloc] init];
[myCar printCarInfo];</span>
如果想建立指向同一個SimpleCar對象的另一個指針,可以使用以下代碼:
<span style="font-size:14px;">SimpleCar *sameCar = myCar;</span>
或這段代碼:
<span style="font-size:14px;">id sameCar = myCar;</span>
id是一種會被翻譯為NSObject *的特殊類型,這個翻譯結果已經包含*字元,表示指向對象的指針。
意識到myCar和sameCar指向相同對象是非常重要的。如果使用sameCar指針修改make、model和year,那麼[myCar printCarInfo]會顯示新的值。
2.3 CarValet 應用程式:實作Car 類
現在是時候練習使用Objective-C語言建立方法、變量和類了。一開始先為CarValet應用程式建立項目。當繼續閱讀這本書時,你會将這個應用程式從簡單逐漸開始變成完整的iOS應用程式,它可以運作在iPhone和iPad上,使用原生使用者界面元素和iOS的其他部分。
第一步是建立CarValet應用程式項目。使用與建立HelloWorld項目時相同的步驟:
(1) 在Xcode中,選擇File | New | Project(或按下Cmd+Shift+N組合鍵)。
(2) 選擇iOS Single View Application模闆,與你建立HelloWorld項目時選中的模闆相同。
(3) 在下一個面闆中,在應用程式的名稱文本框中輸入CarValet。確定Devices被設定為Universal。Organization和Company Identifier文本框中應該已經填入你在建立HelloWorld項目時填寫的内容。如果需要的話可以修改這些内容,然後單擊Next按鈕。
(4) 儲存項目,Xcode就會在新視窗中打開這個項目。如果已經打開一個項目,那麼在Save面闆的底部可能會有Add To(添加到)選項。如果是這樣的話,確定選中的是類似“Don’t Add to Any Project or Workspace”(不要添加到任何項目或工作區)的選項。
注意:
這個示例應用程式中的代碼——以及本章中其他示例的代碼——都可以在本書的示例代碼中找到。參見前言,了解從GitHub 下載下傳本書示例代碼的詳細方法。
與HelloWrold 完全一樣, CarValet 應用程式已經帶有Xcode 模闆提供的兩個類:
AppDelegate和ViewController。現在增加Car類以表示簡單的汽車:
(1) 首先,右擊Navigation視窗中的CarValet應用程式檔案夾,并選擇New File。也可以在菜單中選擇File | New | File或按下Cmd+N快捷鍵。
(2) 在新檔案對話框中,選擇iOS下的Cocoa Touch,然後選擇Objective-C class,如圖2-9所示,然後單擊Next按鈕。
圖2-9 在Xcode 中将Car 類的檔案類型設定為Objective-C (3) 在接下來的面闆中,在Class文本框中輸入Car,在Subclass of文本框中輸入NSObject,如圖2-10所示。正在建立的是Car類,繼承自NSObject。單擊Next按鈕後會出現儲存面闆。
(4) 儲存面闆的底部有一塊區域可用于指定Target成員。此時,重要的是確定CarValet被選中,如圖2-11所示。在确認該複選框已被選中後,單擊Create,Xcode會建立Car類,并且将它放到你的項目中。
圖2-10 設定類名為Car 并繼承自NSObject
圖2-11 在Xcode 中設定Car 類的Target 在Car類建立後,Xcode會自動打開Car.m實作檔案。此時,你需要切換到頭檔案中,并将 Car類的執行個體變量添加進去。編輯Car.h頭檔案,使它與代碼清單2-3保持一緻。
代碼清單2-3 Car.h 頭檔案
// Car.h
// CarValet
#import <Foundation/Foundation.h> // 1
@interface Car : NSObject { // 2
int _year; // 3
NSString *_make; // 4
NSString *_model; // 5
float _fuelAmount; // 6
}
- (id)initWithMake:(NSString *)make // 7
model:(NSString *)model
year:(int)year
fuelAmount:(float)fuelAmount;
- (void)printCarInfo; // 8
- (float)fuelAmount; // 9
- (void)setFuelAmount:(float)fuelAmount;
- (int)year; // 10
- (NSString*)make;
- (NSString*)model;
@end
前兩行是注釋。在Objective-C中,編譯器會忽略雙斜杠(//)後邊的文字。雙斜杠可用于單 行或行内注釋。可以使用斜杠和星号的組合來包圍注釋塊——也就是多行注釋:
// this is a one line comment
// and so is this, even though it follows the last one
[MyObject doSomething]; // and this is an end of line comment
/*
And finally a lot of comments started by a forward-slash and asterisk
that can include lots of lines and ends with an asterisk then forward-slash.
*/
第一個非注釋行導入了Foundation架構,這個iOS和Mac OS家中的耕田老牛。在Foundation架構中,可以找到各種東西,從數組和日期到謂詞,從URL網絡連接配接到JSON處理,還有最最重要的對象NSObject。接下來是Car類的@interface聲明。通過:NSObject标記,可以了解到Car類繼承自NSObject類。
@interface和@end之間的語句對Car類進行了定義。在@interface聲明的花括号中,可以看到4個執行個體變量,用于儲存汽車對象所需要的資訊。這些方法定義了如何給汽車對象發送消息。
下面描述了代碼清單2-3中帶數字注釋的代碼行中所發生的事情:
(1) 導入Foundation架構。
(2) 定義Car對象(NSObject的子類)的接口。
(3) _year是汽車的生産年份,存為一個整體對象,這是一種非對象的原始類型。
(4) _make是汽車的品牌,存為一個NSString對象。
(5) _model是汽車的型号,存為另一個NSString對象。
(6) _fuel是汽車油箱裡的燃料,存為浮點值。
(7) initWithMake:model:year:fuelAmount:方法初始化新配置設定的對象,并設定汽車的品牌、型号和年份,以及油箱中的燃料。這就是前面所說的自定義init方法。
(8) printCarInfo方法向調試控制台列印出汽車的資訊。
(9) fuelAmount和setFuelAmount這一對方法,為_fuelAmount執行個體變量的getter和setter方法。
(10) 剩下的三個方法是其他私有執行個體變量的getter方法。
initWithMake:model:year:fuelAmount:方法是關于方法名稱和參數如何交替放置的清晰示例。這4個參數分别接收NSString *、NSString *、int和float類型的值。注意這兩個方法前面的連字元,它表明這些方法由對象執行個體進行實作。例如,要調用[myCar printCarInfo]而不是[CarprintCarInfo]。後者會将消息發送到Car類而不是實際的Car對象。你會在本書後面看到類方法和執行個體方法的對比差別(類方法由“+”而不是“-”表示),不過,更加完整的讨論超出了本書範圍。
方法調用可以很長。例如,下面的方法調用初始化iOS的UIAlert對象:
initWithTitle:message:delegate:cancelButtonTitle:otherButtonTitles:
每個參數表示在警告框中顯示或可選顯示的元素,包括标題、消息、Cancel按鈕文字以 及其他按鈕的文字。另一個參數表示delegate(委托對象)。
使用委托對象是iOS中的另一常見模式,它提供了一種方法,通過讓兩個對象使用定義好的一組消息(稱為protocal(協定)來進行通信。這些消息是協定提供者和委托對象之間的一種契約。本質上,委托對象承諾實作這些消息,而提供者承諾正确地使用它們。協定與類是分開定義的;它們是不同的事物。任何類可以選擇以委托對象或實作者的身份使用協定。UIAlert采用這種模式以通知委托對象,使用者單擊了哪個按鈕。當閱讀這本書時,你會看到系統對象中更加複雜地使用委托模式的示例。你還會在自己建構的一些對象裡實作這種模式——也就是建立協定并向對象添加委托對象代碼。
提示:在頭檔案和實作檔案之間切換
在Xcode 中,組合鍵Ctrl+Cmd+向上箭頭可以讓你移到下一個配對檔案,而Ctrl+Cmd+向下箭頭可以讓你移到前一個配對檔案。這對組合鍵使得在頭檔案和實作檔案之間切換變得非常簡單。
2.3.1 實作Car 方法
頭檔案和實作檔案共同存儲了一個類的實作以及在應用程式其他部分使用該類所需的所有資訊。實作的源代碼通常包含在.m檔案中(m可以代表implementation或method)。顧名思義,類的方法檔案提供的是方法的實作,以及這個類如何執行它的功能。
在更大的類中,除了會在.h檔案中定義方法外,你可能會實作其他的得到支援的非公有方法。不像公有方法,不需要在定義私有方法前聲明它們。編譯器足夠聰明,甚至在使用這些方法的代碼的後邊才實作這些方法的情況下,編譯器也可以識别私有方法在哪裡。在通讀本書的過程中,你會看到關于這一點的更多内容。
此外,可以聲明僅對這個對象可見的局部執行個體變量。可以在@implement語句下面的花括号中實作這一點。例如,可以添加局部的isAL emon标記變量:
@implementation Car {
BOOL isALemon;
}
對于大多數含有執行個體變量的對象,典型地,實作的第一個方法會是init。下面通過複制代 碼清單2-4中的粗體代碼到Car.m實作檔案中,可以建立這個方法的第一個版本。
代碼清單2-4 Car.m 實作檔案
// Car.m
// CarVale
#import "Car.h"
@implementation Car
- (id)init {
self = [super init]; // 1
if(self != nil) { // 2
_year = 1900; // 3
_fuelAmount = 0.0f; // 4
}
return self; // 5
}
下面描述了在代碼清單2-4中,帶數字注釋的代碼行中所發生的事情:
(1) 第一個任務是在超類(NSObject)上調用init方法。這保證了NSObject要求的任何初始 化邏輯,在特定于Car類的初始化邏輯之前執行完畢。
(2) 檢查一下,確定self實際已經初始化。如果這樣的話,這個對象的剩餘部分将被建立。
(3) _year執行個體變量預設被設定為1900。
(4) _fuelAmount預設被設定為0.0f。盡管不是嚴格必要的,但在數字末尾包含f可以告訴 編譯器這是float值,而不是其他類型的浮點值。
(5) self的值被傳回。注意傳回的内容依賴于第2步的檢查。如果超類傳回nil,此處傳回 nil,否則會傳回現已初始化的Car對象。 到此為止,Car對象已經被初始化,但仍然不能響應以initWithMake:開頭的自定義初始 化方法和printCarInfo方法,或者任何其他方法調用。如果試圖調用那些方法,你将遇到運作 時應用程式崩潰,同時會在Debug區域的Console框中顯示一條消息“unrecognized selector sent to instance”。
向nil 發送一條消息,并發送一條未識别的消息(選擇器)
在Objective-C 中,發送消息和執行選擇器這兩個術語在本質上是同一個東西:在對象上調用方法。盡管向nil 發送任何消息都是完全安全的,但向一個未實作這條消息的對象發送一條消息會導緻運作時應用程式崩潰。這是Objective-C 初學者常犯的錯誤,并且正如你将在本書後邊看到的,存在一些方法,能夠在發送消息之前檢查對象或類是否能響應這條消息(或這個選擇器)。此外還要
注意,消息可以在繼承樹的任何地方實作。也就是說,這條消息可以在特定的類中定義,也可以在這個對象繼承的任何類中定義。
通過添加代碼清單2-5的内容,增加下列兩個方法:自定義初始化方法以及printCarInfo方法。
代碼清單2-5 Car.m 實作initWithMake:model:year:fuelAmount:和printCarInfo 方法
- (id)initWithMake:(NSString *)make // 1
model:(NSString *)model
year:(int)year
fuelAmount:(float)fuelAmount {
self = [super init]; // 2
if(self != nil) { // 3
_make = [make copy]; // 4
_model = [model copy];
_year = year;
_fuelAmount = fuelAmount;
}
return self; // 5
}
- (void)printCarInfo {
if(!_make) return; // 6
if(!_model) return;
NSLog(@"Car Make: %@", _make); // 7
NSLog(@"Car Model: %@", _model);
NSLog(@"Car Year: %d", _year);
NSLog(@"Number of Gallons in Tank: %0.2f", _fuelAmount);
}
下面描述了在代碼清單2-5中,帶數字注釋的代碼行中所發生的事情:
(1) initWithMake:model:year:fuelAmount:配置設定新的對象,然後将每個值傳入Car對象的屬 性中。
(2) 首先調用超類的初始化方法。
(3) 檢查超類是否能夠初始化這個對象,并且如果成功的話,初始化對象的剩餘部分。 如果失敗的話,self的值會是nil。
(4) 現在為Car對象設定所有執行個體變量。
(5) 到此為止,self要麼是nil(如果超類初始化失敗的話),要麼是已經初始化的對象。注 意當初始化失敗時,傳回nil是正确的做法。
(6) 僅當Car定義了make和model時才會列印資訊。
(7) 用NSLog在控制台列印值。
真實的左花括号使用慣例 本書的代碼清單讓方法的左花括号({)緊随方法名。這是為了節省空間,而不是典型的 代碼慣例。按照慣例,花括号單獨占一行。可以在Xcode 自動生成的代碼中看到,如
ViewController.m。
1. 基本初始化方法
你在代碼清單2-4中做的所有事情都可以由代碼清單2-5中的自定義初始化方法來實作。 為避免重複勞動,可以将第一個方法簡化為如下代碼:
- (id)init {
return [self initWithMake:nil model:nil year:1900 fuelAmount:0.0f];
}
initWithMake:model:year:fuelAmount:允許調用者指定所有公有執行個體變量的值——也就是 說,完整地指定新的汽車對象的初始狀态。任何其他的初始化方法都可以調用這個完整的初 始化方法。這是Objective-C的另一常見模式。initWithMake:model:year:fuelAmount:被稱作基 本初始化方法(base initializer),因為它是任何其他自定義初始化方法都可以調用的最基本的一 個。你将在整本書中看到這個模式的使用。
2. 通路器
.h檔案中聲明的最後5個方法用于通路汽車對象的資訊。在這個示例中,也就是執行個體變量。在Car.m檔案的底部添加代碼清單2-6中的代碼。
代碼清單2-6 Car.m 檔案中,通路器方法的實作
- (float)fuelAmount {
return _fuelAmount; // 1
}
- (void)setFuelAmount:(float)fuelAmount{
_fuelAmount = fuelAmount; // 2
}
- (int)year { // 3
return _year;
}
- (NSString*)make {
return [_make copy];
}
- (NSString*)model {
return [_model copy];
}
(1) 傳回_fuelAmount執行個體變量的目前值。
(2) 将_fuelAmount執行個體變量的值設定為fuelAmount參數的值。
(3) 定義剩下的執行個體變量的getter方法。每個getter方法都會傳回相關的執行個體變量的值。通常,每個公有的執行個體變量都可以用getter和setter隐藏起來。變量自己可以在.m檔案中聲明,這樣就隻有它們的汽車對象可以直接通路這些變量。即使在這個簡單的類裡,這也意味着需要聲明和定義8個額外的方法,以及大量的重複代碼。幸運的是,有一種更好的方法。
2.3.2 屬性
屬性讓你定義執行個體變量,并讓編譯器建立通路器方法——也就是說,可以通路(get或set)變量或資訊的方法。編譯器還可以生成下劃線版本的變量。聲明屬性是很簡單的:
@property float fuelAmount;
這會讓編譯器建立一個執行個體變量和兩個方法:
float _fuelAmount;
- (float)fuelAmount;
- (void)setFuelAmount:(float)fuelAmount;
你可能會注意到,此處的變量和方法的定義與代碼清單2-3中的相應内容相同。
編譯器會為你生成下劃線版本的變量。任何非汽車對象都必須使用getter和setter方法。變 量和方法的實作是在編譯時被添加的。而如果需要做一些特殊的事情,那麼可以在.m檔案中, 實作特定的通路器方法。這樣,編譯器就會使用你的方法替代。
遵循以下步驟更新Car對象以使用屬性:
(1) 在編輯器中打開Car.m檔案,并移除fuelAmount、setFuelAmount、year、make和model 這些方法的實作。
(2) 打開Car.h并移除你在第1步中删除的那些方法的聲明。
(3) 修改頭檔案中定義這些執行個體變量的部分,與以下内容一樣(新代碼以粗體顯示,確定 删掉下劃線):
@interface Car : NSObject
@property int year;
@property NSString *make;
@property NSString *model;
@property float fuelAmount;
- (id)initWithMake:(NSString *)make
使用屬性可能看起來是多餘的。畢竟代碼清單2-3中的類定義代碼定義了通路器,并且下 劃線執行個體變量是私有的。那麼為什麼使用屬性?原來,除了節省空間之外,使用屬性比使用 公開聲明的方法還有更多好處,特别是封裝和點表示法。
1. 封裝
封裝允許在應用程式中的其他部分,包括任何使用對象的用戶端,隐藏實作細節。對象 的内部表示(執行個體變量)和行為(方法等)與對象向外界聲明自身的方式隔離。隻要公開聲明的細 節保持不變,就可以自由地徹底修改内部實作。屬性提供的是,以一種結構良好并且受限地 暴露對象狀态和其他資訊的方式。
然而,屬性并不限于用作公有變量。它們在類的定義中也可發揮重要作用。屬性允許向 類定義的其他部分添加時尚的前瞻性開發技術,包括延遲初始化(lazy loading)和緩存。這就 是類既可以作為屬性的用戶端,也可以作為屬性提供者的原因。
除了隐藏細節,封裝還允許在其他項目中重用相同代碼。設計良好的Car 類不限于 CarValet 應用程式,還可以在汽車收藏應用程式、零售庫存跟蹤應用程式,甚至在遊戲中使 用。随着你開發更多應用程式,仔細使用封裝可以收獲一組可以縮短開發周期的即插即用的 類。類不僅限于表示資料;它們還可以實作接口行為、自定義視圖,甚至實作伺服器通信。
2. 點表示法
點表示法允許不用方括号,就可通路對象資訊。可以使用myCar.year代替[myCar year]調 用,讀取year執行個體變量的值。 盡管這可能看起來像是直接通路year執行個體變量,但實際上并非如此。屬性總是調用方法, 而這些方法會通路對象資料。由于屬性依賴于方法将資料帶到對象外部,是以并沒有破壞對 象的封裝。
使用my.year會調用[myCar year]。通過使用屬性,編譯器會自動生成必需的通路器方法。
如果需要做一些特殊的事情,例如檢查遠端Web伺服器,就在.m檔案中定義year通路器方法, 然後編譯器就會用你寫的方法代替自動生成的方法。 由于方法隐藏,屬性簡化了代碼的顯示和布局。例如,可以通過通路屬性,設定表的單 元格文字,代碼如下:
myTableViewCell.textLabel.text = @"Hello World";
而不是以下非常笨重的代碼:
[[myTableViewCell textLabel] setText:@"Hello World"];
代碼的屬性版本更加可讀,并且最終更容易維護。對那些使用點通路結構的應用程式員 來說,記住點通路結構是在調用方法而不是周遊對象層次結構是非常重要的。 要練習使用點表示法,建議将printCarInfo的實作替換為代碼清單2-7中的代碼。
代碼清單2-7 Car.m 中更新後的printCarInfo 實作
- (void)printCarInfo {
if(self.make && self.model) { // 1
NSLog(@"Car Make: %@", self.make); // 2
NSLog(@"Car Model: %@", self.model);
NSLog(@"Car Year: %d", self.year);
NSLog(@"Number of Gallons in Tank: %0.2f", self.fuelAmount);
} else { // 3
NSLog(@"Car undefined: no make or model specified.");
}
}
代碼清單2-7中的關鍵變化如下:
(1) 修改兩處變量檢查并傳回到檢查make和model都不為nil。
(2) 使用點标記法,将每個變量的值列印到日志。
(3) 如果沒有make和model,就更新日志。
現在代碼清單2-7中的代碼更易于閱讀,更不用說,此處還在汽車對象沒有完全定義的情 況下添加了一些列印日志的代碼。而且變量是對象級的元素而不是局部變量,這樣顯得更清 晰。盡管對于這麼短的方法,這樣做似乎無關緊要,但可以想象,在需要通讀更長代碼的情 況下,這樣做的益處。
也可以對初始化方法做類似修改,盡管這是有風險的,尤其是你會使用自定義通路器。在自定義通路器中,使用點表示法可能是最最危險的——參閱下面的旁注,“為何使用下劃線:不用點,不用通路器。”為何使用下劃線:不用點,不用通路器一種常見的錯誤來源,就是在屬性的自定義通路器或可能調用自定義通路器的方法中使用點表示法。
舉一個簡單的示例:
- (void) setMake:(NSString*)newMake {
if(![newMake isEqualToString:self.make) {
self.make = newMake;
}
}
這段代碼是make 屬性的自定義setter。它檢查新的make 值是否與舊的make 值相同, 如果不同就,将汽車對象的make 屬性設為新值。但是此處有一些隐藏的問題。
self.make = newMake 可以解釋為:
[self setMake:newMake];
結果即為對相同setter 方法的遞歸調用,這個調用又調用同樣的setter,一直這樣。這 是一個無限循環——當然,更準确說,在應用程式崩潰之前是無限循環。
正确的做法是,在setter 方法中使用下劃線版本的變量。此處指派變為:
_make = newMake;
是以,安全的做法是,讓setter 和getter 使用它們設定或傳回的iVar(執行個體變量)的下劃 線版本。任何init 方法或自定義初始化方法可以,也應該使用下劃線版本。
2.3.3 建立并列印Car 對象
目前在CarValet應用程式中,你已擁有Car類,但它并未在應用程式中的任何地方被調用 或使用。打開ViewController.m實作檔案,并遵循以下步驟:
(1) 在ViewController.m檔案的頂部,在最後一條import語句的下方位置,添加#import "Car.h"語句。
(2) 在viewDidLoad方法的下方增加代碼清單2-8中的viewWillAppear:方法。
代碼清單2-8 ViewController.m 檔案中的viewWillAppear:方法
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
Car *myCar = [[Car alloc] init]; // 1
[myCar printCarInfo]; // 2
myCar.make = @"Ford"; // 3
myCar.model = @"Escape";
myCar.year = 2014;
myCar.fuelAmount = 10.0f;
[myCar printCarInfo]; // 4
Car *otherCar = [[Car alloc] initWithMake:@"Honda" // 5
model:@"Accord"
year:2010
fuelAmount:12.5f];
[otherCar printCarInfo]; // 6
}
方法viewWillAppear:會在ViewController的視圖每次即将在螢幕上顯示時被調用。此處是 建立和調用Car對象的合适地點。下面描述了在代碼清單2-8中,帶數字注釋的代碼行中所發 生的事情:
(1) myCar被配置設定,并且被初始化為Car類的一個執行個體。
(2) printCarInfo方法被調用,但因為make和model是nil(未初始化),是以會列印汽車對象 未定義的消息。如果傳回看看代碼清單2-7,在printCarInfo方法中,你會看到檢查make和model 不為nil的if語句以及當它們為nil時的結果消息。
(3) make、model、year和fuelAmount被一一設定。NSString值的雙引号前都有個@字首, 浮點值的末尾有個f。
(4) printCarInfo第二次被調用,這一次make和model已設定,是以有關福特汽車的資訊會 被列印。
(5) 一個新的汽車對象被建立,并使用自定義初始化方法設定值。
(6) 不像第(2)步中在簡單init後調用printCarInfo的情形,這次調用會列印出本田汽車的信 息,因為make和model都已被定義。
當運作這段代碼時,會在控制台看到如下内容:
2013-07-02 08:35:44.267 CarValet[3820:a0b] Car undefined: no make or model specified.
2013-07-02 08:35:44.269 CarValet[3820:a0b] Car Make: Ford
2013-07-02 08:35:44.269 CarValet[3820:a0b] Car Model: Escape
2013-07-02 08:35:44.270 CarValet[3820:a0b] Car Year: 2014
2013-07-02 08:35:44.270 CarValet[3820:a0b] Number of Gallons in Tank: 10.00
2013-07-02 08:35:44.270 CarValet[3820:a0b] Car Make: Honda
2013-07-02 08:35:44.271 CarValet[3820:a0b] Car Model: Accord
2013-07-02 08:35:44.271 CarValet[3820:a0b] Car Year: 2010
2013-07-02 08:35:44.272 CarValet[3820:a0b] Number of Gallons in Tank: 12.50
2.4 屬性:另外兩個特性
目前版本的CarValet應用程式是下一章需要的。本節内容涵蓋CarValet項目的兩種不同的變化形式。在本章的示例代碼中也包含了這兩種變化形式。
讀者中有人可能會懷疑,在汽車對象建立後修改make、model和year是否是個好主意。到目前為止,你已經按照屬性預設的讀寫配置使用它們。然而,将屬性設定為隻讀是很容易的。所有需要做的就是在聲明屬性時增加一點額外内容。屬性聲明的一般形式如下:
@property <(qualifier1, qualifier2, ...)> <type> <property_name>;
你已經使用了type(類型)和property_name(屬性名稱)。汽車對象的make類型為NSString *, 屬性名稱為make。其中,限定符能讓你修改很多東西,包括将屬性設定為隻讀,并為記憶體管 理指定不同級别的對象所有權,甚至修改預設getter和setter方法的名稱。是以,如果想要不同 的行為,隻需要修改預設值即可。
故而,建立隻讀屬性非常簡單,隻需要包含readonly限定符。想将make、model和year設 置為隻讀,隻需要修改.h檔案中的定義:
@property (readonly) int year;
@property (readonly) NSString *make;
@property (readonly) NSString *model;
修改成功之後,試着建構這個項目,你會在ViewController.m中得到三個錯誤,提示你在 嘗試設定隻讀屬性的值。現在,删除或注釋掉ViewController中設定隻讀值的這些代碼。
但是,假如你想實作這些屬性在外部隻讀,而在内部可以讀寫。也很簡單,因為可以在.m 檔案中重新聲明屬性。可以通過在Car.m檔案的@implementation語句之前增加如下代碼,添 加或覆寫類的接口定義:
@interface Car()
@property (readwrite) int year;
@property NSString *make;
@property NSString *model;
@end
Car對象使用新的執行個體變量定義。注意明确指定readwrite并不是必需的,因為readwrite是 預設值。
要看到這些代碼能運作,可以遵循以下步驟添加一個更新make屬性的方法:
(1) 打開Car.h檔案,并在printCarInfo:的下方添加如下方法:
- (void)shoutMake;
(2) 打開Car.m檔案并剛好在printCarInfo:的下方添加如下方法:
- (void)shoutMake {
self.make = [self.make uppercaseString];
}
(3) 打開ViewController.m并且移除建立和列印第一個myCar對象的方法調用。
(4) 在viewWillAppear:的末尾增加對shoutMake的調用以及對printCarInfo的調用。最終結 果看起來如下(新代碼以粗體顯示):
...
qfuelAmount:12.5f];
[otherCar printCarInfo];
[otherCar shoutMake];
[otherCar printCarInfo];
}
當運作代碼時,最後一次對printCarInfo的調用會以全大寫字母顯示汽車品牌。可以通過 在viewAppear:中設定myCar的make屬性,讓自己確定一切工作都按預期運作。然而,你将得 到錯誤提示:你正在試圖設定隻讀變量的值。
自定義getter 和setter
在另一版本中,有時你并不想使用編譯器生成的預設getter和setter方法。例如,假設你想 讓self.fuelAmount傳回值為公升而不是加侖,但僅當新屬性showLiters值為YES時才會如此。
因而,你還會想要通過使用isShowLiters,以通路這個新屬性。
添加這個屬性隻需要一行代碼。在已有屬性的下邊添加以下代碼:
@property (getter = isShowingLiters) BOOL showLiters;
這個限定符為該getter方法設定了一個不同的名稱。你沒有使用aCar.showLiters檢查這個 變量的值,而是使用aCar.isShowingLiters—— 一個更具描述性的名稱。設定這個值仍然使用
aCar.showLiters:
if(aCar.isShowingLiters) {
aCar.showLiters = NO;
}
類似的,可以按如下方式修改setter方法的名稱:
@property (setter = setTheFuelAmountTo:) float fuelAmount;
然而,自定義的setter與getter方法表現得有點不同。需要發送一條消息以調用自定義setter 方法。下列語句能執行:
[aCar setTheFuelAmountTo:20.0f];
但是這條語句不能執行:
aCar.setTheFuelAmountTo = 20.f;
原子(atomic)和非原子(nonatomic)
新屬性定義後,是時候覆寫fuelAmount的getter方法了。在Car.m檔案末尾緊跟printCarInfo:
之後添加下面這些代碼:
- (float)fuelAmount {
if(self.isShowingLiters) {
return (_fuelAmount * 3.7854) ;
}
return _fuelAmount;
}
如果isShowingLiters是true,那麼這個自定義getter方法傳回的是公升而不是加侖(當然, 這個公式用的是美國加侖而不是英國加侖,但是有關計量标準的世界範圍統一的知識,超出 了本書範圍)。注意此次必須使用下劃線版本的執行個體變量,以避免無限循環參見2.3.2節的旁注 為什麼使用下劃線:不用點,不用通路器”。
在添加這個方法之後,你會注意到一個黃色的警告三角形,内容是“writable atomic property…”。這是什麼?在此處,這個警告是完全正确的。另一個屬性限定符是變量的原子 性。也就是說,是否在任何時刻僅有一個對象能通路這個變量(原子或同步通路),還是多個 對象可以同時在多個線程中通路這個對象(非原子或非同步通路)?
當在多線程環境中進行開發時,可以使用atomic屬性,確定指派按照預期執行。當将一 個對象設定為atomic時,編譯器會在它被通路或修改之前,添加自動為對象加鎖的代碼,并 且在之後添加解鎖代碼。這就確定了不管是否有并發線程,設定或讀取對象的值都能夠被完 整地執行。然而,設定原子性(atomic)的成本高得離譜,并且有無限等待解鎖的危險。
所有屬性預設都是原子的,但是可以使用nonatomic屬性限定符,這是通常更加安全且性 能更好的替代者:
@property (nonatomic) NSString *make;
将屬性設定為nonatomic并不能加速通路,但可能在兩個互相競争的線程試着同時修改同 一屬性時遇到問題。原子屬性,用加鎖/解鎖行為,可確定對象從開始到結束得到完整更新, 之後才會執行後續的讀取或變更行為,但是原子屬性應當僅在需要時才使用。 有人提出,通路器通常并不是加鎖的合适地點,并不能確定線程安全。即使所有屬性都 是原子的,對象也可能會被設定為無效狀态。
在實踐中,大多數屬性會被标記為nonatomic,處理可能的線程安全通路的問題會用到其 他機制。更深入的讨論可參見Learning Objective C 2.0,第2版,Robert Clair著。
修改Car.h中的fuelAmount屬性聲明,糾正錯誤(當修改時,可以順手為所有屬性增加 nonatomic限定符,包括showLiters):
@property (nonatomic) float fuelAmount;
現在需要測試上述修改是否正确。又一次,隻需要在ViewController.m中viewWillAppear: 的末尾添加幾行代碼即可:
otherCar.showLiters = YES;
[otherCar printCarInfo];
當運作這段代碼時,你會看到一輛本田車,它的油箱有12.50加侖,然後再次列印為47.32 加侖。加侖!? 但是,這是本章末尾挑戰題3的内容。
2.5 子類化和繼承:挑戰一下
随着汽車廠商持續改進使用燃料的方式,客戶要求你能夠表示一輛混合動力汽車。你的 任務是建立繼承自Car類的HibridCar類,并且添加一個方法,以傳回直到汽車的電池電量和燃 料耗盡汽車所跑的裡程數。可以将這個方法命名為-(float)milesUntilEmpty。
線索:
不要忘記跟蹤每加侖英裡數(Miles Per Gallon,MPG),這樣可以計算汽車失去動力前的 距離。例如,2013 款豐田普銳斯混合動力汽車可以達到42MPG。如果油箱裡剩下10 加侖, 那麼理論上這輛車在油箱耗盡之前可以行駛402 英裡。
思考幾分鐘。
想到解決方法了嗎?
閱讀下面的内容,看看如何做。
繼承和子類化
在Objective-C中,每個新類都派生自已有的類。代碼清單2-3到2-7中描述的Car類繼承自 NSObject——Objective-C類樹的根類。每個子類添加或修改從父類(也稱為超類)繼承來的狀态 和行為。Car類向它所繼承的NSObject類中添加了一些執行個體變量和方法。
HibridCar類繼承自Car類并增加了一些功能,依據混合動力汽車能夠達到的MPG,計算 汽車在燃料耗盡前能夠行駛的距離。代碼清單2-9和2-10展示了實作HibridCar類的一種可能方 法。本章的示例代碼在檔案夾“CarValet HybridCar”中包含代碼清單2-9到2-11的項目。
代碼清單2-9 HybridCar.h 頭檔案
// HybridCar.h
// CarValet
#import "Car.h"
@interface HybridCar : Car
@property (nonatomic) float milesPerGallon;
- (float)milesUntilEmpty;
- (id)initWithMake:(NSString *)make
model:(NSString *)model
year:(int)year
fuelAmount :(float)fuelAmount
MPG:(float)MPG;
@end
首先,注意.h檔案有多小。這個檔案所需要做的一切,就是指明Car和HybridCar的不同之 處——此處,是一個屬性和兩個方法。這個屬性存儲了這輛混合動力汽車所能達到的每加侖 英裡數。milesUntilEmpty傳回這輛汽車使用油箱目前含量(fuelAmount)可以行駛的公裡數,自 定義初始化方法增加了一個MPG參數以設定milesPerGallon。
代碼清單2-10顯示了HybridCar類可能的實作檔案。
代碼清單2-10 HybridCar.m 實作檔案
// HybridCar.m
// CarValet
#import "HybridCar.h"
@implementation HybridCar
- (id)init
{
self = [super init] ;
if (self != nil) {
_milesPerGallon = 0.0f;
}
return self;
}
- (id)initWithMake:(NSString *)make
model:(NSString *)model
year:(int)year
fuelAmount:(float)fuelAmount
MPG:(float)MPG {
self = [super initWithMake:make model:model year:year fuelAmount:fuelAmount];
if(self != nil) {
_milesPerGallon = MPG;
}
return self;
}
- (void)printCarInfo {
[super printCarInfo];
NSLog(@"Miles Per Gallon: %0.2f", self.milesPerGallon);
if(self.milesPerGallon > 0.0f) {
NSLog(@"Miles until empty: %0.2f",
[self milesUntilEmpty]);
}
}
- (float)milesUntilEmpty {
return (self.fuelAmount * self.milesPerGallon);
}
@end
當子類和超類都包含基本初始化方法時,在子類中實作init方法至少有兩種主要方法。一 種是使用一些預設值以調用子類的基本初始化方法。方法主體看上去會是這樣:
return [self initWithMake:nil model:nil year:1900 fuelAmount:0.0f MPG:0.0f];
然而,這樣做會産生隐藏的bug和/或維護成本。假設Car有若幹子類,可能有混合動力型、電動型以及柴油型。甚至可能有子類的子類,如GasElectricHybrid、DieselElectricHybrid等。
将生産年份的預設值設定為0,進而能夠很容易地檢測到是否存在忘記設定的值。如果每個子類的init方法都使用相應類的自定義初始化方法,那就不得不修改每個子類中的值。忘記值的修改就會引入bug。然而,如果init方法使用[super init],然後設定特定于子類的預設值,那麼隻需要在一個地方進行修改即可。
此處有個不錯的示例,使用的initWithMake:model:year:fuelAmount:MPG:是子類自定義初始化方法繼承超類方法,并增加額外功能——具體地設定了milesPerGallon。首先,調用超類的initWithMake:model:year:fuelAmount:方法,初始化Car對象的屬性,然後初始化HybridCar對象具體的值。
由于為Car類增加了新的屬性,HybridCar具體化了printCarInfo方法。第一件事就是調用超類Car裡的相同方法。然後,特定于混合動力汽車的資訊被列印出來。具體化(specialization)是繼承中很強大的一部分,允許每個類隻做它需要做的事情。當具體化與封裝結合時,可以讓一個類的開發者隻聚焦于那個類,利用繼承鍊上方的公有方法和屬性以加速開發過程。milesUntilEmpty方法用于計算在油箱耗盡前這輛汽車還能再跑多少英裡。它使用一個簡單的公式,将MPG乘以油箱中燃料的加侖數。在真實的混合動力汽車中,算法将很可能複雜得多。
最後一步是往CarValet應用程式的ViewController中增加一個HybridCar類的執行個體。你需要在ViewController.m檔案的頂部添加#import "HybridCar.h"語句,然後将代碼清單2-11中的内容添加到viewWillAppear:方法中。
代碼清單2-11 添加一輛混合動力汽車到ViewController.m 檔案中
HybridCar *myHybrid = [[HybridCar alloc] initWithMake:@"Toyota"
model:@"Prius"
year:2012
fuelAmount:8.3f
MPG:42.0f];
[myHybrid printCarInfo];
myHybrid執行個體被建立并且用make、model、year和MPG設定了執行個體變量。混合動力汽車 的資訊被列印,然後NSLog被調用,顯示燃料耗盡之前汽車還可以行駛多少英裡。如果運作 CarValet應用程式,就将在調試控制台看到如下資訊:
2013-07-03 08:39:45.458 CarValet[9186:a0b] Car Make: Toyota
2013-07-03 08:39:45.458 CarValet[9186:a0b] Car Model: Prius
2013-07-03 08:39:45.459 CarValet[9186:a0b] Car Year: 2012
2013-07-03 08:39:45.459 CarValet[9186:a0b] Number of Gallons in Tank: 8.30
2013-07-03 08:44:39.419 CarValet[9346:a0b] Miles Per Gallon: 42.00
2013-07-03 08:44:39.419 CarValet[9346:a0b] Miles until empty: 348.60
HybridCar類可以用多種不同的方式進行定義和實作。花點時間建立一些變化形式。關鍵 是要開始适應Objective-C的文法。可以通過完成本章結尾的挑戰題,進行更多練習。當繼續 閱讀本書,并且繼續編寫自己的應用程式時,Objective-C的文法和模式會變成你的第二天性。
2.6 小結
本章提供無删節的、大資訊量的對Xcode、Objective-C文法、對象、類、屬性和繼承的介紹,此外還讓你使用Xcode練習建立項目,以及Objective-C概念。
本章還是在你通讀本書時,可以傳回檢視的一章。要想獲得最大的價值,應該試着直接在Xcode中試驗本章中讨論的所有内容。花時間擺弄示例應用程式,親自動手獲得的經驗是擷取iOS開發關鍵技能的最好方法。
學習Objective-C需要的不僅僅是一章。如果要認真學習iOS程式設計,并且這些概念對你來說還很生疏,那麼請考慮搜尋專門為這個平台的新手介紹這些技術的單一主題書籍。考慮Learning Objective-C 2.0:A Hands-on Guide to Objectiv-C for Mac and iOS Developers,第2版,Robert Clair著;或者Programming in Objective-C,第5版,Stephen G. Kochan著。對于Xcode,查找Xcode 4 Unleashed,第2版,Fritz F. Anderson著;或者Xcode 4 Developer Reference,Richard Went著,Richard Went為Xcode 5提供了更新的版本(盡管Xcode 4版本也是有用的)。蘋果公司也提供非常好的 Objective-C 2.0 簡介, 位于http ://developer.apple.com/Mac/library/do cumentation/Cocoa/Conceptual/ObjectiveC/Introduction/introObjectiveC.html。
在本章,你建立了CarValet應用程式。在下一章,你将在這個項目的基礎上建構并學習故事闆,這是一種圖形化地建立應用程式中所有螢幕的方式,并且将它們組裝在一起。你還将學習更多關于Objective-C和iOS程式設計中一些重要技術的知識。
2.7 挑戰題
1. 更新printCarInfo方法,當隻有make是nil時列印“Car undefined:no make specified”,當隻有model為nil列印“Car undefined:no model specified”。如果兩者都為nil,那麼仍然列印“Car undefined:no make or model specified”。可以通過調用initWithMake:model:year:fuelAmount:的變種,建立汽車測試對象以檢查代碼。
2. 建立Car類的子類ElectricCar。花點時間設計該類:執行個體變量,對已有方法的修改,以及任何獨有的方法。當做完這些後,要麼開始實作,要麼繼續閱讀以了解一些可能的方式。設計ElectricCar類有若幹方式。一部分選項取決于你想從Car類繼承多少東西。在描述汽車的執行個體變量中,隻有fuel可能是個問題。電動汽車使用充電器。可以重用fuel并且假裝它就是charge(充電器),那麼你需要做的僅僅是修改printCarInfo并且增加一個方法,用于列印剩餘電量。還可以增加一個執行個體變量用于表示每千瓦時的行駛距離,并且使用那個值計算這輛汽車剩餘的可行駛裡程。
3. 在2.4節的“自定義getter和setter”部分,可以看到如何根據fuelAmount傳回美國加侖或公升數。但是printCarInfo總是列印加侖數。修改printCarInfo,使得在isShowingLiters為NO時列印加侖數,為YES時列印公升數。當printCarInfo使用公升時,修改Car類,使得可以用英國加侖、美國加侖和公升列印結果。你需要找到一種方法,設定燃料用哪種機關進行顯示。如果使用BOOL類型,注意有可能多個BOOL變量同時被設定為YES。
《iOS開發完全上手——使用iOS 7和Xcode 5開發移動與平闆應用》試讀電子書免費提供,有需要的留下郵箱,一有空即發送給大家。 别忘啦頂哦!
微信:qinghuashuyou 更多最新圖書請點選檢視哦 互動出版網china-pub購書位址:http://product.china-pub.com/3770438