1. 程式人生 > IOS開發 >巨集定義與可選括號

巨集定義與可選括號

作者:Mike Ash,原文連結,原文日期:2015-03-20 譯者:俊東;校對:numbbbbbNemocdz;定稿:Pancf

前幾天我遇到了一個有趣的問題:如何編寫一個 C 語言前處理器的巨集,刪除包圍實參的括號?

今天的文章,將為大家分享我的解決方案。

起源

C 語言前處理器是一個相當盲目的文字替換引擎,它並不理解 C 程式碼,更不用說 Objective-C 了。它的工作原理還算不錯,可以應付大部分情況,但偶爾也會出現判斷失誤。

這裡舉個典型的例子:

objc
XCTAssertEqualObjects(someArray,@[ @"one",@"two" ],@"Array is not as expected");
複製程式碼

這會無法編譯,並且會出現非常古怪的錯誤提示。前處理器查詢分隔巨集引數的逗號時,沒能將陣列結構 @ [...] 中的東西理解為一個單一的元素。結果程式碼嘗試比較 someArray@[@"one"。斷言失敗訊息 @"two"]@"Array is not as expected" 是另外的實參。這些半成品部分用於 XCTAssertEqualObjects 的巨集擴充套件中,生成的程式碼當然錯得離譜。

要解決這個問題也很容易:新增括號就行。預編譯器不能識別 [],但它確實知道 () 並且能夠理解應該忽略裡面的逗號。下面的程式碼就能正常執行:

objc
XCTAssertEqualObjects(someArray,(@[ @"one",@"two" ]),@"Array is not as expected");
複製程式碼

在 C 語言的許多場景下,你新增多餘的括號也不會有任何區別。巨集擴充套件開之後,生成的程式碼雖然在陣列文字周圍有括號,但沒有異常。你可以寫搞笑的多層括號表示式,編譯器會愉快地幫你解析到最裡面一層:

objc
NSLog(@"%d",((((((((((42)))))))))));
複製程式碼

甚至將 NSLog 這樣處理也行:

objc
((((((((((NSLog))))))))))(@"%d",42);
複製程式碼

在 C 中有一個地方你不能隨意新增括號:型別(types)。例如:

objc
int f(void); // 合法
(int) f(void); // 不合法
複製程式碼

什麼時候會發生這種情況呢?這種情況並不常見,但如果你有一個使用型別的巨集,並且型別包含的逗號不在括號內,則會出現這種情況。巨集可以做很多事情,當一個型別遵循多個協議時,在 Objective-C 中可能出現一些型別帶有未加括號的逗號;當使用帶有多個模板引數的模板化型別時,在 C++ 中也可能出現。舉個例子,這有一個簡單的巨集,建立從字典中提供靜態型別值的 getter

objc
#define GETTER(type,name) \
	- (type)name { \
		return [_dictionary objectForKey: @#name]; \
	}
複製程式碼

你能這樣使用它:

objc
@implementation SomeClass {
	NSDictionary *_dictionary;
}

GETTER(NSView *,view)
GETTER(NSString *,name)
GETTER(id<NSCopying>,someCopyableThing)
複製程式碼

到目前為止沒問題。現在假設我們想要建立一個遵循兩個協議的型別:

objc
GETTER(id<NSCopying,NSCoding>,someCopyableAndCodeableThing)
複製程式碼

哎呀!巨集不起作用了。而且新增括號也無濟於事:

objc
GETTER((id<NSCopying,NSCoding>),someCopyableAndCodeableThing)
複製程式碼

這會產生非法程式碼。這時我們需要一個刪除可選括號的 UNPAREN 巨集。將 GETTER 巨集重寫:

#define GETTER(type,name) \
	- (UNPAREN(type))name { \
		return [_dictionary objectForKey: @#name]; \
	}
複製程式碼

我們該怎麼做呢?

必須的括號

刪除括號很容易:

objc
#define UNPAREN(...) __VA_ARGS__
#define GETTER(type,name) \
	- (UNPAREN type)name { \
		return [_dictionary objectForKey: @#name]; \
	}
複製程式碼

雖然看上去很扯,但這的確能執行。預編譯器將 type 擴充套件為 (id <NSCopying,NSCoding>),生成 UNPAREN (id<NSCopying,NSCoding>)。然後它會將 UNPAREN 巨集擴充套件為 id <NSCopying,NSCoding>。括號,消失!

但是,之前使用的 GETTER 失敗了。例如,GETTER(NSView *,view) 在巨集擴充套件中生成 UNPAREN NSView *。不會進一步擴充套件就直接提供給編譯器。結果自然會報編譯器錯誤,因為 UNPAREN NSView * 是無法編譯的。這雖然可以通過編寫 GETTER((NSView *),view) 來解決,但是被迫新增這些括號很煩人。這樣的結果可不是我們想要的。

巨集不能被過載

我立刻想到了如何擺脫剩餘的 UNPAREN。當你想要一個識別符號消失時,你可以使用一個空的 #define,如下所示:

objc
#define UNPAREN
複製程式碼

有了這個,a UNPAREN b 的序列變為 a b。完美解決問題!但是,如果已經存在帶引數的另一個定義,則前處理器會拒絕此操作。即使前處理器可能選擇其中一個,它也不會同時存在兩種形式。如果可行的話,這能有效解決我們的問題,但可惜的是並不允許:

objc
#define UNPAREN(...) __VA_ARGS__
#define UNPAREN
#define GETTER(type,name) \
	- (UNPAREN type)name { \
		return [_dictionary objectForKey: @#name]; \
	}
複製程式碼

這無法通過前處理器,它會由於 UNPAREN 的重複 #define 而報錯。不過,它引導我們走上了成功的道路。現在的瓶頸是怎麼找出一種方法來實現相同的效果,而不會使兩個巨集具有相同的名稱。

關鍵

最終目標是讓 UNPAREN(x)UNPAREN((x)) 結果都是 x。朝著這個目標邁出的第一步是製作一些巨集,其中傳遞 x(x) 產生相同的輸出,即使它並不確定 x 是什麼。這可以通過將巨集名稱放在巨集擴充套件中來實現,如下所示:

objc
#define EXTRACT(...) EXTRACT __VA_ARGS__
複製程式碼

現在如果你寫 EXTRACT(x),結果是 EXTRACT x。當然,如果你寫 EXTRACT x,結果也是 EXTRACT x,就像沒有巨集擴充套件的情況。這仍然給我們留下一個 EXTRACT。雖然不能用 #define 直接解決,但這已經進步了。

識別符號粘合

前處理器有一個操作符 ##,它將兩個識別符號粘合在一起。例如,a ## b 變為 ab。這可以用於從片段構造識別符號,但也可以用於呼叫巨集。例如:

objc
#define AA 1
#define AB 2
#define A(x) A ## x
複製程式碼

從這裡可以看到,A(A) 產生 1A(B) 產生 2

讓我們將這個運算子與上面的 EXTRACT 巨集結合起來,嘗試生成一個 UNPAREN 巨集。由於 EXTRACT(...) 使用字首 EXTRACT 生成實參,因此我們可以使用識別符號粘合來生成以 EXTRACT 結尾的其他標記。如果我們 #define 那個新標記為空,那就搞定了。

這是一個以 EXTRACT 結尾的巨集,它不會產生任何結果:

objc
#define NOTHING_EXTRACT
複製程式碼

這是對 UNPAREN 巨集的嘗試,它將所有內容放在一起:

objc
#define UNPAREN(x) NOTHING_ ## EXTRACT x
複製程式碼

不幸的是,這並不能實現我們的目標。問題在操作順序上。如果我們寫 UNPAREN((int)),我們將會得到:

objc
UNPAREN((int))
NOTHING_ ## EXTRACT (int)
NOTHING_EXTRACT (int)
(int)
複製程式碼

標示符粘合太早起作用,EXTRACT 巨集永遠不會有機會擴充套件開。

可以使用間接的方式強制前處理器用不同的順序判斷事件。我們可以製作一個 PASTE 巨集,而不是直接使用 ##

objc
#define PASTE(x,...) x ## __VA_ARGS__
複製程式碼

然後我們將根據它編寫 UNPAREN

objc
#define UNPAREN(x)  PASTE(NOTHING_,EXTRACT x)
複製程式碼

仍然不起作用。情況如下:

objc
UNPAREN((int))
PASTE(NOTHING_,EXTRACT (int))
NOTHING_ ## EXTRACT (int)
NOTHING_EXTRACT (int)
(int)
複製程式碼

但更接近我們的目標了。序列 EXTRACT(int) 顯然沒有觸發標示符粘合操作符。我們必須讓前處理器在它看到 ## 之前解析它。可以通過另一種方式間接強制解析它。讓我們定義一個只包裝 PASTEEVALUATING_PASTE 巨集:

objc
#define EVALUATING_PASTE(x,...) PASTE(x,__VA_ARGS__)
複製程式碼

現在讓我們用UNPAREN

objc
#define UNPAREN(x) EVALUATING_PASTE(NOTHING_,EXTRACT x)
複製程式碼

這是展開之後:

objc    
UNPAREN((int))
EVALUATING_PASTE(NOTHING_,EXTRACT (int))
PASTE(NOTHING_,EXTRACT int)
NOTHING_ ## EXTRACT int
NOTHING_EXTRACT int
int
複製程式碼

即使沒有額外加括號也能正常執行,因為額外的賦值並沒有影響:

objc
UNPAREN(int)
EVALUATING_PASTE(NOTHING_,EXTRACT int)
PASTE(NOTHING_,EXTRACT int)
NOTHING_ ## EXTRACT int
NOTHING_EXTRACT int
int
複製程式碼

成功了!我們現在編寫 GETTER 時可以不需要圍繞型別的括號了:

objc
#define GETTER(type,name) \
	- (UNPAREN(type))name { \
		return [_dictionary objectForKey: @#name]; \
	}
複製程式碼

獎勵巨集

在選擇一些巨集來證明這個結構時,我構建了一個很好的 dispatch_once 巨集來製作延遲初始化的常量。實現如下:

objc
#define ONCE(type,name,...) \
	UNPAREN(type) name() { \
		static UNPAREN(type) static_ ## name; \
		static dispatch_once_t predicate; \
		dispatch_once(&predicate,^{ \
			static_ ## name = ({ __VA_ARGS__; }); \
		}); \
		return static_ ## name; \
	}
複製程式碼

使用案例:

objc
ONCE(NSSet *,AllowedFileTypes,[NSSet setWithArray:@[ @"mp3",@"m4a",@"aiff" ]])
複製程式碼

然後,你可以呼叫 AllowedFileTypes() 來獲取集合,並根據需要高效建立集合。如果型別不巧包括括號,新增括號就能執行。

結論

僅僅寫這個巨集,我就發現了很多艱澀的知識。我希望接觸這些知識也不會影響你的思維。請謹慎使用這些知識。

今天就這樣。以後還會有更多令人興奮的探索,可能比這還要再不可思議。在此之前,如果你對此主題有任何建議,請傳送給 我們

本文由 SwiftGG 翻譯組翻譯,已經獲得作者翻譯授權,最新文章請訪問 swift.gg