第三章 接口與API設計—第15條:用前綴避免命名空間沖突

我們在構建應用程序時,可能想將其中部分代碼用于后續項目,也可能想把某些代碼發布出來,供他人使用。即便現在還不想這么做,將來也總會有用到的時候。如果決定重用代碼,那么我們在編寫接口時就會將其設計成易于復用的形式。這需要用到Objective-C語言中常見的編程范式(paradigm),同時還需了解各種可能碰到的陷阱。
近年來,開源社區與開源組件隨著iOS開發而流行起來,所以我們經常會在開發自己的應用程序時使用他人所寫的代碼。與此同時,別人也會用到你的代碼,所以,要把代碼寫的清晰一些,以便其他開發者能夠迅速而方便地將其集成到他們的項目里。沒準會有成千上萬個應用程序使用你所寫的程序庫呢。


Objective-C沒有其他語言那種內置的命名空間(namespace)機制。鑒于此,我們在起名時要設法避免潛在的命名沖突,否則很容易就重名了。如果發生命名沖突(naming clash),那么應用程序的鏈接過程就會出錯,因為其中出現了重復符號:

duplicate symbol _OBJC_METACLASS_$_EOCTheClass in:
      build/something.o
      build/something_else.o
duplicate symbol _OBJC_CLASS_$_EOCTheClass in:
      build/something.o
      build/something_else.o

錯誤原因在于,應用程序中的兩份代碼都各自實現了名為EOCTheClass的類,這導致EOCTheClass所對應的類符號和"元類"符號各定義了兩次。你也許是把兩個相互獨立的程序庫都引入到當前項目中,而它們又恰好有重名的類,所以產生了這一問題。
比無法鏈接更糟糕的情況是,在運行期載入了含有重名類的程序庫。此時,"動態加載器"(dynamic loader)就遭遇了"重名符號錯誤"(duplicate symbol error),很可能會令整個應用程序崩潰。
避免此問題的唯一辦法就是變相實現命名空間: 為所有名稱都加上適當前綴。所選前綴可以使公司、應用程序或二者皆有關聯之名。比方說,假設你所在的公司叫做Effective Widgets,那么就可以在所有應用程序都會用到的那部分代碼中使用EWS作前綴,如果有些代碼只用于名為Effective Browser的瀏覽器項目中,那就在這部分代碼中使用EWB作前綴。即便加了前綴,也難保不出現命名沖突,但是其幾率會小很多。
使用Cocoa創建應用程序時一定要注意,Apple宣稱其保留使用所有"兩字母前綴"(two-letter prefix)的權利,所以你自己選用的前綴應該是三個字母的。舉個例子,假如開發者不遵循這條守則,使用TW這兩個字母作前綴,那么就會出問題。iOS5.0 SDK發布時,包含了Twitter框架,此框架就使用TW作前綴,其中有個類叫做TWRequest,它可以發送HTTP請求以調用Twitter API。如果你所在的公司叫做Tiny Widgets,那么很有可能把訪問本公司API所用的那個類也命名為TWRequest。
不僅是命名,應用程序中的所有名稱都應加前綴。如果要為既有類新增"分類"(category),那么一定要給"分類"及"分類"中的方法加上前綴,第25條解釋了這么做的原因。開發者可能會忽視另外一個容易引發命名沖突的地方,那就是類的實現文件中所用的純C函數及全局變量,這個問題必須要注意。大家可別忘了,在編譯好的目標文件中,這些名稱是要算作"頂級符號"(top-level symbol)的。比方說,iOS SDK的AudioToolbox里有個函數能播放聲音文件。開發者可向其傳入回調函數(callback),以便在播放完畢時調用。你也許想編寫一個Objective-C類,把這套邏輯封裝起來,當播放完聲音文件之后,即命令其中的委托對象(delegate)處理回調事宜:

//EOCSoundPlayer.h
#import <Foundation/Foundation.h>

@class EOCSoundPlayer;
@protocol EOCSoundPlayerDelegate <NSObject>
- (void)soundPlayerDidFinish:(EOCSoundPlayer *)player;
@end

@Interface EOCSoundPlayer : NSObject
@property (nonatomic, weak) id <EOCSoundPlayerDelegate> delegate;
- (id)initWithURL:(NSURL *)url;
- (void)playSound;
@end
//EOCSoundPlayer.m
#import "EOCSoundPlayer.h"
#import <AudioToolbox/AudioToolbox.h>

void completion (SystemSoundID ssID, void*clientData) {
      EOCSoundPlayer *player = (__bridge EOCSoundPlayer *)clientData;
      if ([player.delegate respondsToSelector:@selector(soundPlayerDidFinish:)]) {
              [player.delegate soundPlayerDidFinish:player];
        }
}

@implementation EOCSoundPlayer {
      SystemSoundID _systemSoundID;
}

- (id)initWithURL:(NSURL *)url {
        if ((self = [super init])) {
              AudioServicesCreateSystemSoundID((__bridge CFURLRef)url, &_systemSoundID);
        }
        return self;
}

- (void)dealloc {
 AudioServicesDisposeSystemSound(_systemSoundID);
}

- (void)playSound{
      AudioServicesAddSystemSoundCompletion {
            _systemSoundID,
            NULL,   
            NULL,
            completion,
            (__bridge void *)self;
  AudioServicesPlaySystemSound(_systemSoundID);
      }
      @end
}

這段代碼看上去完全正常,不過你再看看該類目標文件中的符號表(symbol table),就會發現問題了:

屏幕快照 2017-04-13 09.32.46.png

符號表中間有個名叫_completion的符號,這就是為了處理聲音播放完畢之后的邏輯而創建的那個completion函數。雖說此函數是在實現文件里定義的,并沒有聲明于頭文件中,不過它仍然算作"頂級符號"。這樣的話,若在別處又創建了一個名叫completion的函數,則會于鏈接時發生類似下面這種"重復符號錯誤":

duplicate symbol _completion in:
        build/EOCSoundPlayer.o
        build/EOCAnotherClass.o

如果將代碼發布為程序庫,供他人在開發應用程序時使用,那么就更糟糕了。這等于辦了件壞事: 因為已經有了名叫_completion的符號,所以使用此程序庫的開發者就無法再創建名為completion的函數了。

由此可見,我們總是應該給這種C函數的名字加上前綴。比方說,在剛才那個例子中,播放完聲音之后所執行的處理程序可以改名為EOCSoundPlayerCompletion。這么做還有個好處:若此符號出現在棧回溯信息中,則很容易就能判明問題源自哪塊代碼。
如果用第三方庫編寫自己的代碼,并準備將其再發布為程序庫供他人開發應用程序所用,那么尤其要注意重復符號問題。你的程序庫所包含的那個第三方庫也許還會為應用程序本身所引入,若是如此,那就很容易出現重復符號錯誤了。這是應該給你所用的那一份第三方庫代碼都加上你自己的前綴。例如,你準備發布的程序庫叫做EOCLibrary,其中引入了名為XYZLibrary的第三方庫,那么就應該把XYZLibrary中的所有名字都冠以EOC。于是,應用程序就可以隨意使用它自己直接引入的那個XYZLibrary庫了,而不必擔心與EOCLibrary里的這個XYZLibrary相沖突。
雖說逐個改名是很令人厭煩的事情,不過若想避免命名沖突,還是得費這番功夫才行。讀者也許會問: 為什么非要這么做呢?應用程序自己不要直接引入XYZLibrary,改用EOCLibrary里面的那個不就行了嗎?沒錯,可以這么做,但是,應用程序也許還會引入另一個名為ABCLibrary的第三方庫,而該庫中又包含了XYZLibrary。此時,如果你和ABCLibrary庫的作者都不給各自所用的XYZLibrary加前綴,那么應用程序依然會出現重復符號錯誤。還有一種可能就是,你的庫里所用的XYZLibrary是X版本的,而應用程序卻需要使用Y版本的某些功能,所以它必須自己再引入一份。你可以花些時間,使用幾個流行的第三方庫來開發一下iOS程序,那時會經常看到這種前綴的。

要點

  • 選擇與你的公司、應用程序或二者皆有關聯之名稱作類名的前綴,并在所有代碼中均使用這一前綴。
  • 若自己所開發的程序庫中用到了第三方庫,則應為其中的名稱加上前綴。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,923評論 18 139
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 173,259評論 25 708
  • 接上篇函數返回 異常 擴充類型的功能 有點像iOS中的分類 js允許給基本類型擴充功能。通過給Object的pro...
    紙簡書生閱讀 336評論 0 0
  • 2018年3月3日周六上午10點零基礎新班開課,學費2600/25課時,上課地點:在臺東體育街羽毛球場兒童...
    楚言初語閱讀 217評論 0 0
  • 2017是瘋狂的一年 科技的發展 思想運轉的速度跟不上發展的速度 每天都千變萬化 站在一個位置 觀賞著BAT在各個...
    玥燁閱讀 447評論 0 0