玩轉iOS“巨集定義”

来源:https://www.cnblogs.com/simplepp/archive/2020/04/28/12793880.html
-Advertisement-
Play Games

巨集定義在C類語言中非常重要,因為巨集是一種預編譯時的功能,因此其可以比運行時更高層面的對程式流程進行控制。在初學巨集定義的時候,大家可能都會有這樣一種感覺:就是完全替換麽,太簡單了。但如果你真這麼想,那你就太天真了,不說自己編寫巨集,在Foundation框架中內置定義的許多巨集要看明白也要費一番腦筋。本篇 ...


巨集定義在C類語言中非常重要,因為巨集是一種預編譯時的功能,因此其可以比運行時更高層面的對程式流程進行控制。在初學巨集定義的時候,大家可能都會有這樣一種感覺:就是完全替換麽,太簡單了。但如果你真這麼想,那你就太天真了,不說自己編寫巨集,在Foundation框架中內置定義的許多巨集要看明白也要費一番腦筋。本篇博客,總結了前輩的經驗,同時收集了一些編寫非常巧妙的巨集進行分析,希望可以幫助大家對巨集定義有更加深刻的理解,並且可以將心得應用於實際開發中。

一、準備

巨集的本質是預編譯時的替換,在開始正文之前,我們需要先介紹一種觀察巨集替換後結果的方法,這樣幫助我們更方便的對巨集最終的結果進行驗證與測試。Xcode開發工具自帶查看預編譯結果的功能,首先需要對工程編譯一遍,之後選擇工具欄中的Assistant選項,打開助手視窗,如下圖所示:

 

之後選擇視窗的Preprocess選項,即可打開預編譯結果視窗,可以看到,巨集被替換後的最終結果,如下圖所示:

 

後面,我們將使用這種方式來對編寫的巨集進行驗證。

二、關於“巨集定義”

巨集使用#define來進行定義,巨集定義分為兩種,一種是對象式巨集,一種是函數式巨集。對象式巨集通常對來定義量值,在預編譯時,直接將巨集名替換成對應的量值,函數式巨集在定義時可以設置參數,其作用與函數很類似。

例如,我們可以將π的值定義成一個對象式巨集,在使用的時候,用有意義的巨集名要比直接使用π的字面值方便很多,例如:

#import <Foundation/Foundation.h>
#define PI 3.1415926
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        CGFloat res = PI * 3;
        NSLog(@"%f", res);
    }
    return 0;
}

函數式巨集要更加靈活一些,例如對圓面積計算的方法,我們就可以將其定義成一個巨集:

#define PI 3.1415926
#define CircleArea(r) PI * r * r
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        CGFloat res = CircleArea(1);
        NSLog(@"%f", res);
    }
    return 0;
}

現在,有了這個面積計算巨集我們可以更加方便的計算圓的面積了,看上去很完美,後面我們就使用這個函數式巨集為例,來深入理解巨集的原理。

作為一個開發者,有一個學習的氛圍跟一個交流圈子特別重要,這是一個我的點擊加入群聊iOS交流群:789143298 ,不管你是小白還是大牛歡迎入駐 ,分享BAT,阿裡面試題、面試經驗,討論技術,大家一起交流學習成長!

 

三、從一個簡單的函數式巨集說起

再來看下上面我們編寫的計算面積的巨集,正常情況下好像沒什麼問題,但是需要註意,歸根結底巨集並不是函數,如果完全把其作為函數使用,我們就可能會陷入一系列的陷阱中,比如這樣使用:

#define PI 3.1415926
#define CircleArea(r) PI * r * r
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        CGFloat res = CircleArea(1 + 1);
        NSLog(@"%f", res);
    }
    return 0;
}

運行代碼,運算的結果並不是半徑為2個圓的面積,哪裡出了問題呢,我們還是先看下巨集預編譯後的結果:

CGFloat res = 3.1415926 * 1 + 1 * 1 + 1;

一目瞭然了,由於運算符的優先順序問題導致了運算順序錯誤,在編程中,所有運算符優先順序產生的問題都可以使用一種方式解決:用小括弧。對CircleArea巨集進行一下改造,如下:

#define CircleArea(r) (PI * (r) * (r))

對執行順序進行了強制的控制,代碼執行又恢復了正常,看上去好像是沒有問題了,現在就滿意了還為時過早,例如下麵這樣使用這個巨集:

#import <Foundation/Foundation.h>
#define PI 3.1415926
#define CircleArea(r) PI * (r) * (r)
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        int r = 1;
        CGFloat res = CircleArea(r++);
        NSLog(@"%f, %d", res, r);
    }
    return 0;
}

運行,發現結果又錯了,不僅計算結果與我們的預期不符,變數自加的的結果也不對了,我們檢查其展開的結果:

CGFloat res = 3.1415926 * (r++) * (r++);

原來問題出在這裡,巨集在展開的時候,將參數替換了兩次,由於參數本身是一個自加表達式,所以被自加了兩次,產生了問題,那麼這個問題怎麼解決呢,C語言中有一種很有用的語法,即使用大括弧定義代碼塊,代碼塊會將最後一條語句的執行結果返回,修改上面巨集定義如下:

#import <Foundation/Foundation.h>
#define PI 3.1415926
#define CircleArea(r)   \
({                      \
    typeof(r) _r = r;   \
    (PI * (_r) * (_r)); \
})
int main(int argc, const char * argv[]) {
    @autoreleasepool {

        int r = 1;
        CGFloat res = CircleArea(r++);
        NSLog(@"%f, %d", res, r);
    }
    return 0;
}

這次程式又恢復的了正常。但是,如果如果在調用巨集是變數的名字與巨集內的臨時變數產生了重名,災難就又發生了,例如:

#import <Foundation/Foundation.h>
#define PI 3.1415926
#define CircleArea(r)   \
({                      \
    typeof(r) _r = r;   \
    (PI * (_r) * (_r)); \
})
int main(int argc, const char * argv[]) {
    @autoreleasepool {

        int _r = 1;
        CGFloat res = CircleArea(_r);
        NSLog(@"%f, %d", res, _r);
    }
    return 0;
}

運行上面代碼,會發現巨集內的臨時變數沒有被初始化成功。這確實難受,我們在進一步,比如對臨時變數的名字做一些手腳,將其命名為極其不容易重覆的名字,其實系統內置的一個巨集就是專門用來構造唯一性變數名的:COUNTER,這個巨集是一個計數器,在編譯的時候會自動進行累加,再次對我們編寫的巨集進行改造,如下:

#import <Foundation/Foundation.h>
#define PI 3.1415926
#define PAST(A, B) A##B
#define CircleArea(r)   __CircleArea(r, __COUNTER__)
#define __CircleArea(r, v)      \
({                              \
    typeof(r) PAST(_r, v) = r;         \
    (PI * PAST(_r, v) * PAST(_r, v));     \
})
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int _r = 1;
        CGFloat res = CircleArea(_r);
        CGFloat res2 = CircleArea(_r);
        NSLog(@"%f, %f", res, res2);
    }
    return 0;
}

這裡改造後,我們的巨集就沒有那麼容易理解了,首先COUNTER在每次巨集替換時都會進行自增,##是一種巨集中專用的特殊符號,用來將參數拼接到一起,但是需要註意,使用##符號拼接的如果是另外一個巨集,則其會阻止巨集的展開,因此我們定義了一個轉換巨集PAST(A, B)來處理拼接。如果你一下子不能理解為什麼這樣就可以解決巨集展開的問題,你只需要記住這樣一條巨集展開的原則:如果形參有使用#或##這種處理符號,則不會進行巨集參數的展開,否則先展開巨集參數,在展開當前巨集。上面代碼最終預編譯的結果如下:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int _r = 1;
        CGFloat res = ({ typeof(_r) _r0 = _r; (3.1415926 * _r0 * _r0); });
        CGFloat res2 = ({ typeof(_r) _r1 = _r; (3.1415926 * _r1 * _r1); });
        NSLog(@"%f, %f", res, res2);
    }
    return 0;
}

一個簡單的計算圓面積的巨集,為了安全,我們就進行了這麼多的處理,看來要用好巨集,的確不容易。

四、編寫巨集時的好習慣

通過前面的介紹,我們知道,如果隨隨意意的編寫一個巨集是非常不負責任的,看上去好像沒問題與在任何場景下使用都沒有問題是完全不同的。在編寫巨集時,我們可以刻意的去培養這樣幾個編碼習慣:

  • 參數與計算結果要加小括弧

    這條原則應該不必多說了,前面的示例中就有演示,完整的添加小括弧可以避免很多由於運算符優先順序造成的異常問題。

  • 多語句功能性巨集,要使用do-while包裹

    這條原則看上去有些莫名其妙,但是其非常重要,例如,我們需要編寫一個自定義的LOG巨集,在進行列印時添加一些自定義的信息,你或許會這樣寫:

#define LOG(string)     \
NSLog(@"自定義的信息");   \
NSLog(string);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LOG(@"info")
    }
    return 0;
}

運行代碼,目前貌似沒有問題,但是如果其和if語句進行結合,可能問題就來了:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        if (NO)
            LOG(@"info")
    }
    return 0;
}

運行代碼,還是有一行LOG信息被輸出了,看下其預編譯後的結果如下:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        if (__objc_no)
            NSLog(@"自定義的信息"); NSLog(@"info");
    }
    return 0;
}

找到問題了,由於if結構如果不加大括弧進行規範,其預設作用域只有一句代碼,多寫大括弧是不會出問題,因此編寫多語句巨集時,加上大括弧是一個好習慣,如下:

#define LOG(string)     \
{NSLog(@"自定義的信息");   \
NSLog(string);}

這樣解決了問題,但是並不完美,假設在使用時這樣寫:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        if (NO)
            LOG(@"NO");
        else
            LOG(@"YES");
    }
    return 0;
}

結果發現還是會報錯,是由於分號搗的鬼,預編譯結果如下:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        if (__objc_no)
            {NSLog(@"自定義的信息"); NSLog(@"NO");};
        else
            {NSLog(@"自定義的信息"); NSLog(@"YES");};
    }
    return 0;
}

我們知道,像if,while,for這種語法結構塊的大括弧後是不需要分號的,我們為了相容單行if語句由於巨集的原因被展開成多行的問題強行加了一個大括弧上去,就產生這樣的問題了,解決它的一個好方法是真的將多行的巨集轉化成單語句,do-whlie結構就可以實現這種效果,修改巨集如下:

#define LOG(string)     \
do {NSLog(@"自定義的信息");   \
NSLog(string);} while(0);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        if (NO)
            LOG(@"NO")
        else
            LOG(@"YES");
    }
    return 0;
}

預編譯後:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        if (__objc_no)
            do {NSLog(@"自定義的信息"); NSLog(@"NO");} while(0);
        else
            do {NSLog(@"自定義的信息"); NSLog(@"YES");} while(0);;
    }
    return 0;
}

現在,無論外面怎麼使用,這個巨集都可以正常工作了。

  • 對於不定參數的巨集,藉助##符號來拼接參數

    在定義函數時,我們可以定義函數的參數為不定個數參數,定義函數式巨集時也類似,使用符號"..."可以指定不定個數參數,例如對LOG巨集進行調整,如下:

#define LOG(format, ...)     \
do {NSLog(@"自定義的信息");   \
NSLog(format, __VA_ARGS__);} while(0);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        if (NO)
            LOG(@"%d", NO)
        else
            LOG(@"%d", YES);
    }
    return 0;
}

VA_ARGS也是一個內置的巨集符號,則作用是代表巨集定義中的可變參數“...”,需要註意,如果按照上面的寫法,如果我們傳入的可變參數為0個,會產生問題,其原因也是由於多了一個逗號,例如:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        if (NO)
            LOG(@"%d") // 這裡會被預編譯成NSLog(@"%d", )
        else
            LOG(@"%d", YES);
    }
    return 0;
}

解決方案是對可變參數進行一次##拼接,巨集在使用##符號進行參數拼接時,如果後面的參數為空,其會自動將前面的逗號去掉,如下:

#define LOG(format, ...)     \
do {NSLog(@"自定義的信息");   \
NSLog(format, ##__VA_ARGS__);} while(0);

五、特殊的巨集符號與常用內置巨集

有幾個特殊的符號可以讓巨集定義變得非常靈活,常用的特殊符號和特殊巨集列舉如下:

  •  

    井號的作用是將參數字元串化,例如:

#define Test(p) #p

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Test(abc); // 預編譯後成為  "abc";
    }
    return 0;
}

 

  •  

    雙井號我們前面有使用過,其作用是對參數進行拼接,例如:

#define Test(a,b) a##b

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Test(1,2); // 預編譯後成為  12;
    }
    return 0;
}
  • __TIME __

    可變參數巨集中專用,表示所有傳入的可變參數。

  • __COUNTER __

    一個累加計數巨集,常用來構造唯一變數名。

  • __LINE __

    記錄LOG信息時,常用的一個內置巨集,預編譯時會將其替換為當前的行號。

  • __FILE __

    記錄LOG信息時,常用的一個內置巨集,預編譯時會將其替換為當前文件的全路徑。

  • __FILE_NAME __

    記錄LOG信息時,常用的一個內置巨集,預編譯時會將其替換為當前的文件名。

  • __DATE __

    記錄LOG信息時,常用的一個內置巨集,預編譯時會將其替換為當前日期。

  • __TIME __

    記錄LOG信息時,常用的一個內置巨集,預編譯時會將其替換為當前時間。

六、巨集的展開規則

通過前面的介紹,對於應用巨集我們已經沒有太大的問題,並且也瞭解了很多巨集的使用技巧。這一小節將更深入的對巨集的替換規則進行討論。巨集本身是支持嵌套的,例如:

#define M1(A) M2(A)
#define M2(A) A
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        M1(1);
    }
    return 0;
}

上面代碼中定義的兩個巨集基本上是沒有意義的,M1巨集替換後的結果是M2巨集,M2巨集最終被替換為參數本身,從這個例子可以看出,巨集是可以嵌套遞歸展開的,但是遞歸展開是有原則,不會出現無限遞歸,例如:

#define M1(A) M2(A)
#define M2(A) M1(A)
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        M1(1); // 最終展開為 M1(1)
    }
    return 0;
}

巨集的展開需要符合下麵原則:

  1. 在展開巨集的過程中會先將參數進行展開,如果使用##對參數進行了拼接或使用#進行了處理,則此參數不會被展開。
  2. 在巨集的展開過程中,如果替換列表中出現了要被展開的巨集,則此巨集不會被展開。

上面的展開原則提到了替換列表,巨集在展開過程中會維護一個替換列表,展開的過程中需要從參數到巨集本身,從外層巨集到內層巨集一層一層的替換,每次替換的時候都會將被替換的巨集名放入維護的替換列表中,再下一輪替換中,如果再次出現替換列表中出現過的巨集名,則不會被再次替換。以我們上面的代碼為例進行分析:

  1. 首先M1巨集在第一輪替換後,被替換成了M2,此時替換列表中放入巨集名M1。
  2. M2依然是一個巨集名,第二輪對M2進行替換,將其替換為M1,再次將M2放入替換列表,此時替換列表中有巨集名M1和M2。
  3. M1依然是巨集名,但是替換列表中已經存在M1,此巨集名不再展開。

七、巨集的妙用

這一小節,我們要轉身成為鑒賞家,來對很多實用的巨集的巧妙案例進行分析與鑒賞。從這些優秀的使用案例中,可以擴寬我們對巨集使用的思路。

  1. MIN與MAX

    Foundataion內置了一些常用的運算巨集,如獲取兩個數的最大值、最小值、絕對值等等。以MAX巨集為例,這個巨集的編寫基本涵蓋了函數式巨集所有要註意的點,如下:

#define __NSX_PASTE__(A,B) A##B
#if !defined(MAX)
    #define __NSMAX_IMPL__(A,B,L) ({ __typeof__(A) __NSX_PASTE__(__a,L) = (A); __typeof__(B) __NSX_PASTE__(__b,L) = (B); (__NSX_PASTE__(__a,L) < __NSX_PASTE__(__b,L)) ? __NSX_PASTE__(__b,L) : __NSX_PASTE__(__a,L); })
    #define MAX(A,B) __NSMAX_IMPL__(A,B,__COUNTER__)
#endif

其中NSMAX_IMPL巨集藉助計數COUNTER和拼接NSX_PASTE巨集來構造唯一的內部變數名,我們前面提供的示例巨集的寫法也基本是參照這個系統巨集來的。後面大家在編寫函數式巨集的時候,都可以參照下這個巨集的實現。

  1. NSAssert等

    NSAssert是斷言巨集,在開發調試中經常會使用斷言來進行安全保障,這個巨集的定義如下:

#define NSAssert(condition, desc, ...)  \
    do {                \
    __PRAGMA_PUSH_NO_EXTRA_ARG_WARNINGS \
    if (__builtin_expect(!(condition), 0)) {        \
            NSString *__assert_file__ = [NSString stringWithUTF8String:__FILE__]; \
            __assert_file__ = __assert_file__ ? __assert_file__ : @"<Unknown File>"; \
        [[NSAssertionHandler currentHandler] handleFailureInMethod:_cmd \
        object:self file:__assert_file__ \
            lineNumber:__LINE__ description:(desc), ##__VA_ARGS__]; \
    }               \
        __PRAGMA_POP_NO_EXTRA_ARG_WARNINGS \
    } while(0)

NSAssert巨集定義中使用到了不定參數拼接消除逗號的技巧,並且是多行巨集語句使用do-while進行優化的一個實踐。

  1. @weakify與@strongify

weakify與strongify是ReactCocoa中常用的兩個巨集,用來處理迴圈引用問題。這兩個巨集的定義非常巧妙,以weakify巨集為例,要看懂這個巨集並不是十分簡單,首先與這個巨集相關的巨集定義列舉如下:

#if DEBUG
#define rac_keywordify autoreleasepool {}
#else
#define rac_keywordify try {} @catch (...) {}
#endif

#define rac_weakify_(INDEX, CONTEXT, VAR) \
CONTEXT __typeof__(VAR) metamacro_concat(VAR, _weak_) = (VAR);

#define weakify(...) \
rac_keywordify \
metamacro_foreach_cxt(rac_weakify_,, __weak, __VA_ARGS__)

#define metamacro_foreach_cxt(MACRO, SEP, CONTEXT, ...) \
metamacro_concat(metamacro_foreach_cxt, metamacro_argcount(__VA_ARGS__))(MACRO, SEP, CONTEXT, __VA_ARGS__)

#define metamacro_argcount(...) \
metamacro_at(20, __VA_ARGS__, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1)
#define metamacro_at20(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17, _18, _19, ...) metamacro_head(__VA_ARGS__)
#define metamacro_at(N, ...) \
metamacro_concat(metamacro_at, N)(__VA_ARGS__)

#define metamacro_concat(A, B) \
metamacro_concat_(A, B)

#define metamacro_concat_(A, B) A ## B

#define metamacro_head(...) \
metamacro_head_(__VA_ARGS__, 0)

#define metamacro_foreach_cxt1(MACRO, SEP, CONTEXT, _0) MACRO(0, CONTEXT, _0)

#define metamacro_head_(FIRST, ...) FIRST

其中rac_keywordify區分DEBUG和RELEASE環境,在DEBUG環境下,其實際上是創建了一個無用的autoreleasepool,消除前面的@符號,在RELEASE環境下,其會創建一個try-catch結構,用來消除參數警告。metamacro_foreach_cxt巨集比較複雜,其展開過程如下:

// 第一步: 原始巨集
metamacro_foreach_cxt(rac_weakify_,, __weak, obj)
// 第二步: 展開metamacro_foreach_cxt
metamacro_concat(metamacro_foreach_cxt, metamacro_argcount(obj))(rac_weakify_,, __weak, obj)
// 第三步: 展開metamacro_argcount      
metamacro_concat(metamacro_foreach_cxt, metamacro_at(20, obj, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1))(rac_weakify_,, __weak, obj)
// 第四步: 展開metamacro_at       
metamacro_concat(metamacro_foreach_cxt,metamacro_concat(metamacro_at, 20)(obj, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1))(rac_weakify_,, __weak, obj)
// 第五步:展開metamacro_concat       
metamacro_concat(metamacro_foreach_cxt,metamacro_at20(obj, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1))(rac_weakify_,, __weak, obj)
// 第六步:展開metamacro_at20        
metamacro_concat(metamacro_foreach_cxt,metamacro_head(1))(rac_weakify_,, __weak, obj)
// 第七步:展開metamacro_head        
metamacro_concat(metamacro_foreach_cxt,metamacro_head_(1, 0))(rac_weakify_,, __weak, obj)
// 第八步:展開metamacro_head_      
metamacro_concat(metamacro_foreach_cxt,1)(rac_weakify_,, __weak, obj)
// 第九步:展開metamacro_concat        
metamacro_foreach_cxt1(rac_weakify_,, __weak, obj)
// 第十步:展開metamacro_foreach_cxt1
rac_weakify_(0, __weak, obj)
// 第十一步:展開rac_weakify_
__weak __typeof__(obj) metamacro_concat(obj, _weak_) = (obj);
// 第十二步:展開metamacro_concat        
__weak __typeof__(obj) obj_weak_ = (obj);

strongify巨集的展開與之類似。

  1. ParagraphStyleSet巨集

    ParagraphStyleSet巨集是YYLabel中提供的一個設置屬性字元串ParagraphStyle相關屬性的快捷方法,其中使用到的一個技巧是直接使用巨集的形參作為屬性名進行使用,使得各種屬性的設置都使用同一個巨集即可完成,其定義如下:

#define ParagraphStyleSet(_attr_) \
[self enumerateAttribute:NSParagraphStyleAttributeName \
                 inRange:range \
                 options:kNilOptions \
              usingBlock: ^(NSParagraphStyle *value, NSRange subRange, BOOL *stop) { \
                  NSMutableParagraphStyle *style = nil; \
                  if (value) { \
                      if (CFGetTypeID((__bridge CFTypeRef)(value)) == CTParagraphStyleGetTypeID()) { \
                          value = [NSParagraphStyle yy_styleWithCTStyle:(__bridge CTParagraphStyleRef)(value)]; \
                      } \
                      if (value. _attr_ == _attr_) return; \
                      if ([value isKindOfClass:[NSMutableParagraphStyle class]]) { \
                          style = (id)value; \
                      } else { \
                          style = value.mutableCopy; \
                      } \
                  } else { \
                      if ([NSParagraphStyle defaultParagraphStyle]. _attr_ == _attr_) return; \
                      style = [NSParagraphStyle defaultParagraphStyle].mutableCopy; \
                  } \
                  style. _attr_ = _attr_; \
                  [self yy_setParagraphStyle:style range:subRange]; \
              }];

八、結語

巨集看上去簡單,但是真的用好用巧卻並不容易,我想,最好的學習方式就是在實際應用中不斷的使用,不斷的琢磨與優化。如果能將巨集的使用駕輕就熟,一定會為你的代碼能力帶來質的提升。

更多文章:

2020年iOS大廠面試題總結(一)
iOS學習棧(將持續更新)上
阿裡、位元組:一套高效的iOS面試題

推薦

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 1. MongoDB創建用戶角色及開啟驗證 先啟動單例的mongodb [root@service ~]# mongod -f mongodb.conf 以配置的方式啟動 配置文件如下 systemLog: #MongoDB發送所有日誌輸出的目標指定為文件 destination: file #mo ...
  • 下載安裝包 下載地址:https://www.mongodb.com/download-center/community curl -O https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-rhel70-4.2.5.tgz 實驗步驟 解壓Mon ...
  • 表結構: 方法1: select a.* from Table_Test as a where 3 > (select count(*) from Table_Test where Brand_Id = a.Brand_Id and AddTime > a.AddTime ) order by a. ...
  • 背景 最近在負責公司數據Oracle轉PG;老平臺資料庫:Oracle11g;新平臺資料庫:PostgreSQL12。由於平臺統計規則有變動;所以正在推廣的游戲數據無法全部遷移過來;只能在老平臺上運行。而支付數據介面升級;統一進入新平臺數據PG。需要將部分支付數據由PostgreSQL同步到Orac ...
  • Apache Kylin on Apache HBase 方案經過長時間的發展已經比較成熟,但是存在著一定的局限性。因此,Kyligence 推出了 Kylin on Parquet 方案。本文中,Kyligence 的大數據研發工程師王汝鵬講解了該解決方案的架構、原理以及如何開發調試代碼。 ...
  • 實時數據分析門檻較高,我們如何用極少的開發工作就完成實時數據平臺的搭建,做出炫酷的圖表呢? 如何快速的搭建實時數據分析平臺,首先我們需要實時數據的接入端,我們選擇高擴展性、容錯性、速度極快的消息系統Kafka,而實時數據倉庫,由於 Druid提供了非常方便快捷配置方式,如果不想編寫負責的Flink和 ...
  • 1.MongoDB 如何備份? >[root@service bin]# mongodump 備份數據 進行備份之後,如果你沒指定備份目錄會在當前目錄自動生成dump備份文件 數據備份成功效果圖 當前目錄多了個copy文件,也就是我們剛備份時候的庫名,用來備份數據 2.MongoDB如何做恢復? m ...
  • 首先,介紹一個Markdown 的編輯器,編輯後可以直接渲染出效果,有需要的可以自行下載。 Typora 官網:https://typora.io/ 一 Markdown 標題 1.使用 = 和 標記一級和二級標題 2.使用 號標記 使用 號可表示 1 6 級標題,一級標題對應一個 號,二級標題對應 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...