解决NSString stringWithFormat:参数nil时返回(null)

前言

NSString 这个类,在 OC 中很常见,[NSString stringWithFormat:] 这个方法我们都很熟悉了。但是也有很常见的一个问题,那就是参数为空的时候会返回@"(null)"

举个例子

1
label.text = [NSString stringWithFormat:@"所在医院 : %@", self.doctor.hospital];

当 doctor.hospital 为 nil 时,显示“所在医院 : (null)”,理所当然 (null) 这样的字眼不应该出现在界面上被用户看见。

解决

方法一、使用宏

定义一个宏,写到PCH文件中

1
#define DKNonnullString(str) ((str && [str isKindOfClass:[NSString class]] && str.length) ? str : @"")

可变参数每一个都包上这个宏

1
label.text = [NSString stringWithFormat:@"所在医院 : %@", DKNonnullString(self.doctor.hospital)];

原理

先判断参数是否为 nil,如果是,就把它替换为空字符串,再进行 format。

缺点

虽然可以实现,但这样子每次调用的时候每个参数都要包一个宏,感觉很麻烦!网上逛了一圈包括 stackoverflow 有人提到都是这样子解决的。程序猿是无法忍受这种重复累赘的,至少我是这样子的,那有没有更酷的办法呢?

方法二、使用分类

跳到头文件 NSString.h 中,可以看到与之相关的方法。

1
2
3
+ (instancetype)stringWithFormat:(NSString *)format, ... NS_FORMAT_FUNCTION(1,2);
- (instancetype)initWithFormat:(NSString *)format, ... NS_FORMAT_FUNCTION(1,2);
- (instancetype)initWithFormat:(NSString *)format arguments:(va_list)argList NS_FORMAT_FUNCTION(1,0);

首先需要了解 initWithFormat: 和 stringWithFormat: 的区别

initWithFormat: stringWithFormat:
实例方法 类方法
非autoRelease,MRC下需要手动release autoRelease,推荐使用

在MRC下这两种方式生成的字符串在内存管理上面有点差异,现在的项目绝大部分都是ARC了,所以两者可以说是没有区别的,也就是两者之间可以互换!这是重点!

再来看看 arguments

可以看到参数类型是 va_list,这货一看就不像是 OC 的东西,应该是 C 语言或 C++的。网上一搜,果然跟猜想的一样。它的意思是参数列表,除了 va_list,还有 va_start、va_arg、va_end、NS_FORMAT_FUNCTION,都是 C 语言提供的处理变长参数的方法。

结合这两点,给 NSString 添加一个分类,添加 dk_stringWithFormat: 方法。

NSString+DKExtension.h

1
2
3
4
5
6
7
/**
格式化字符串,并过滤格式化后的字符串中的(null)
@param format 格式
@return 格式化后,过滤掉@"(null)"的字符串
*/
+ (instancetype)dk_stringWithFormat:(NSString *)format, ... NS_FORMAT_FUNCTION(1,2);

NSString+DKExtension.m

1
2
3
4
5
6
7
8
9
10
11
+ (instancetype)dk_stringWithFormat:(NSString *)format, ... NS_FORMAT_FUNCTION(1,2)
{
va_list arglist;
va_start(arglist, format);
NSString *outStr = [[NSString alloc] initWithFormat:format arguments:arglist];
va_end(arglist);
if ([outStr containsString:@"(null)"])
return [outStr stringByReplacingOccurrencesOfString:@"(null)" withString:@""];
return outStr;
}

参数说明

方法 说明
NS_FORMAT_FUNCTION(1, 2) 告诉编译器,索引1处的参数是一个格式化字符串,而实际参数从索引2处开始
va_list 定义一个指向个数可变的参数列表的指针,这个参数列表指针就是 arglist
va_start 使参数列表指针指向 format,从 format 的下一个元素开始
va_end 结束,清空 va_list 可变参数列表

调用

1
2
3
4
5
NSString *nilStr = [NSString stringWithFormat:@"过滤前的[nil] -> [%@] ",nil];
NSLog(@"%@",nilStr);
NSString *nonullStr = [NSString dk_stringWithFormat:@"过滤后的[nil] -> [%@] ",nil];
NSLog(@"%@",nonullStr);

打印

1
2
2017-01-05 00:25:19.486 DKExtensionExample[82935:4502309] 过滤前的[nil] -> [(null)]
2017-01-05 00:25:19.487 DKExtensionExample[82935:4502309] 过滤后的[nil] -> []

原理

添加一个方法,使用 C 语言提供的处理变长参数的方法获取参数列表,调用 initWithFormat:arguments: 方法,把格式化后的字符串在 return 前做过滤处理,把@"(null)"全部替换成@""

头脑风暴

原本我有个想法,是在 NSString 的分类中直接重写 stringWithFormat:,即下面的方法三,原因有两个。

  1. 在分类中重写系统的方法,无需引入分类文件,load 完毕就生效。
  2. 调用方式不需要改,依然是 [NSString stringWithFormat:],不用改变习惯。

方法三、在 NSString 的分类中直接重写 stringWithFormat:(不推荐)

NSString+DKExtension.m

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 重写系统方法,替换返回的字符串中的 (null) 为空字符串
* @"(null)" -> @""
*/
+ (instancetype)stringWithFormat:(NSString *)format, ...
{
va_list arglist;
va_start(arglist, format);
NSString *outStr = [[NSString alloc] initWithFormat:format arguments:arglist];
va_end(arglist);
if ([outStr containsString:@"(null)"])
return [outStr stringByReplacingOccurrencesOfString:@"(null)" withString:@""];
return outStr;
}

在 Demo 中测试没有问题,但引入到实际项目后发现,在某个很正常的界面就崩溃了。

1
2
3
4
5
6
7
8
9
10
2017-01-05 21:17:02.087 YouYun[85270:4528882] exceptionString:name:
NSInvalidArgumentException
reason:
Attempt to mutate immutable object with replaceOccurrencesOfString:withString:options:range:
callStackSymbols:
0 CoreFoundation 0x000000010f58634b __exceptionPreprocess + 171
1 libobjc.A.dylib 0x000000010e8b721e objc_exception_throw + 48
2 CoreFoundation 0x000000010f5ef265 +[NSException raise:format:] + 197
3 CoreFoundation 0x000000010f4ec91b -[__NSCFString replaceOccurrencesOfString:withString:options:range:] + 123
...

除了我们手动调用这个方法,系统自己也会调用这个方法,而且频率是极高的。分析一下异常信息,无效参数异常,原因应该是系统调用某个方法的时候需要一个参数是可变的字符串 NSMutableString, 而我们重写的方法中的 replaceOccurrencesOfString:withString:options:range: 返回了一个不可变的 NSString。

知道原因后,稍做修改。

1
2
3
4
5
6
7
8
9
10
11
+ (instancetype)stringWithFormat:(NSString *)format, ... NS_FORMAT_FUNCTION(1,2)
{
va_list arglist;
va_start(arglist, format);
NSMutableString *outStr = [[NSMutableString alloc] initWithFormat:format arguments:arglist];
va_end(arglist);
if ([outStr containsString:@"(null)"])
return [NSMutableString stringWithString:[outStr stringByReplacingOccurrencesOfString:@"(null)" withString:@""]];
return outStr;
}

然后跟往常一样调用 [NSString stringWithFormat:] 即可。

也许有人会说,重写了系统的方法,会不会带来其它地方的未知问题。我个人觉得还好吧,虽然重写了 stringWithFormat: 这个方法把返回的字符串中的 @”(null)” 过滤掉了,但实际上还是调用系统的另一个方法 initWithFormat:arguments:

思前想后,最终我还是放弃了这个解决方案,为什么?

正如上面所说的,系统自己也会调用这个方法,比如下面几个常见的系统调用 format 后的字符串。

1
2
3
4
5
[] -firstOrDefault: (null) success:error:
ClearButton_state:0_variant:0(null)
37.000000-12-1-UIExtendedGrayColorSpace 1 1-(null)-{0, 0}-CW-OutlineShadowOFF

系统这是在干嘛就不管它了,但说不定苹果还对某些返回的含有@”(null)”的字符串进行了一系列处理呢?比如拿到@”(null)”的 Range,或者判断某个参数 format 后是不是为@”(null)”,然后可能做相对应的处理(纯粹猜想)。如果被我们过滤掉了,也许真的会打乱系统的逻辑导致出现一些莫名其妙的 bug。(暂时没遇到过,如果真想这么玩,需要广大 Coder 在不同项目不同环境下进行检验,目前没有这个人力资本)

所以,我还是决定不去重写系统的方法,就给分类加多一个方法,在我们需要的时候去调用。相对于第一种用宏对每一个参数进行判断过滤的方法来说,我是完全可以接受的。

补充

如果是在 MRC 下,再调用 autorelease 方法即可。

1
NSString *outStr = [[[NSString alloc] initWithFormat:formatStr arguments:arglist] autorelease];

后话

我们整理了开发中常用的分类 DKExtension,里面已经包含了上面所说的内容,欢迎导入我们的分类库进行开发。

  • 支持 Cocoapods

    1
    pod 'DKExtension.h'
  • Download or Usage 请移步 GitHub, 如果有什么更好的解决方案或者建议,欢迎 GitHub Issues。

  • 本文作者: Bingo
  • 本文链接: https://blog.bingo.ren/2.html
  • 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!