iOS開發中宏的應用

什麼是宏

宏(#define)是一種抽象(Abstraction),它根據一系列預定義的規則替換一定的文本模式。一個標識符被宏定義後,該標識符便是一個宏名。這時,在程序中出現的是宏名,對於編譯語言,在該程序被編譯前,先將宏名用被定義的字符串替換,這稱爲宏替換,替換後才進行編譯,宏替換是簡單的替換。

爲什麼要用到宏定義

例如,在開發中,當我們在很多地方要用到同一個數值是,肯定不能直接使用字面值,因爲這樣會使代碼變的不可維護,所以我們經常要這樣做:

#define PI 3.1415926535897932

這樣,如果我們要修改PI的精度,直接修改宏就可以了。

我們要針對編譯環境進行優化,要這樣做:

#ifdef DEBUG
    NSLog(@"test");
#endif

通過這個,我們可以只在DEBUG環境下輸出log。

宏定義在C系開發中可以說佔有舉足輕重的作用,通過使用宏定義,我們可以實現靈活的編程,提高開發和執行效率。爲了編譯優化和方便,以及跨平臺能力,宏被大量使用。

如何使用?

宏在預編譯時展開,替換成它的本體。只要我們瞭解到它的這個原理,就能拓展出不同的用法。
上面兩個例子只是簡單的使用,它能滿足我們特定的需求。
宏在預編譯時展開,替換成它的本體。只要我們圍繞它的這個原理,就能拓展出不同的用法,寫出更靈活的代碼,實現神奇的功能;

幾個概念:

  1. 對象宏和函數宏

    分別指定義常量的宏和可以傳參數的宏

  2. 使用”\”來確保多行代碼宏定義可讀性

    #define CHECK_FOR_EGOCACHE_PLIST() \
        if( [key isEqualToString:@”test”] ) { \
            NSLog(@”testlog”); \
        return; \
    }

  3. #與##操作符

    #的功能是將其後面的宏參數進行字符串化操作,意思就是對它所應用的宏變量通過替換後在其左右各加上一個雙引號。##操作符有兩種作用:1是可以把兩個參數字符串拼接在一起,2是當##放在可變參數前時(##__VA_ARGS__),在可變參數數量爲0時,去掉前面的逗號,防止編譯器報錯。

  4. 可變參數”…”

    用作函數宏中,允許傳0個以上的參數,以逗號分隔。

坑在哪?如何處理?

大部分坑都源人們使用它的時候忽略了它的展開過程,大部分是可以通過編寫更嚴謹的宏和更多的使用括號來避免的。

  • 例如,計算兩數的和
#define SUM(a,b) a + b

在與乘法配合時出現了問題

int x = 3 * SUM(2,1);

它的結果是 x = 3 * 2 + 1,顯然不是我們預期的,這種情況我們一般不能吝嗇括號

#define SUM(a,b) (a + b)
  • 還有一種常見情況,一個宏執行多個語句
#define WARN_AND_LOG() warn(); log();

這種語句在碰到條件語句的時候容易出錯:

if (flag)
    WARN_AND_LOG()
else
    //do sth. else

它的展開是

if (flag)
    warn(); log();
else
    //do sth. else

條件控制完全失效了
這種情況一般使用 do while 語句,防止出現意料之外的問題

#define WARN_AND_LOG() do { warn(); log();} while(0)

實例,通過宏來實現靈活的代碼

需求
擴展NSLog()的功能,以使它能打印出更多信息,確定文件行號方法等

首先我們要知道,在預編譯時,編譯器爲我們指定了一些內置的宏(__FILE__,__func__,__LINE__),分別對應文件、方法和行號,像NSLog這種應用廣泛的宏,最好能少改動其他文件的代碼部分。那麼,最終只要在項目的預編譯pch文件里加上下面的代碼:

#define NSLog(format, ...) do {                                                 \
    fprintf(stderr, "<%s : %d> %s\n",                                           \
            [[[NSString stringWithUTF8String:__FILE__] lastPathComponent] UTF8String],  \
            __LINE__, __func__);                                                        \
    NSLog((format), ##__VA_ARGS__);                                           \
    fprintf(stderr, "-------\n");                                               \
} while (0)

我們定義了一個NSLog同名的宏⊙﹏⊙b(幸虧宏的展開比較聰明,編譯時不會循環展開NSLog),根據內置宏,我們打印出了文件名、行號和方法名,爲了安全起見把format括起來,”__VA_ARGS__”用來接收宏的可變參數列表,在它的前邊加上##告訴編譯器在沒有可變參數的時候去掉逗號,do while循環避免了宏展開坑。
它的Log結果是這樣的:

<main.m : 26> main
2015-08-26 00:07:14.975 TestMacro[96566:4306478] Hello, World!

UIKit裏製作view跟名字綁定的NSDictionary的宏

/* This macro is a helper for making view dictionaries for +constraintsWithVisualFormat:options:metrics:views:.  
 NSDictionaryOfVariableBindings(v1, v2, v3) is equivalent to [NSDictionary dictionaryWithObjectsAndKeys:v1, @"v1", v2, @"v2", v3, @"v3", nil];
 */
#define NSDictionaryOfVariableBindings(...) _NSDictionaryOfVariableBindings(@"" # __VA_ARGS__, __VA_ARGS__, nil)
UIKIT_EXTERN NSDictionary *_NSDictionaryOfVariableBindings(NSString *commaSeparatedKeysString, id firstValue, ...) NS_AVAILABLE_IOS(6_0);

這句宏就應用了#號來把參數轉義成string,隨後再進行處理。

宏的展開順序爲先完全展開宏參數,再掃描宏展開后里面的宏,如此反覆,有兩種例外參數不會被先展開,宏內容中含有#,##這兩個符號的時候。
根據這個特性,我們可以做到一個很神奇的功能:打印出宏的展開語句

#define __toString(x) __toString_0(x)
#define __toString_0(x) #x
#define LOG_MACRO(x) NSLog(@"%s=\n%s", #x, __toString(x))

用它來打印我們剛剛的SUM宏,結果是這樣的

LOG_MACRO(SUM(10,3));
2015-08-26 01:02:02.011 TestMacro[96950:4438855] SUM(10,3)=
(10 + 3)

如果你不太明白的話,可以一步一步展開來解析
1. 第一層展開,展開了LOG_MACRO()

NSLog(@"%s=\n%s", "SUM(10,3)", __toString(SUM(10,3)));

2.第二層展開,展開了__toString()和SUM()

NSLog(@"%s=\n%s", "SUM(10,3)", __toString_0((a + b)));

3.第三層展開,__toString_0()

NSLog(@"%s=\n%s", "SUM(10,3)", "(a + b)");

是不是很神奇?

結語

本文對C中宏定義#define在使用時容易出現的問題進行了解析,列舉了多個例子希望能清楚的解釋問題,謝謝觀看~

                                                                     杏樹林研發 王儒林
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章