2017-10-17 67 views
2

我使用以下模式爲iOS庫編寫了一個具有c函數的Helper類。 有2個包裝(可變參數)函數,最終調用相同的函數,參數略有不同。想法是設置「默認」屬性。然後Variadic函數緩存最後調用的參數列表

__attribute__((overloadable)) void func1(NSString* _Nonnull format, ...); 
__attribute__((overloadable)) void func1(int param1, NSString* _Nonnull format, ...); 

雙方將調用下面的函數:

void prefixAndArguments(int param1, NSString* _Nonnull format, va_list arguments); 

實現如下:

__attribute__((overloadable)) void func1(NSString* _Nonnull format, ...) 
{ 
    va_list argList; 
    va_start(argList, format); 
    prefixAndArguments(0, format, argList); 
    va_end(argList); 
} 

__attribute__((overloadable)) void func1(int param1, NSString* _Nonnull format, ...) 
{ 
    va_list argList; 
    va_start(argList, format); 
    prefixAndArguments(param1, format, argList); 
    va_end(argList); 
} 


void prefixAndArguments(NMXLogLevelType logLevel, NSString* _Nullable logPrefix, __strong NSString* _Nonnull format, va_list arguments) 
{ 
    // Evaluate input parameters 
    if (format != nil && [format isKindOfClass:[NSString class]]) 
    { 
     // Get a reference to the arguments that follow the format parameter 
     va_list argList; 
     va_copy(argList, arguments); 

     int argCount = 0; 
     NSLog(@"%d",argCount); 
     while (va_arg(argList, NSObject *)) 
     { 
      argCount += 1; 
     } 
     NSLog(@"%d",argCount); 
     va_end(argList); 

     NSMutableString *s; 
     if (numSpecifiers > argCount) 
     { 
      // Perform format string argument substitution, reinstate %% escapes, then print 
      NSString *debugOutput = [[NSString alloc] initWithFormat:@"Error occured when logging: amount of arguments does not for to the defined format. Callstack:\n%@\n", [NSThread callStackSymbols]]; 
      printf("%s\n", [debugOutput UTF8String]); 
      s = [[NSMutableString alloc] initWithString:format]; 
     } 
     else 
     { 
      // Perform format string argument substitution, reinstate %% escapes, then print 
      va_copy(argList, arguments); 

      // This is were the EXC_BAD_ACCESS will occur! 
      // Error: Thread 1: EXC_BAD_ACCESS (code=EXC_I386_GPFLT) 
      s = [[NSMutableString alloc] initWithFormat:format arguments:argList]; 
      [s replaceOccurrencesOfString:@"%%" 
           withString:@"%%%%" 
            options:0 
            range:NSMakeRange(0, [s length])]; 
      NSLog(@"%@",s); 
      va_end(argList); 
     } 
    ... 
} 

的功能我的單元測試看看以下(順序很重要)。

// .. some previous cases, I commented out 
XCTAssertNoThrow(NMXLog(@"Simple string output")); 
XCTAssertNoThrow(NMXLog(@"2 Placeholders. 0 Vars %@ --- %@")); 

的崩潰發生時,我想使用的參數和格式(製作格式強並沒有解決這個問題,似乎並沒有成爲問題的一部分,見下文):

s = [[NSMutableString alloc] initWithFormat:format arguments:argList]; 

下面是日誌:

xctest[28082:1424378] 0 
xctest[28082:1424378] --> 1 
xctest[28082:1424378] Simple string output 
xctest[28082:1424378] 0 
xctest[28082:1424378] --> 4 

當然,我們不會看到所需的字符串作爲"2 Placeholders. 0 Vars %@ --- %@"在飛機墜毀前發生的事情。

所以,現在的問題是:爲什麼參數的數量現在是4而不是0?由於沒有在第二次調用中傳遞,函數被立即再次調用時是否收集了參數?

於是,我開始爲「一次」調用的函數,以確保參數的列表被清除,儘管va_end是被稱爲:

__attribute__((overloadable)) void func1(NSString* _Nonnull format, ...) 
{ 
    va_list argList; 
    va_start(argList, format); 
    prefixAndArguments(none, nil, format, argList); 
    va_end(argList); 
    NSString *obj = nil; 
    prefixAndArguments(none, nil, obj, nil); 
} 

這不現在的工作就像一個魅力(參數的列表是清除並正在接收所需的輸出):

xctest[28411:1453508] 0 
xctest[28411:1453508] --> 1 
xctest[28411:1453508] Simple string output 
xctest[28411:1453508] 0 
xctest[28411:1453508] --> 1 
Error occured when logging: amount of arguments does not for to the defined format. Callstack: .... 
xctest[28411:1453508] 2 Placeholders. 0 Vars %@ --- %@ 

這裏是我的最後一個問題:

這種行爲的原因是什麼?我該如何避免這種行爲?有沒有更好的方法來解決這個問題,而不是「愚蠢地」第二次用「無」參數調用函數來清除它們? P.s.我試圖不使用宏,因爲我認爲它們比c函數更容易出錯。看到這個線程:Macro vs Function in C

+1

問問你自己:如何'va_arg'知道什麼時候停止? –

+0

這似乎是通過實施可選的第一個參數來解決很多麻煩的問題。如何,而不是。 –

+0

@JohnBollinger謝謝你在約翰。爲了簡單起見,我剛剛提到了兩個帶有一個可選參數的函數。當然,還有更多。 – Lepidopteron

回答

2

你似乎有關於可變參數的功能,通過這種方法例證一些誤解,以計數的變量參數:

 while (va_arg(argList, NSObject *)) 
     { 
      argCount += 1; 
     } 

該代碼假定可變參數至少有一個成員,它們都是NSObject *類型,並且該列表將由該類型的空指針終止。這些都不是由系統保證的,如果這些假設不滿足,那麼一個或多個調用的行爲將是不確定的。

在實際應用中,你也許可以矇混過關是其他類型的指針(儘管形式上,該行爲仍然會在這種情況下,未定義)的實際參數。但是,如果參數可能有非指針類型,那麼對它們進行計數的方法就完全失效了。更重要的是,你的測試用例似乎認爲系統將提供尾隨NULL的說法,但那是在沒有辦法保證。

如果您的函數依賴於由NULL參數發送信號的變量參數列表的末尾,那麼它將依靠調用者提供一個。在您的參數列表中很可能沒有空終止引起您所問的行爲。

+0

謝謝約翰澄清。這個函數應該在一個庫中使用,因此我想確保用戶不會崩潰他們的代碼,如果他們沒有附加預期的sentinel值。在調用包裝函數'prefixAndArguments'之前,可以在參數的列表中追加一個這樣的參數嗎? – Lepidopteron

+0

不,@Lepidopteron,不能。但是,可以想象,爲包裝真實函數的用戶提供類似於函數的*宏,並將'(NSObject *)NULL'附加到用戶的參數列表中。但是,如果傳遞了非指針類型的可變參數,那麼它仍然無法保護您免受不當行爲的影響。變量功能給用戶帶來實質性的責任以傳遞適當的論據;編譯器無法檢查它們。 –

+1

@Lepidopteron如果使用宏'NS_REQUIRES_NIL_TERMINATION'未正確終止列表,則可能會收到編譯器警告。但是,正如約翰指出的那樣,**只有在所有參數都是指針類型**時纔有效。如果你期望原始數據被傳遞(看起來很可能),你必須獲得要讀取的項目數。這可以通過計算函數中的格式說明符來實現(這就是'printf' /'stringWithFormat:'做到這一點),或者通過添加一個明確的計數參數。 –

相關問題