解决NSString stringWithFormat:参数nil时返回(null)
# 前言
NSString 这个类,在 OC 中很常见,[NSString stringWithFormat:]
这个方法我们都很熟悉了。但是也有很常见的一个问题,那就是参数为空的时候会返回@"(null)"
。
举个例子
label.text = [NSString stringWithFormat:@"所在医院 : %@", self.doctor.hospital];
当 doctor.hospital 为 nil 时,显示“所在医院 : (null)”,理所当然 (null) 这样的字眼不应该出现在界面上被用户看见。
# 解决
# 方法一、使用宏
# 定义一个宏,写到PCH文件中
#define DKNonnullString(str) ((str && [str isKindOfClass:[NSString class]] && str.length) ? str : @"")
# 可变参数每一个都包上这个宏
label.text = [NSString stringWithFormat:@"所在医院 : %@", DKNonnullString(self.doctor.hospital)];
# 原理
先判断参数是否为 nil,如果是,就把它替换为空字符串,再进行 format。
# 缺点
虽然可以实现,但这样子每次调用的时候每个参数都要包一个宏,感觉很麻烦!网上逛了一圈包括 stackoverflow 有人提到都是这样子解决的。程序猿是无法忍受这种重复累赘的,至少我是这样子的,那有没有更酷的办法呢?
# 方法二、使用分类
跳到头文件 NSString.h 中,可以看到与之相关的方法。
+ (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);
2
3
首先需要了解 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
/**
格式化字符串,并过滤格式化后的字符串中的(null)
@param format 格式
@return 格式化后,过滤掉@"(null)"的字符串
*/
+ (instancetype)dk_stringWithFormat:(NSString *)format, ... NS_FORMAT_FUNCTION(1,2);
2
3
4
5
6
7
# NSString+DKExtension.m
+ (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;
}
2
3
4
5
6
7
8
9
10
11
12
参数说明
方法 | 说明 |
---|---|
NS_FORMAT_FUNCTION(1, 2) | 告诉编译器,索引1处的参数是一个格式化字符串,而实际参数从索引2处开始 |
va_list | 定义一个指向个数可变的参数列表的指针,这个参数列表指针就是 arglist |
va_start | 使参数列表指针指向 format,从 format 的下一个元素开始 |
va_end | 结束,清空 va_list 可变参数列表 |
调用
NSString *nilStr = [NSString stringWithFormat:@"过滤前的[nil] -> [%@] ",nil];
NSLog(@"%@",nilStr);
NSString *nonullStr = [NSString dk_stringWithFormat:@"过滤后的[nil] -> [%@] ",nil];
NSLog(@"%@",nonullStr);
2
3
4
5
打印
2017-01-05 00:25:19.486 DKExtensionExample[82935:4502309] 过滤前的[nil] -> [(null)]
2017-01-05 00:25:19.487 DKExtensionExample[82935:4502309] 过滤后的[nil] -> []
2
# 原理
添加一个方法,使用 C 语言提供的处理变长参数的方法获取参数列表,调用 initWithFormat:arguments: 方法,把格式化后的字符串在 return 前做过滤处理,把@"(null)"
全部替换成@""
。
# 头脑风暴
原本我有个想法,是在 NSString 的分类中直接重写 stringWithFormat:,即下面的方法三,原因有两个。
- 在分类中重写系统的方法,无需引入分类文件,load 完毕就生效。
- 调用方式不需要改,依然是
[NSString stringWithFormat:]
,不用改变习惯。
# 方法三、在 NSString 的分类中直接重写 stringWithFormat:(不推荐)
NSString+DKExtension.m
/**
* 重写系统方法,替换返回的字符串中的 (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;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
在 Demo 中测试没有问题,但引入到实际项目后发现,在某个很正常的界面就崩溃了。
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
...
2
3
4
5
6
7
8
9
10
除了我们手动调用这个方法,系统自己也会调用这个方法,而且频率是极高的。分析一下异常信息,无效参数异常,原因应该是系统调用某个方法的时候需要一个参数是可变的字符串 NSMutableString, 而我们重写的方法中的 replaceOccurrencesOfString:withString:options:range:
返回了一个不可变的 NSString。
知道原因后,稍做修改。
+ (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;
}
2
3
4
5
6
7
8
9
10
11
然后跟往常一样调用 [NSString stringWithFormat:]
即可。
也许有人会说,重写了系统的方法,会不会带来其它地方的未知问题。我个人觉得还好吧,虽然重写了 stringWithFormat:
这个方法把返回的字符串中的 @"(null)" 过滤掉了,但实际上还是调用系统的另一个方法 initWithFormat:arguments:
。
思前想后,最终我还是放弃了这个解决方案,为什么?
正如上面所说的,系统自己也会调用这个方法,比如下面几个常见的系统调用 format 后的字符串。
[] -firstOrDefault: (null) success:error:
ClearButton_state:0_variant:0(null)
37.000000-12-1-UIExtendedGrayColorSpace 1 1-(null)-{0, 0}-CW-OutlineShadowOFF
2
3
4
5
系统这是在干嘛就不管它了,但说不定苹果还对某些返回的含有@"(null)"的字符串进行了一系列处理呢?比如拿到@"(null)"的 Range,或者判断某个参数 format 后是不是为@"(null)",然后可能做相对应的处理(纯粹猜想)。如果被我们过滤掉了,也许真的会打乱系统的逻辑导致出现一些莫名其妙的 bug。(暂时没遇到过,如果真想这么玩,需要广大 Coder 在不同项目不同环境下进行检验,目前没有这个人力资本)
所以,我还是决定不去重写系统的方法,就给分类加多一个方法,在我们需要的时候去调用。相对于第一种用宏对每一个参数进行判断过滤的方法来说,我是完全可以接受的。
# 补充
如果是在 MRC 下,再调用 autorelease 方法即可。
NSString *outStr = [[[NSString alloc] initWithFormat:formatStr arguments:arglist] autorelease];
# 后话
我们整理了开发中常用的分类 DKExtension,里面已经包含了上面所说的内容,欢迎导入我们的分类库进行开发。
- 支持 Cocoapods
pod 'DKExtension.h'
1 - Download or Usage 请移步 GitHub, 如果有什么更好的解决方案或者建议,欢迎 GitHub Issues。