Objective—C(以下簡稱OC)作為C語言的超集,其基本特性就是C語言可以直接在OC中無縫結合。隨著開發時間越來越長,對C語言的熟悉往往給筆者帶來很多便利,所以這裡一部分做個分析,一部分也是作為一些小經驗的分享。新手寫文,捉襟見肘,總結的不好,或是有更好的方法,還請諸位不吝指出。
本文的示例代碼以Command Line Tools的方式做了一個簡單的demo,上傳到了Github上,如果您感興趣,也可以下載下來自己調試。
程式語言
對於語言的定義多種多樣,但是總讓人覺得差強人意。所以筆者索性自己總結了下,為計算機語言歸納出了三個要素:
1.語素 簡單說來,語素就是符號與文字單元。從早起的二進位0和1,到彙編語言的彙編指令,到C語言的ASCII碼字符集,再到swift的Unicode字符集。當然了,單純的符號是無意義的,更為狹義的語素會經過語言語法的劃分,如關鍵字、標識符、運算符、注釋符等等。就定義而言,語素規範是由語法規定的,而就功能而言,語素明確了我們「用什麼寫」。
2.語法 語法並不是某種顯式的符號,而是編寫或羅列語素的規則。從單個語素的含義,到語素間的排列規則,再到大段代碼的執行順序。語法負責讓一整篇無意義的符號按照一定的規則表達出含義,我們可以將其稱之為語義。一般來說,學語言初期,主要在學的就是語法了。就定義而言,語法是語義的表達框架,就功能而言,語法則明確了我們「如何去寫」。
3.語義 語義,可以歸納為思維的描述,引申的話,也可以歸結為邏輯,畢竟邏輯就是思維的規律。無論是平常交流用的語言也好,與計算機交互使用的計算機語言也罷,究其本質,都是為了表達、溝通與理解。就定義而言,語義是語言使用者思維的具化體現,就功能而言,語義其實就是在表達「想寫什麼」。
雖然從描述思維的角度來說,語義可以是無窮的,但無窮依舊有其局限性。這種局限性可以追溯到設計語言之初,從語言的特性與適用場所窺得端倪。
比如C語言中,選擇ASCII字符集和其字符組合作為語素,用if(...)作為語法中的判斷邏輯表達式,用if(x)return 1;來表達具體的語義。但當我們希望描述一個變量擁有自己的函數時,就顯得無比吃力。我們或許可以用很多句話來模擬,但是想要一句話解決問題就不可能了。究其原因,是因為C語言本身不支持這樣的描述方式。
從層次上來說,支持什麼樣的描述方式往往是由語法來確定的,但究其原因,語法作為表達框架,其集合的邊界是取決於我們語義的寬度的。其實說的簡單點,只是因為設計語言的時候,我們想說的東西還沒有甚至沒敢這麼複雜。
深層次的語言設計初衷就不再探討了,這些理由基本都囊括在自己的語言特性裡,有人說想深學一門語言,就要學它的歷史。從某個角度來說,這也是為了更方便我們去理解,設計語言的人,想要用這個語言來表達什麼而已。
順便討論下日常語言和計算機語言的區別,對於日常語言和來說計算機語言,最大的區別在於受體。日常語言的表達對象是具有相似思維能力的活生生的人,語言從思維形成,到表述,到傳達,到接收時轉化為自己所理解的信息,往往會進行多層次多維度的歸納和演繹。
計算機語言面對的是擁有著既定規則和一定計算能力的機器,所以需要我們的表達是精確到位的。所以,對應的語法也需要精確,「你不是一個人」這樣的歧義句,放在日常語言裡或許只博得一笑,可放在計算機語言裡,問題就嚴重了。
語素、語法、語義作為語言的三要素,未必是最精確和科學的,不過對於計算機語言來說,這也算是一種歸納並定性的思路,可以讓我們對一門語言有另一個層面的認識與特性區分。不推薦強行的教條劃分,頂多在自頂向下或是自底向上分析語言的時候,有個模型做借鑑吧。
聲明、實現與調用
說完了假大空的理論,我們回到C語言。 C語言定義了關鍵字、標識符、運算符、注釋符這樣的基本語素,定義了if、for等等的邏輯語句表達之後,最核心的元素分為兩種:數據與函數。對於C語言而言,代碼的實際構成都可以歸結為對數據和函數做以下三件事:聲明、實現與調用。讓我們從代碼上來做一些簡單的區分:
int var;//變量聲明,格式為 「數據類型 標識符;"
int func(int);//函數聲明,格式為「數據類型 標識符(數據類型,...);」
int func(int x){//函數實現,格式為「數據類型 標識符(數據類型 形參標識符,...){ ... return ...}」
var = x; //變量賦值,利用賦值運算符「=」
printf("%d",var); //變量調用,直接使用變量的標識符
return x?func(x-1):0;//函數調用,格式為「函數名標識符(傳入參數)」
}
相信你看到上面這幾句代碼後面的擁擠的注釋已經心慌慌了,不必緊張,這只是語法的一個列舉。C語言語法自成一本書,筆者不會妄想到用一篇博客講完的。如果你有一定的C語言開發經驗,你就會發現上面這段代碼寫的毫無意義,也就最後一句的遞歸調用可以裝裝逼。
首先,函數有一個很特別的地方,那就是它的調用,比如func()。這樣的表達是被視作一個數據來佔位的,在實際含義中表示它的返回值。這個特性將函數與數據從語法上放在了平行的位置,雖然看著不起眼,但這是個很棒的特性。讓我們可以用同樣的方式去對待計算過程和一個具體的值。 我們可以以此做兩種假設:
把變量當做函數:變量取值本身也是個過程,我們調用一個變量,其實也是通過一定過程取得了一個返回值。 int i = 1;與int i(){return 1;}在功能上是否很相似呢?
把函數當做變量:任何一個函數在調用後一定對應著一個確定的返回值,哪怕是一個void類型,也是與數據類型所對應的。所以哪怕是func(func(func(func(1)))),最終依舊是個int型的數據而已。
當然了,從實現角度來說,變量與函數的差別很大。但在編寫代碼的時候,尤其是將函數當做一個變量,會減少我們所需要關注的地方。我們完全可以聲明一個int whoCare(int,int),當做一個變量調用。最後再去考慮它的實現過程。
當我們把函數的調用視作一個數據佔位符的時候,我們會發現整個語法變得簡單了不少。除了聲明和實現的格式,語句就剩下各種情況下的數據運算了,然後嵌套,調用,隨意重複n++次。
那麼回到我們的聲明和實現,整個語法格式其實是走一步算一步的。
大概就是上圖這個樣子了。語法是個坑,一邊學一邊用成長起來比較快。大部分實踐經驗證明,學語法基本都是從寫什麼不可以開始的,找個報錯功能靠譜,比如Xcode這樣的IDE軟體開始碼代碼吧,什麼時候靜態編譯不報錯了,語法關也就過的差不多了。
OOP的OC
語言設計從來不是一件輕鬆的事情,每一個規則的設計都取捨於性能和易用性,一方面要求嚴謹,一方面要求靈活。從C語言中發展出來的面向對象語言就有好幾種,它們唯一的共同特徵大概就是都可以直接運行C語言代碼了(笑= =)。
從個人情感角度來說,筆者對於OC的這門語言的熱愛大概只有Python能夠分一杯羹了。無關於性能、環境與語法,其鍾愛點僅僅源於方括號[],讓整篇代碼看起來整齊美觀。
為了拓展面向對象的功能,一方面,OC在C語言中採納了很多固有特性來模擬對象特性。另一方面,則另起爐灶,設計了另外一套關鍵字語法系統,而且設計的頗具美感。第一眼看上去,語法或許特殊到再也見不到類似的,但是細細揣摩上去,反倒是因為這種不同讓我們在同一個片段裡混用C語言語法與OC語法,也絕對不會混淆。
理論上,這裡是應該對比一下C++的,不過筆者對C++學習甚淺,還是不要妄加論斷的好。這裡要說的是,OC也加入了與C++混編的功能,不過因為對象機制不同,不同語言下的面向對象語法是不能相互替換的。一想到一篇代碼裡會存在兩種類,一個對象= new CPPClass,一個= [OCClass new],望而生畏。
OC語法區別於C語言的主要地方都是在對象層面,對於基礎的語法,基本上可以照抄照搬,倒也省心省事。
所以從基本語素說起,我們就可以直接看OC的對象與方法了。順便提一句OC的文件結構。在C語言裡面,.c文件是靠#include來聲明包含的,在OC裡面,則會分為.h和.m文件。
如果你像我一樣對引用生出好奇心的話,會發現是沒有對.m的引用的,但是文件裡的實現代碼卻會被執行,答案很簡單,編譯器用另外一種方式替我們做了這件事。至於具體如何做到,這裡先不討論(筆者也順便再研究研究= =)。要說明的是,儘可能不要在OC中寫#import "....m",即便這樣是完全可以的。
第一步,我們來建立一個新的對象好了。為了方便,我稍稍違反一下默認的文件結構,將聲明和實現代碼都搬到一個.h文件裡,請不要在寫代碼的時候這樣做。
//BCObject.h
@import Foundation;
#pragma mark - BCObject.h
@interface BCObject : NSObject
@end
#pragma mark - BCObject.m
@implementation BCObject
@end
與C語言的聲明與實現相對應,在OC中,一個類也需要做這兩件事,並且賦予了專門的關鍵字。在這裡叫關鍵字或許不太合適,你也可以稱為編譯器標記,畢竟從實現角度來說,它們和之前的關鍵字的功能還不太一樣。在OC中,@符號的功能頗多,在這裡,是與後面定義好的字符共同作為一個標記出現,供編譯器進行轉義。比如@import是庫引用,@interface是類聲明,而@implementation則是類實現,當然了,聲明和實現還需要伴隨@end這樣的結束記號。
OC的面向對象語法基本都是在自己的編譯器標記中才可以使用的,它等於和編譯器約定,在@...@end之間的代碼,是使用OC編寫的。也可以反過來做假設,如果不在聲明和實現的段落中,則該語句與對象無關,那麼面向對象的語法也就想當然的無法成立了。 不過,這只是針對聲明和實現,如果僅僅是調用,OC和C語法的混搭,真的是爽歪歪~
@implementation BCObject
+(NSString *)hello{
return @"hello world";
}
@end
void say(NSString *str){
NSLog(@"%@",str);
}
然後,我們就可以使用say([BCObject hello]);這樣的語句來實現"hello world"了。
如果你仔細觀察,你會發現我並沒有為+(NSString *)hello這個方法做聲明,只是和C語言中一樣,把實現放在了調用的代碼段前。
這裡也順便說明一個問題,在面向對象中,往往會有私有方法和實例變量這樣的說法。在OC中,最簡單的方法是把私有的東西放在.m方法裡,因為不存在文件包含,所以也就無法調用了。
而像我們剛才這樣,直接調用@implenmentation的方法,就會發現他是沒有語法上的防禦力的,僅僅是靠文件結構的約定俗成而已。這也是為什麼不讓大家引用.m文件的原因之一。
不過,如果是私有實例變量的話,OC卻是提供了對應的標記來聲明私有。
@import Foundation;
@interface BCObject : NSObject{
@public
NSString *hello;
@private
NSString *bye;
}
@end
@implementation BCObject
-(instancetype)init{
self = [super init];
if(self){
hello = @"hello";
bye = @"bye";
}
return self;
}
-(NSString *)bye{
return bye;
}
@end
//在main.m中調用
int main(int argc, const char * argv[]) {
@autoreleasepool {
say([BCObject hello]);
BCObject *obj = [BCObject new];
say(obj -> hello);
say(obj.bye);
return 0;
}
}
在C語言中,我們可以用->來尋址結構體指針中的元素數據,而在OC中,我們則可以通過這個符號來尋址對象內的實例變量。有沒有聯想到什麼呢?對= =因為OC的對象的其實就是個結構體指針,這個運算是通用的。只不過,我們可以通過@private和@public,當然也會有@protect來限制訪問權限。
不過,在實際編寫中,這樣的聲明和取值實在太過繁瑣,雖然可以這麼用,但早已鮮有人問津。自OC2.0之後,我們有了@property用來解決類內實例變量對外的存取,我們將其翻譯為屬性,並形象的將它的存取方法稱為setter和getter。在這之後,訪問類內的實例變量就全部靠方法,而非指針了。
@interface BCObject : NSObject{
@public
NSString *hello;
@private
NSString *bye;
}
@property NSString *word;
@end
//在main.m中調用
int main(int argc, const char * argv[]) {
@autoreleasepool {
say([BCObject hello]);
BCObject *obj = [BCObject new];
obj.word = obj -> hello;
say(obj.word);
return 0;
}
}
回過頭來看OC的聲明和實現語法,數據的聲明和c語言完全一樣,只不過對象都是指針,所以總會多個"*"號。而在方法的聲明和實現中,為了區別於C語言的函數,OC使用「()」來聲明數據類型,使用「:」來標識傳入參數,並且創新的引入了參雜多個「:」的函數命名方式,讓整個句子看著更符合語言邏輯。
雖然這個特性對我等英語盲人來說意義並不大,但是[you doItWith:A and:B for:C]看起來比you.doIt(A,B,C)還是要舒服的多了。如果英語詞彙量充足的話,我們是可以有效的減少翻閱文檔的次數的。
因為格式特立獨行,所以想要混淆就變的困難。OC的語法看起來唬人,學起來倒也不算難,還是那句話,在xcode裡多寫代碼,什麼時候靜態編譯不報錯了,語法自然就過關了。
多說一點
除了聲明、實現、調用的格式之外,語法需要學習的地方還有很多。
在C語言中,我們要考慮變量的作用域,參數的值傳遞與引用傳遞,要考慮一個值的儲存方式等等。
在OC中,複雜的東西則更多,不過,隨著學習的遞進,我們就會發現這些東西其實不在語法範疇之內,比如模式,比如庫和框架,比如內存管理,再比如Runtime機制。
如果你已經有編程基礎的話,那麼玩轉一門新語言的語法並不是一件非常困難的事。
學語法,或許一周左右。
學習語言特性,或許把玩個把月也就很有成效了。
不過學語言真正困難的地方在於語言的環境:框架、適應場合、需要解決的問題、持續的更新與升級還有使用群體的約定俗成。如果非讓我加個期限的話,那會是,一萬年。
總結與吐槽
對於一個初學者來說,學習語法往往是個很彆扭的事情。因為大量的符號、關鍵字和標記需要逐個去記憶,而且還需要分門別類的明確這些符號分別是做什麼用的。
尤其是對於一個英語盲來說,將int、long、short、float、double、char、void歸為一類比獨立背下來這些單詞還要再難上一分。而好不容易過了詞彙關的我們,接下來面對的示例代碼,全都是些陌生的單詞,根本不知道在說什麼好不好。
記起來當初大神們為特性舉例子總喜歡拿幾個系統內置的生僻方法來舉例,語法理解不了不說,還要特地先去查這些方法是什麼,否則返回值根本莫名其妙啊= =。
正因為這樣的事情總是發生,所以總結一些語法特徵,可以多多少少幫我們點忙。比如()是函數,[]是方法,{}是一大句話,標識符都是大神們戲謔我們特地起的人名地名(吐槽...吐槽= =)。閱讀理解逼著我們挨個看單詞,可讀原始碼,是可以先讀符號的。這也是語法給我們帶來的一點小便利。
(來自bifidy的投稿)
喜歡我們的內容,可以點擊右上角「分享到朋友圈」,或「查看官方帳號」並關注我們。