Objective-C:"格式字符串不是字符串文字(可能不安全)"用宏警告

div*_*ges 5 string macros objective-c ios

我正在使用宏来简化返回本地化字符串,如下所示:

#define GetLocalStr(key, ...) \
    [NSString stringWithFormat:[[NSBundle mainBundle] localizedStringForKey:key value:@"" table:nil], ##__VA_ARGS__]
Run Code Online (Sandbox Code Playgroud)

基本上,如果您在本地化字符串文件中有一个条目,如"name" = "My name is %@";调用

GetLocalStr( @"name", @"Foo" );
Run Code Online (Sandbox Code Playgroud)

将返回NSString @"My name is Foo"

然而,当我运行它时,如:

NSString * str = GetLocalStr( @"name", @"Foo" );
Run Code Online (Sandbox Code Playgroud)

我得到"格式字符串不是字符串文字"警告.即使遵循关于此警告的其他答案的建议并将其替换为:

NSString * str = [NSString stringWithFormat:@"%@", GetLocalStr( @"name", @"Foo" )];
Run Code Online (Sandbox Code Playgroud)

我仍然得到警告,此外,它有点打破宏观使生活更容易的点.

如何摆脱GetLocalStr#pragma suppressors中包含所有调用的警告?

编辑27/08

在通过CRD的答案并进行更多测试之后,似乎我对错误做了一个错误的假设.澄清:

本地化字符串文件:

"TestNoArgs" = "Hello world";
"TestArgs" = "Hello world %@";
Run Code Online (Sandbox Code Playgroud)

码:

NSString * str1 = GetLocalStr( @"TestNoArgs" ); // gives warning
NSString * str2 = GetLocalStr( @"TestArgs", @"Foo" ); // doesn't give warning
Run Code Online (Sandbox Code Playgroud)

我的大部分翻译都没有参数,而那些是提供警告的,但在我阅读CRD的答案之前我没有建立联系.

我把我的单个宏改为两个,就像这样:

#define GetLocalStrNoArgs(key) \
    [[NSBundle mainBundle] localizedStringForKey:key value:@"" table:nil]

#define GetLocalStrArgs(key, ...) \
    [NSString stringWithFormat:[[NSBundle mainBundle] localizedStringForKey:key value:@"" table:nil], ##__VA_ARGS__]
Run Code Online (Sandbox Code Playgroud)

如果我分别给每个人打电话,那就没有警告了.

我想GetLocalStr扩展到任何一个GetLocalStrNoArgsGetLocalStrArgs取决于是否传递任何参数,但到目前为止我一直没有运气(宏不是我的强项:D).

sizeof(#__VA_ARGS__)用来确定是否传递了任何参数 - 它对参数进行了字符串化,如果大小为1,则它是空的(即"\ 0").也许这不是最理想的方法,但似乎有效.

如果我将我的GetLocalStr宏重写为:

#define GetLocalStr(key,...) (sizeof(#__VA_ARGS__) == 1) ? GetLocalStrNoArgs(key) : GetLocalStrArgs(key,##__VA_ARGS__)
Run Code Online (Sandbox Code Playgroud)

我可以使用它,但是我仍然会在它使用的任何地方得到警告,并且没有传递任何参数,而类似的东西

#define GetLocalStr( key,...)               \
    #if ( sizeof(#__VA_ARGS__) == 1 )       \
        GetLocalStrNoArgs(key)              \
    #else                                   \
        GetLocalStrArgs(key,##__VA_ARGS__)
Run Code Online (Sandbox Code Playgroud)

不会编译.如何让我的GetLocalStr宏正确扩展?

CRD*_*CRD 5

Clang和GCC编译器检查格式字符串和提供的参数是否一致,如果格式字符串不是文字,则它们不能这样做 - 因此,当您从包中获取格式字符串时,您会看到错误消息.

为了解决这个问题,有一个属性format_arg(n)(docs)来标记带有格式字符串的函数; 在改变实际格式说明符的情况下以某种方式改变它,例如翻译它; 然后返回它.Cocoa NS_FORMAT_ARG(n)为此属性提供了方便的宏.

要解决您的问题,您需要做两件事:

  1. NSBundle在指定此属性的函数中结束调用; 和

  2. 更改"密钥"以包含格式说明符.

其次,您的字符串文件应包含:

"name %@" = "My name is %@"
Run Code Online (Sandbox Code Playgroud)

所以键具有与结果相同的格式说明符(如果需要重新排序特定语言的说明符,则使用位置格式说明符).

现在定义一个简单的函数来进行查找,将其归为格式转换函数.注意我们将其标记为static inline,使用宏NS_INLINE作为编译器的提示,将其内联到宏扩展中; 将static允许你把它列入无符号冲突多个文件:

NS_INLINE NSString *localize(NSString *string) NS_FORMAT_ARGUMENT(1);
NSString *localize(NSString *string)
{
   return [[NSBundle mainBundle] localizedStringForKey:string value:@"" table:nil];
}
Run Code Online (Sandbox Code Playgroud)

你的宏变成了:

#define GetLocalStr(key, ...) [NSString stringWithFormat:localize(key), ##__VA_ARGS__]
Run Code Online (Sandbox Code Playgroud)

现在当你:

GetLocalStr(@"name %@", @"Foo")
Run Code Online (Sandbox Code Playgroud)

您将获得本地化格式字符串和格式检查.

更新

在Greg的评论之后我回去检查了 - 我已经重现了你的错误,因此认为它归结为一个缺失的属性.但是正如Greg指出的那样localizedStringForKey:value:table:已经有了属性,为什么会出错?我在重现你的错误时心不在焉的是:

NSLog( GetLocalStr( @"name %@", @"Foo" ) );
Run Code Online (Sandbox Code Playgroud)

并且编译器指向定义而不是那一行 - 我应该发现编译器误导了我.

那么你离开了哪里?也许你做过类似的事情?关键是格式字符串必须是文字或属于格式转换函数的函数/方法的结果.不要忘记,您还必须具有上述密钥的格式说明符.

更新2

在您的附加注释之后,您需要使用的是函数,而不是宏,以及formatCocoa为其提供方便NS_FORMAT_FUNCTION(f,a)宏的属性.此属性通知编译器该函数是格式化函数,值f是格式字符串的编号,是格式a的第一个参数的编号.这给出了函数声明:

NSString *GetLocalStr(NSString *key, ...) NS_FORMAT_FUNCTION(1,2);
Run Code Online (Sandbox Code Playgroud)

和定义(假设ARC):

NSString *GetLocalStr(NSString *key, ...)
{
   va_list args;
   va_start(args, key);
   NSString *format = [[NSBundle mainBundle] localizedStringForKey:key value:@"" table:nil];
   NSString *result = [[NSString alloc] initWithFormat:format arguments:args];
   va_end (args);
   return result;
}
Run Code Online (Sandbox Code Playgroud)

(这与@ A-Live的基本相同).

将适当检查其使用情况,例如:

int x;
...
NSString *s1 = GetLocalStr(@"name = %d", x); // OK
NSString *s2 = GetLocalStr(@"name = %d");    // compile warning - More '%" conversions than data arguments
NSString *s3 = GetLocalStr(@"name", x);      // compile warning - Data argument not used by format string
NSString *s4 = GetLocalStr(@"name");         // OK
Run Code Online (Sandbox Code Playgroud)


A-L*_*ive 5

此变体不会产生警告(因为总是有va_list):

NSString* GetLocalStr1(NSString *formatKey, ...)  {
    va_list ap;
    va_start(ap, formatKey);
    NSString * format = [[NSBundle mainBundle] localizedStringForKey:formatKey value:@"" table:nil];
    NSString *result =  [[NSString alloc] initWithFormat:format arguments:ap];
    va_end (ap);
    return [result autorelease];
}

...

__unused NSString * str = GetLocalStr1( @"name", @"Foo" );
__unused NSString * str1 = GetLocalStr1( @"TestNoArgs" );
__unused NSString * str2 = GetLocalStr1( @"TestArgs", @"Foo" );

NSLog(@"%@", str);
NSLog(@"%@", str1);
NSLog(@"%@", str2);
Run Code Online (Sandbox Code Playgroud)

结果:

我叫Foo

TestNoArgs

你好世界Foo

它没有准确回答问题,但可能会帮助您避免警告,直到找到解决方案.