我聽很多人跟我說,他們擅長C++或者Java,但是他們根本不懂Smalltalk。據他們說,Smalltalk有壹本書!我想了壹下,覺得他們說的可能很有道理。如果我只懂Java,從我寫了很多年的代碼裏選壹段我也看不懂。在理解Smalltalk之前,我們必須澄清壹些非常簡單的概念和壹些微妙而奇怪的語法概念。如果“老王不懂Smalltalk”,也許我可以改善他的處境。希望讀者能快速上手。我假設讀者理解面向對象編程。如果妳已經知道Smalltalk,請原諒我玩遊戲。
哦,詞匯的細節是如此簡單
第壹次讀Smalltalk時遇到的壹些約定俗成的習慣用法可能與其他語言大相徑庭,可能會讓妳感到困惑,比如註釋兩邊的雙引號,字符串兩邊的單引號,字符的特殊語法表達(比如$x代表“x”)。還有符號的概念,符號是內存中只有壹個實例的字符串;例如,當構造壹個符號時(通常在編譯時),首先從內存中尋找同壹個實例,如果有就直接使用。這樣做的目的不是為了節省內存,而是為了優化比較效率(詳見下文):
“這是壹條評論”
“這是壹根繩子”
# '這是壹個象征'
# thisIsASymbolToo
賦值運算符和比較運算符之間有細微的區別:
:=//賦值
=//內容相等的比較,深度比較
= =//唯壹比較,淺層比較
如果妳給我兩個不同的對象,它們被不同的變量“A”和“B”引用,我可以告訴妳它們是同壹個對象(通過a == b)還是只是看起來壹樣的不同對象(通過a = b)。說白了,= =比較兩個指針,=比較對象的全部內容。
逗號很少出現在Smalltalk中,因為它們不充當語法元素。這就是為什麽數組是直截了當的,比如那些沒有多余逗號的數組:
#(1 2 3 4 5)
然而,逗號是有意義的。它是壹個運算符。妳偶爾可以看到它用來連接兩個字符串,例如:
字符串1 ','字符串2 '
關鍵詞無處不在
Smalltalk裏到處都是關鍵詞。但是它們有利於可讀性,而不是混淆。為了找出原因,讓我們從壹個C++和Java片段開始。例如,妳可能熟悉下面的文字:
t->;旋轉(a,v);// C++
t.rotate(a,v);// Java
t對象被發送了壹個帶有參數a和v的rotate消息,想要理解這類代碼的讀者通常需要找到變量的聲明並確定其類型。我們假設該語句如下:
轉換t;
浮動a;
向量v;
變量可以引用Smalltalk中任何類型的對象。所以不需要類型描述,但是我們仍然需要聲明變量,比如:
|t a v|
不看聲明,優秀的Smalltalk程序員會根據名字判斷變量的類型。那麽讓我們換壹種說法如下:
| a變形角矢量|
但請允許我繼續使用最初的短命名,以免示例代碼太長影響閱讀。讓我們通過刪除不必要的因素來“改進”C++和Java的語法。例如,下面的代碼仍然是清晰的:
t.rotate(a,v);//原始書寫方法
t旋轉(a,v);//誰需要號碼?
t旋轉a,v;//誰需要括號?
為了進壹步提高語法,我們需要知道參數A和V分別代表什麽。我們假設整個例子的意思是“繞矢量v旋轉壹個角度a”。它進壹步改進如下:
t繞v旋轉a;//誰需要逗號?
我們能清楚每種成分是什麽嗎?沒問題,因為在我們改進的例子中,“t”是變量,“rotate”是方法名,“by”是分隔符,“a”是變量,“around”是分隔符,最後壹個“v”也是變量。為了消除潛在的歧義,我們設置了壹個規則,即分隔符後面跟壹個冒號。我們有:
t按:a左右旋轉:v;//誰需要曖昧?
最後,我們強調分隔符是方法名的壹部分;例如,假設我們需要壹個“rotate by: around:”形式的函數,去掉空格,我們將得到“rotateby: around”作為最終名稱,然後將非首字母大寫以提高可讀性,得到“rotateBy: around”。那麽我們的例子可以寫成:
這是閑聊。
方法名分為幾個部分。幸運的是,很容易將這些片段聚集成壹個完整的名稱。當在壹個類中時,我們如下定義方法名:
自旋轉角度:向量
|結果|
結果:=計算答案。
結果
運行時,“T”與“自我”,“A”與“角度”,“V”與“矢量”之間是壹壹對應的關系。請註意,“”表示結果已返回;這是Smalltalk中關鍵字“return”的寫法。“自我”這個變量就是“這個”的同意詞。如果方法末尾沒有返回語句,“self”作為隱含語句執行;當妳完成壹個方法時,妳可能會忘記添加壹個return語句,但是沒關系。這也意味著,即使消息的發送者不需要,該方法也將返回值。
事實上,慣用的Smalltalk語法要求“self”不能顯式出現在方法頭中(但必須是隱含的),例如:
旋轉比:角度:向量
|結果|
結果:=計算答案。
結果
關鍵字語法的妙處在於,我們可以為不同的方法定義不同的關鍵字。例如,我們可以將第二種方法定義如下:
t旋轉方向:矢量方向:角度
妳不必記住參數順序。關鍵字提示我們訂購。當然,程序員有能力濫用關鍵字,例如,如果我們如下定義關鍵字:
t旋轉:角度和:向量
讀者很難理解參數的正確順序。這是壹種糟糕的編程風格,如果只有壹個參數,那就好了。當只有壹個參數時,我們仍然需要方法名;例如:
t rotateAroundXBy:角度
t rotateAroundYBy:角度
我們希望關鍵字(容易用冒號區分)是參數的描述。但是如果這個方法沒有參數呢:
T makeIdentity: //結尾的冒號有意義嗎?
如果關鍵字表示參數的描述,那麽我們就不能在沒有參數的情況下使用關鍵字。所以零參數的信息應該是:
T makeIdentity //這是閑聊。
當然,二元操作符是壹樣的,但是壹元操作符(makeIdentity是壹元消息,但不是壹元操作符)就不壹樣了。當多種信息壹起出現時,我們的表情可能是這樣的:
a負數| (b介於:c和:d之間)
ifTrue: [a := c取反]
作為讀者,妳應該知道“A”被發送了壹個名為“negative”(零參數)的消息,該消息返回true或false還向“B”發送了名為“between: c and: d”的消息,該消息返回true或false。兩項或的結果壹起成為消息“ifTrue: [a := c取反]”的接收者。這是if-then控制結構的正宗寫法,不是專門的語法。它只是壹個標準的關鍵字語法,以布爾值為接收方,“ifTrue”為關鍵字,“[a := c negated]”(我們稱之為block)為參數。在Smalltalk中,妳永遠不會遇到“a := -c”,因為沒有壹元運算符,但妳會看到常數“-5”,其中“-”作為常數的壹部分。
所以如果妳看到壹個類似“-3.5否定截斷事實”的表達式,妳應該馬上意識到裏面沒有關鍵詞。所以“-3.5”壹定是被發送了“否定”的消息;執行結果3.5與“截斷的”消息壹起發送;然後執行結果3被“階乘”發送,產生最終結果6。
當然還有操作優先級(從左到右)、消息優先級(零參數最高,二進制操作次之,最後壹個關鍵字)等規則。這些在寫代碼的時候非常重要,但是讀代碼的時候就不用關心這些細節了。表達式從左到右如下:
1+2 * 3得9
沒有優先順序,但是妳很少遇到Smalltalk程序員會這樣寫表達式,因為這樣會把非Smalltalk的讀者搞糊塗。普通的Smalltalk程序員使用下面的替代寫法:
(1 + 2) * 3
即使括號是不必要的。
分號和句號是不同的。
大多數非Smalltalk程序員都把分號當作語句結束的標誌,但是在Smalltalk中,他們用句號來表達這個意思。所以我們不會寫:
賬戶存款:100美元;
收藏添加:轉換;
上面寫著:
賬戶存款:100美元。
集合添加:轉換。
嗯!“美元”的消息讓妳困惑了嗎?不要覺得不可思議。必須有壹個名為“dollars”的方法在Integer類中構造壹個“Dollar”對象並返回它。它不在Smalltalk的標準環境中,但是我們可以擴展它。需要時,基類(內置類)可以擴展為用戶定義的類。
因此,句號是壹個語句結束符,在最後壹句中是可選的(如果妳願意,妳可以把它用作語句結束符)。但是,分號仍然是合法的特殊分隔符(不是終止符)。它用於指定接收者是可收縮的。所以,把它寫成這樣:
|p|
p :=新客戶。
p名:“傑克”。
p年齡:32。
p地址:“地球”。
可以寫成:
|p|
p :=新客戶。
p
姓名:‘傑克’;
年齡:32;
地址:“地球”。
或者更好的是:
|p|
p :=新客戶
姓名:‘傑克’;
年齡:32;
地址:“地球”。
Smalltalk對排版不敏感。我們甚至可以將所有語句放在同壹行。本質上,分號指定了前壹條消息是為了修改接收方而發送的,下壹條消息應該發送給同壹個接收方(而不是發送給被忽略和丟棄的操作結果)。
在最後壹個例子中,“new”被發送到壹個類以獲得壹個實例(操作結果)。然後“姓名:‘傑克’”被發送到該實例。第壹個分號指定“姓名:'傑克'”的結果被忽略,應該將“年齡:32”發送給前壹個收件人(同壹個實例)。指定“年齡:32”的第二個分號的結果被忽略,並且“地址:‘地球’”應該被發送到前壹個接收者(仍然是同壹個實例)。最後將“address: 'Earth '”的運算結果賦給P,修改接收方的方法通常返回接收方本身,所以P被綁定到最近修改的客戶端實例。
我們可以通過用英語短語“AND ALSO”替換分號來簡化上面的賦值。也就是說,“new”被發送到類“Client”,結果實例與“name:' jack '”以及“age: 32”和“address:' earth '”消息壹起發送。向同壹收件人重復發送不同的消息在Smalltalk中稱為級聯。分號也可以出現在子表達式中,比如“p:=(Client new name:' Jack ';年齡:32;地址:‘地球’)”——註意括號。
Get/Set方法與變量實例同名。
在Smalltalk中,客戶類實例的成員變量如姓名、年齡和地址都是私有的。但是您可以通過實現某些方法來訪問它們。例如,在C++(類似Java)中,我們經常會編寫以下訪問方法(通常稱為get/set方法):
long getAge(){ return age;}
void setAge(long new age){ age = new age;}
如果將這種方法應用於大量的類,您將會編寫數百條以get和set開頭的消息。如果您無意中決定使用簡化命名(稍後編寫)來簡化這些方法,即使Java編譯器可以正確識別它們,也會給C++編譯器造成解析混亂,因為它無法區分變量和方法:
long age(){ return age;}
void age(long new age){ age = new age;}
妳能區分變量“年齡”和消息“年齡”嗎?應該區分壹下。使用messages時,需要放上括號如“age()或age(22)”;使用變量時不壹定要放括號。Smalltalk中的對應寫法是:
^age時代
年齡:新年齡
我們通常使用下面的逐行書寫來提高可讀性:
年齡
年齡
年齡:新年齡
年齡:=新年齡
在Smalltalk中,您可以很容易地將變量與消息區分開,而不需要依靠括號。如果妳清楚地知道它們之間的區別,妳可以看到下面的表達式中有多少個變量:
年齡年齡年齡:年齡年齡+年齡年齡
嗯!答案是三;第壹個和第四個年齡必須是變量(緊跟在關鍵字後面的子表達式和所有表達式都必須以變量開頭),第七個年齡也必須是變量(二元運算符後面的子表達式也必須以變量開頭)。看壹個更明顯的類似典型例子:
名稱大小//名稱必須是變量。
自身名稱// name必須是變量。
廣泛使用的收藏
Smalltalk中最常用的兩個集合是有序集合和字典。數組的概念相當於壹個大小不變的有序集。
|a b|
a :=已訂購集合新
補充:#紅色;
補充:#綠色;
妳自己。
b :=字典新
at:# red put:# rouge;
at:#果嶺推桿:# vert
妳自己。
上面每個賦值中的變量都綁定到最後壹條消息的執行結果上;比如“自己”的結果就是最後創建的集合。“yourself”消息旨在返回消息接收者(類似於無操作操作),但“add:”和“at: put:”不是(它們返回最後壹個參數)。所以如果沒有“自己”,“A”必然“#綠”,“B”必然“#綠”。
我特意用層疊式寫法來解釋為什麽“自己”會獨立出現在內置類的方法中。
Smalltalk中集合的優點是可以在其中存儲任何類型的對象。甚至字典中的鍵也可以是任何類型;同壹集合中的對象也可以是不同的類型。我們不必為了在壹個實例中收集壹批新類型而重新發明新的集合類型。
您可以像訪問數組壹樣訪問有序集合;例如,“a at: 1”索引到元素1。字典也可以用同樣的方式訪問;比如“at: # red”。但是在很多應用中,我們不必關心鍵。這樣,很容易叠代元素:
壹個do: [:item |
用物品做某事]。
b do: [:item |
用物品做某事]。
即使集合中的元素屬於不同的類型,“item”變量也會逐個獲取每個元素。如果需要,我們可以在運行時知道壹個對象是什麽,可以寫成“item isKindOf: Boat”,它返回true或false。同時,還有許多特殊類型的查詢消息,如“item isCollection”或“item isNumber”。此外,還有許多用於創建新集合的循環構造消息,例如:
c:= clients select:[:client | client netWorth & gt;500000].
d :=客戶端收集:[:客戶端|客戶端名稱]。
在前壹個例子中,我們得到了壹批大客戶。在後者中,我們得到壹個客戶名稱的集合(原始集合是客戶的集合)。
有序抽象不需要構造新的類。
讀者經常會看到下面的代碼:
填充值:x值:y值:z
這裏的關鍵詞都是“價值:”。對於壹個非Smalltalk程序員來說,這種寫法毫無意義,令人困惑。程序員已經(並且經常)在這裏創造新的抽象。
我來解釋壹下只有Smalltalk支持的特性。以我們多次介紹的Client類為例,假設我們有壹個簡單的需求,遍歷壹個客戶的所有部分;例如,我們想先遍歷姓名,然後遍歷年齡,最後遍歷地址。
C++和Java中對這壹需求的常規解決方案是創建壹個新的特殊的流類或枚舉器類,也許叫做ClientIterator,它有初始化、判斷叠代是否結束、如果沒有結束則叠代下壹個對象等方法。使用這些方法,我們可以編寫壹個循環來初始化叠代器,獲取下壹個對象並處理它,直到叠代結束。叠代器的優點是可以提供單個變量來跟蹤順序處理中叠代的位置;沒有必要將客戶端類擴展成壹個“臨時”變量來進行叠代。
下面是壹段刻意抽象的代碼:
aClient :=獲取壹個的代碼。
客戶端部件Do: [:object |
對象打印:抄本]
註意partsDo:像壹個以object為循環變量的循環。第壹次遍歷時,我們獲取名稱並將其打印到抄本(Smalltalk編程環境中的壹個特殊工作區)。然後第二次遍歷得到年齡,第三次遍歷得到地址。另外值得註意的是,“partsDo:”是壹個關鍵字消息,以“aClient”為接收方,以“[:object | object print on:transcript]”(壹個塊)為參數。
在我深入之前,我先給出Smalltalk的解決方案。然後我解釋它是如何工作的,並給出更多的成語例子。我們需要做的是向客戶端添加以下方法:
零件號:aBlock
封鎖值:自我名稱。
壹個封閉的價值觀:自我年齡。
封鎖值:自身地址。
為了理解這段代碼,我們必須首先認識到這些塊是匿名函數。為了更好地理解我所說的,想象壹下我們想給壹個變量賦壹個函數,但不調用它。我來寫它的類C語法風格(我知道用C語法的確切做法,但對解釋關鍵思想沒有幫助;所以我不會寫嚴格的C語法):
a = f(x){ return x+1;}//C風格的語法
A := [:x | x+1] // Smalltalk語法
這裏變量“a”變成了壹個函數對象。f是壹個函數,我們可以通過“f(10)”調用它得到11。但是我們也可以通過執行“a(10)”來調用它,因為A的值是壹個函數。通過變量“a”執行壹個函數,不需要知道與它原來名字相關的信息。
所以在Smalltalk中,我們甚至不用擔心函數的名字。我們可以很容易地將它賦給任何變量,並普遍使用。在上面函數調用的簡單例子中,我們設置“a值:10”使其返回11。在執行過程中,參數X被綁定到10,參與x+1的運算,直到塊結束,返回最終的計算結果。
通常,我們很少直接執行block。相反,我們把它寫成“partsDo:”並隱藏笨拙的塊“call”來提供抽象函數。
多看看例子。假設我們有壹個維護乘客鏈表的飛機類。我們試著遍歷訪問者中的所有孩子(假設12歲及以下定義為孩子)。實現該功能的代碼如下:
壹架飛機的乘客做:[: person |
人的年齡& lt= 12
ifTrue: [..和某人壹起做某事..]]
如果我們需要在其他上下文中遍歷子節點,壹點抽象將有助於簡化代碼。我們需要做的就是在飛機類中實現壹個名為“kidsDo:”的抽象(我在代碼中添加了行號以供參考):
1.孩子們:壹塊石頭
2.“這裏自己是壹架飛機”
3.自助乘客有:[:person |
4.人的年齡& lt= 12
5.ifTrue: [aBlock value: person]]
我們調整示例代碼來表達抽象,如下所示:
6.壹架飛機kidsDo: [:kid |
7...和孩子壹起做些事情..].
8.“搞定。”
妳能看出6號線是如何工作的嗎?當“孩子們:……”消息時,調用第1行中的“kidsDo:”方法。然後第1行的變量“aBlock”用“[:kid |”綁定..和孩子壹起做些事情...]”(暫時叫kid block)。kidsDo:方法的第三行中的“do:”將遍歷所有乘客。在第5行中,只有當乘客的年齡不高於12時,才會向aBlock發送“值:”消息。當執行以“person”為參數的“value:”消息時,會觸發對kid block的函數調用,導致“kid”被綁定到“person”和“..和孩子壹起做點什麽..”在第7行。在塊的末尾,執行流從“kidsDo:”返回到“Do:”循環(在第5行的末尾),然後繼續以這種方式處理其他孩子。循環結束後,執行流從第6行的“kidsDo:”方法調用返回,到達第8行。
總之,第6行代碼導致第1到5行代碼循環執行,第1到5行將導致kid塊(第7行)執行。
壹般來說,block為Smalltalk提供了抽象控制流的最簡單的方法。它還被巧妙地設計成執行語義返回語句,並且這是表達這種語義的唯壹方式。讓我通過向飛機添加壹個類似於第6到8行的方法來說明這個問題:
10.findAnySickKid
11."在這裏,自我也是壹架飛機."
12.self kidsDo: [:kid |
13.基德·伊斯克
14.ifTrue: [^kid]].
15.零“沒有生病的人”
通讀代碼,我肯定妳不會看到任何異常。這也是壹個遍歷平面上所有孩子的循環。如果發現病童,返回。此外,如果進壹步叠代,如果沒有生病的孩子,循環結束並返回到nil(壹個很容易檢測到的特殊對象)。那麽這裏有什麽值得註意的呢?嗯,有三個重點:10行的findAnySickKid方法的開頭,1行的KidsDo的開頭,13行和14行的kid塊的結尾。通過執行“anAirplane findAnySickKid”,依次調用findanysckkid方法,然後調用kidsDo:方法,再調用kid block方法。在kid塊中執行“^kid”不會返回給發送方(kidsDo方法),而是返回給findAnySickKid的發送方。無論從^kidsDo:到kid block內部的消息鏈有多長,“kid”總是從findAnySickKid返回。原諒我的無知,我沒聽過這個功能的名字。我個人稱之為短路返回。