Io 程式設計/初學者指南/物件
Io 是一種動態型別的、動態分派的、面向物件的程式語言,非常類似於 Self 和 Smalltalk。到目前為止,我們主要處理的是語言中最原始的東西。
然而,現在是讀者學習物件及其在 Io 環境中的使用方法的時候了。
物件,在計算機程式設計的語境中,是概念或有形事物的抽象,擬人化地說,你可以告訴它做什麼。例如,這條語句
Io> writeln("Hello world!")
可以看作是告訴計算機在螢幕上寫一行。面向物件的語言使我們能夠接受這個概念並將其提升到對我們需求更有用的水平。如果你回顧你的第一個 Io 程式,其中有一行是這樣的
you := File standardInput readLine
該表示式按以下順序求值
- 首先,File 被求值。我們告訴計算機給我們一個 File,無論它是什麼(我們現在不必關心它)。
- 接下來,我們告訴File給我們標準輸入。
- 接下來,我們告訴那個東西讀取一行文字。
- 最後,我們將其分配給你。
如果我們讓計算機本身給我們一個標準輸入物件,會發生什麼?
Io> standardInput
·
Exception: Object does not respond to 'standardInput' --------- Object standardInput Command Line 1
我們看到的是一個錯誤訊息。它由幾個部分組成,你應該熟悉它們。當你開始時,你會看到很多這樣的東西。
- 第一行(Exception: Object does not...)告訴我們Object沒有響應 standardInput 訊息。
- 最下面部分是一個回溯,它告訴我們錯誤發生在哪裡。這在試圖找出哪裡出了問題時非常寶貴。
到目前為止,我們已經看到了如何使用物件將一些名稱空間劃分為有用的庫。但是,我們還沒有看到任何東西;物件不僅僅是庫的別稱。
為了看到如何使用物件使我們的程式更模組化,瞭解“它們如何工作”是有用的。
可以將物件視為這個東西,它可以執行命令。它可以執行哪些命令由其槽位中出現的內容決定。
例如,讓我們自己建立一個小物件。面向物件的等效於臭名昭著的hello world程式莫過於
Io> Account := Object clone Io> Account balance := 0 Io> Account withdraw := method(amount, self balance = self balance - amount) Io> Account deposit := method(amount, self balance = self balance + amount)
為了簡潔起見,我在這裡省略了 Io 直譯器的輸出。但我們剛剛做了
- 建立了一個Account 物件。
- 配置物件使之具有零的餘額。
- 教 Account 物件如何提取東西(大概是現金)。
- 教 Account 物件如何存入東西。
我們可以很容易地查詢此物件的當前賬戶餘額
Io> Account balance ==> 0
這是因為執行Account足以告訴直譯器,“好的,我現在執行在Account這個東西的上下文中”。下一條指令是balance。直譯器看到Account顯然實現了balance,因此執行它。在本例中,它的值為零。因此,==> 0 報告。
另一種看待它的方式,實際上也是首選方式,是思考,“嘿!Account!你在那裡!Balance,現在!”。換句話說,我們告訴Account物件給我們餘額。如你所見,這與File standardInput readLine並沒有太大區別——後者只是命令更多,但機制完全相同。
接下來,讓我們在我們的賬戶中存入一些現金。
Io> Account deposit(100) ==> 100
這產生了意外的結果——為什麼它返回了 100,而不是nil?這是因為它計算的最後一件事情是self balance + amount,當你考慮它的時候,它計算為 0+100。
仔細看看——當我們執行deposit(100)時,deposit 槽位在 Account 物件中被獲取,就像上面的balance一樣。與balance一樣,直譯器執行這個槽位。但是,它不僅僅是一個未修飾的值,它是一個方法。還記得我說過我們很快就會講到方法嗎?
- 觀察
如果你試圖告訴物件做一些事情,那麼你顯然是在向它們傳送訊息,這些物件知道如何解釋(以某種方式)這些訊息。在本例中,balance和deposit都是訊息。但是,解釋這些訊息的方法完全取決於物件本身。因此得名方法。現在我們已經解開了其中一個謎團,我們又引入了另一個——我們稍後將講到方法與過程的意義。
- 注意
從數學角度來說,deposit不是一個函式,因為它有稱為副作用的東西。這意味著評估訊息將導致一些狀態變化,這些狀態變化會持續到訊息評估結束之後。純數學函式從不這樣做。現在你知道為什麼我們也不將deposit稱為函數了!
在deposit內部執行時,我們看到我們需要用self balance來引用我們的餘額。在這裡,self顯然指的是執行方法的物件,因此它允許我們訪問我們物件的資料。
- 觀察
預設情況下,所有槽位都對最內層詞法範圍區域性。換句話說,沒有全域性變數。
- 練習
試著弄清楚Account withdraw(50)是如何工作的!
我保證,這是我們在看到物件如何真正變得有用之前最後的偏離。我們需要了解繼承是如何工作的,因為它是在 Io 中的基石。
再次看看我們上面的Account物件,物件的建立涉及克隆Object。在 Io 中,一切最終都是某種Object,但如果你正在建立一個真正新穎獨特的東西,你可以直接克隆Object。
這句話的意思是,好吧,Account是一個Object。但是,我們繼續專門化這個物件,它有餘額,以及一些調整餘額的方法。
但是,與 Io 中的所有事物一樣,這個非常簡單的例子比表面上看起來要複雜。考慮一下,如果我們總是透過傳送訊息來指揮物件,那麼self被髮送到哪裡呢?
如果你回答Object,那麼你不完全正確。還記得我說過,預設情況下,Io 中的所有東西都是區域性的嗎?方法變數也不例外。Io 查詢事物順序如下
- 首先在方法本身中查詢。沒有名為self的區域性變數。
- 接下來在Account物件中查詢。不,這裡也沒有。
- 接下來在Object中查詢。啊哈!
每個物件都實現了一系列原型,它使用這些原型作為靈感來源,如果你願意的話,來確定如何處理訊息。如果你向物件傳送一條它不知道如何處理的訊息,那麼它會諮詢它的原型以找出如何處理。如果,並且僅當它在這項任務中失敗時,你才會收到那個臭名昭著的錯誤訊息,Object does not respond to 等等。
現在我們已經瞭解了物件的本質及其如何從其他物件繼承,讓我們看看如何將它付諸實踐。
首先,假設你想要多個帳戶。有許多不同型別的帳戶,但我們將堅持使用基本儲蓄帳戶。這些帳戶隨著時間的推移會積累利息。但是,來自不同銀行的帳戶會以不同的利率積累利息。我們如何管理這種額外的複雜性?
首先,我們需要一個儲蓄帳戶
Io> SavingsAccount := Account clone
正如我們在這裡看到的,我們建立了一個新物件,它依賴 Account 作為其行為的靈感來源。我們可以驗證它沒有任何自己的方法
Io> SavingsAccount slotNames foreach(println) type ==> type
好吧,它有一個槽位type,當呼叫它時會返回SavingsAccount。但除此之外沒有其他東西。然而,我們仍然可以像使用普通帳戶物件一樣使用它
Io> SavingsAccount balance ==> 0
儲蓄帳戶通常會有一些利率
Io> SavingsAccount interestRate := 0.045
有了它,我們可以估計我們將在年底有多少
Io> SavingsAccount yearEndEstimate := method( )-> self balance * self interestRate + self balance )-> )
因此,當它執行時,搜尋順序是首先嚐試在方法本身中找到balance,然後在SavingsAccount中找到,然後在Account中找到,在那裡它實際上被定義。
假設我們需要為你的家人開設銀行帳戶。我們現在可以這樣做
Io> MomsAccount := SavingsAccount clone Io> DadsAccount := SavingsAccount clone Io> WifesAccount := SavingsAccount clone
所以,我們應該能夠獨立地跟蹤不同的帳戶
注意:本節描述的行為與當前 Io 版本的行為不符。有關詳細資訊,請參見討論頁面。
Io> MomsAccount deposit(450) Io> DadsAccount deposit(450) Io> WifesAccount balance ==> 900
哇,這是怎麼回事?看起來所有的存款都進入了一個餘額!
- 練習
你能弄清楚為什麼嗎?
在面向物件程式設計社群中,你經常會聽到這句話,“例項化一個類 x 的物件”,其中 x 是他們所指的任何類。例如,要建立一個新列表,你可能會看到關於某種 列表類 的提及。
Io 沒有類,而這正是前一節中所有關於平衡的描述都歸結為一個的原因。由於 沒有 任何派生物件實現它自己的 balance 訊息,它假設物件的原型知道如何處理它。事實證明,這種假設是錯誤的。
那麼,我們如何例項化一個物件,以便每次需要時它都會建立自己的 balance 槽位?我們使用 init 方法來實現。
Io> SavingsAccount init := method(self balance := 0)
透過這樣做,我們已經將 SavingsAccount 從任何普通的物件轉變為 一個類。我們現在可以像對待 SavingsAccount 的 型別 一樣,整天對 SavingsAccount 物件進行邏輯推理。事實上,它們正是這樣。
我們現在可以重新例項化我們家庭的賬戶。
Io> MomsAccount := SavingsAccount clone Io> DadsAccount := SavingsAccount clone Io> WifesAccount := SavingsAccount clone
這裡發生的事情是,在克隆過程結束後,會發送 SavingsAccount init 以確保物件被正確初始化。
Io> MomsAccount deposit(450) Io> DadsAccount deposit(450) Io> WifesAccount balance ==> 0
請注意,我們不需要手動實現或重新實現 withdraw 或 deposit 方法。我們對基本 Account 物件知道如何處理取款和存款的假設仍然成立。唯一錯誤的是從哪裡取款或存入。因此,透過建立一個意識到這種混淆的賬戶型別,我們可以在執行任何進一步的程式程式碼之前,有機會澄清。
應該指出,所有類都是原型——供另一個物件使用的模板。但是,並非所有原型都是類。正如我們所見,Account 也是一個原型,但嚴格來說,它不是一個類。是 SavingsAccount 及其 init 方法保留了那些讓我們可以將它視為 型別 而不是 事物 的假設。然而,如果我們願意,我們可以自由地根據 事物 來定義 型別,就像我們在本例中所做的那樣。在基於類的面嚮物件語言中,這種靈活性是不可能的。
編寫面向物件程式時,要記住的一點是,你無需第一次就做到完美。上面我說過,我們可以有多種型別的賬戶,每種賬戶都有不同的利率,但是到目前為止,軟體的演變方式並不容易實現。所以,讓我們修復這些問題。
Io> Account init := method(self balance := 0)
Io> SavingsAccount removeSlot("init")
Io> AccountWithInterest := Account clone
Io> AccountWithInterest yearEndEstimate := SavingsAccount getSlot("yearEndEstimate")
Io> SavingsAccount removeSlot("yearEndEstimate")
Io> SavingsAccount setProto(AccountWithInterest)
透過少量語句,我們剛剛重新設計了整個型別層次結構。現在,Account 是(所謂的)基類,而 SavingsAccount 現在只是 AccountWithInterest 的一種特殊型別。依賴於 SavingsAccount 的軟體應該仍然可以執行,因為我們並沒有從根本上改變 SavingsAccount 物件的 行為。
現在,我們可以定義什麼是 CheckingAccount。
Io> CheckingAccount := AccountWithInterest clone Io> CheckingAccount interestRate := 0.015
這就是人們很少使用支票賬戶來攢錢的原因。
Io> MomsChecking := CheckingAccount clone ...etc...
顯然,這是一個非常簡單的例子,但它向你展示了物件、原型和類之間關係的改變,可以在程式執行期間進行修改,甚至可以是在程式正在執行時進行修改。它們不像其他語言那樣靜態。
話雖如此,我想要明確一點,這種程式設計非常適合 探索性工作,但你不應該將它用於生產程式碼。如果你像這樣“熱修復”軟體,請確保相應地修復程式的原始碼,以便將來不再需要這種熱修復。
實現這一目標的一種方法是測試驅動開發。但是,這個過程超出了本書的範圍。在這裡,我的任務是教你如何在 Io 中編寫軟體。它並不是 如何設計軟體。如果你想了解更多關於這方面的資訊,請參閱 [1]。