在每一个应用里咱们都大量使用字符串。下面咱们将快速看看一些常见的操做字符串的方法,过一遍常见操做的最佳实践。html
排序和比较字符串比第一眼看上去要复杂得多。不仅是由于字符串能够包含代理对(surrogate pairs )(详见 Ole 写的这篇关于 Unicode 的文章) ,并且比较还与字符串的本地化相关。在某些极端状况下至关棘手。ios
苹果文档中 String Programming Guide 里有一节叫作 “字符与字形集群(Characters and Grapheme Clusters)”,里面提到一些陷阱。例如对于排序来讲,一些欧洲语言将序列“ch”看成单个字母。在一些语言里,“ä”被认为等同于 ‘a’ ,而在其它语言里它却被排在 ‘z’ 后面。正则表达式
而 NSString 有一些方法来帮助咱们处理这种复杂性。首先看下面的方法:编程
- (NSComparisonResult)compare:(NSString *)aString options:(NSStringCompareOptions)mask range:(NSRange)range locale:(id)locale
它带给咱们充分的灵活性。另外,还有不少“便捷函数”都使用了这个方法。数组
与比较有关的可用参数以下:缓存
NSCaseInsensitiveSearch NSLiteralSearch NSNumericSearch NSDiacriticInsensitiveSearch NSWidthInsensitiveSearch NSForcedOrderingSearch
它们均可以用逻辑或运算组合在一块儿。app
NSCaseInsensitiveSearch :“A”等同于“a”,然而在某些地方还有更复杂的状况。例如,在德国,“ß” 和 “SS”是等价的。编程语言
NSLiteralSearch :Unicode 的点对 Unicode 点比较。它只在全部字符都用相同的方式组成的状况下才会返回相等。LATIN CAPITAL LETTER A 加上 COMBINING RING ABOVE 并不等同于 LATIN CAPITAL LETTER A WITH RING ABOVE.ide
译注:这个要解释一下,首先,每个Unicode都是有官方名字的!LATIN CAPITAL > LETTER A是一个大写“A”,COMBINING RING ABOVE是一个 ̊,LATIN CAPITAL > LETTER A WITH RING ABOVE,这是Å前二者的组合不等同于后者。函数
NSNumericSearch:它对字符串里的数字排序,因此 “Section 9” \< “Section 20” \< “Section 100.”
NSDiacriticInsensitiveSearch : “A” 等同于 “Å” 等同于 “Ä.”
NSWidthInsensitiveSearch : 一些东亚文字(平假名 和 片假名)有全宽与半宽两种形式。
很值得一提的是 - (NSComparisonResult)localizedStandardCompare: ,它排序的方式和 Finder 同样。它对应的选项是 NSCaseInsensitiveSearch 、 NSNumericSearch 、NSWidthInsensitiveSearch 以及 NSForcedOrderingSearch 。若是咱们要在UI上显示一个文件列表,用它就最合适不过了。
大小写不敏感的比较和音调符号不敏感的比较都是相对复杂和昂贵的操做。若是咱们须要比较不少次字符串那这就会成为一个性能上的瓶颈(例如对一个大的数据集进行排序),一个常见的解决方法是同时存储原始字符串和折叠字符串。例如,咱们的 Contact 类有一个正常的 name 属性,在内部它还有一个foldedName 属性,它将自动在 name变化时更新。那么咱们就可使用 NSLiteralSearch 来比较 name 的折叠版本。 NSString 有一个方法来建立折叠版本:
- (NSString *)stringByFoldingWithOptions:(NSStringCompareOptions)options locale:(NSLocale *)locale
要在一个字符串中搜索子字符串,最灵活性的方法是:
- (NSRange)rangeOfString:(NSString *)aString options:(NSStringCompareOptions)mask range:(NSRange)searchRange locale:(NSLocale *)locale
同时,还有一些“便捷方法”,它们在最终都会调用上面这个方法,咱们能够传入上面列出的参数,以及如下这些额外的参数:
NSBackwardsSearch NSAnchoredSearch NSRegularExpressionSearch
NSBackwardsSearch :在字符串的末尾开始反向搜索。
NSAnchoredSearch : 只考虑搜索的起始点(单独使用)或终止点(当与 NSBackwardsSearch 结合使用时)。这个方法能够用来检查前缀或者后缀,以及大小写不敏感(case-insensitive)或者音调不敏感(diacritic-insensitive)的比较。
NSRegularExpressionSearch :使用正则表达式搜索,要了解更多与使用正则表达式有关的信息,请关注 Chris’s 的 String Parsing 。
另外,还有一个方法:
- (NSRange)rangeOfCharacterFromSet:(NSCharacterSet *)aSet options:(NSStringCompareOptions)mask range:(NSRange)aRange
与前面搜索字符串不一样的是, 它只搜索给定字符集的第一个字符。即便只搜索一个字符,但若是因为此字符是由元字符组成的序列(composed character sequence),因此返回范围的长度也可能大于1。
必定不要使用 NSString 的 -uppercaseString 或者 -lowercaseString 的方法来处理 UI 显示的字符串,而应该使用 -uppercaseStringWithLocale 来代替, 好比:
NSString *name = @"Tómas"; cell.text = [name uppercaseStringWithLocale:[NSLocale currentLocale]];
同C语言中的 sprintf 函数( ANSI C89 中的一个函数 )相似, Objective C 中的 NSString 类也有以下的3个方法:
-initWithFormat: -initWithFormat:arguments: +stringWithFormat:
须要注意这些格式化方法都是 非本地化 的 。因此这些方法获得的字符串是不能直接拿来显示在用户界面上的。若是须要本地化,那咱们须要使用下面这些方法:
-initWithFormat:locale: -initWithFormat:locale:arguments: +localizedStringWithFormat:
Florian 有一篇关于 字符串的本地化 的文章更详细地讨论了这个问题。
printf(3)的man页面有关于它如何格式化字符串的所有细节。除了所谓的转换格式(它以%字符开始),格式化字符串会被逐字复制:
double a = 25812.8074434; float b = 376.730313461; NSString *s = [NSString stringWithFormat:@"%g :: %g", a, b]; // "25812.8 :: 376.73"
咱们格式化了两个浮点数。注意单精度浮点数和双精度浮点数共同了一个转换格式。
除了来自 printf(3) 的转换规范,咱们还可使用 %@ 来输出一个对象。在对象描述那一节中有述,若是对象响应 -descriptionWithLocale: 方法,则调用它,不然调用 -description 。 %@ 被结果替换。
使用整形数字时,有些须要注意的细节。首先,有符号数(d和i)和无符号数(o、u、x和X)分别有转换规范。须要使用者选择具体的类型。
若是咱们使用的东西是 printf不知道的,咱们必需要作类型转换。 NSUInteger 正是这样一个例子,它在64位和32位平台上是不同的。下面的例子能够同时工做在32位和64位平台。
uint64_t p = 2305843009213693951; NSString *s = [NSString stringWithFormat:@"The ninth Mersenne prime is %llu", (unsigned long long) p]; // "The ninth Mersenne prime is 2305843009213693951"
Modifier d, i o, u, x, X -------------- --------------- ---------------------- hh signed char unsigned char h short unsigned short (none) int unsigned int l (ell) long unsigned long ll (ell ell) long long unsigned long long j intmax\_t uintmax\_t t ptrdiff\_t z size\_t
适用于整数的转换规则有:
int m = -150004021; uint n = 150004021U; NSString *s = [NSString stringWithFormat:@"d:%d i:%i o:%o u:%u x:%x X:%X", m, m, n, n, n, n]; // "d:-150004021 i:-150004021 o:1074160465 u:150004021 x:8f0e135 X:8F0E135"
%d 和 %i 具备同样的功能,它们都打印出有符号十进制数。 %o 就较为晦涩了:它使用八进制表示。 %u 输出无符号十进制数——它是咱们经常使用的。最后 %x 和 %X 使用十六进制表示——后者使用大写字母。
对于 x% 和 X% ,咱们能够在 0x 前面添加 “#” 井字符前缀看,增长可读性。
咱们能够传入特定参数,来设置最小字段宽度和最小数字位数(默认二者都是0),以及左/右对齐。请查看man页面获取详细信息。下面是一些例子:
int m = 42; NSString *s = [NSString stringWithFormat:@"'%4d' '%-4d' '%+4d' '%4.3d' '%04d'", m, m, m, m, m]; // ‘42’ ‘42 ’ ‘ +42’ ‘ 042’ ‘0042’ m = -42; NSString *s = [NSString stringWithFormat:@"'%4d' '%-4d' '%+4d' '%4.3d' '%04d'", m, m, m, m, m]; // ‘ -42’ ‘-42 ’ ‘ -42’ ‘-042’ ‘-042’
%p 可用于打印出指针——它和 %#x 类似但可同时在32位和64位平台上正常工做。
关于浮点数的转换规则有8个:eEfFgGaA。但除了 %f 和 %g 外咱们不多使用其它的。对于指数部分,小写的版本使用小写 e,大写的版本就使用大写 E。
一般 %g 是浮点数的全能转换符 ,它与 %f 的不一样在下面的例子里显示得很清楚:
double v[5] = {12345, 12, 0.12, 0.12345678901234, 0.0000012345678901234}; NSString *s = [NSString stringWithFormat:@"%g %g %g %g %g", v[0], v[1], v[2], v[3], v[4]]; // "12345 12 0.12 0.123457 1.23457e-06" NSString *s = [NSString stringWithFormat:@"%f %f %f %f %f", v[0], v[1], v[2], v[3], v[4]]; // "12345.000000 12.000000 0.120000 0.123457 0.000001"
和整数同样,咱们依然能够指定最小字段宽度和最小数字数。
格式化字符串容许使用参数来改变顺序:
[NSString stringWithFormat:@"%2$@ %1$@", @"1st", @"2nd"]; // "2nd 1st"
咱们只需将从1开始的参数与一个$
接在%后面。这种写法在进行本地化的时候极其常见,由于在不一样语言中,各个参数所处的顺序位置可能不尽相同。
NSLog() 函数与 +stringWithFormat: 的工做方式同样。咱们能够调用:
int magic = 42; NSLog(@"The answer is %d", magic);
下面的代码能够用一样的方式构造字符串:
int magic = 42; NSString *output = [NSString stringWithFormat:@"The answer is %d", magic];
显然 NSLog()会输出字符串,而且它会加上时间戳、进程名、进程ID以及线程ID做为前缀。
有时在咱们本身的类中提供一个能接受格式化字符串的方法会很方便使用。假设咱们要实现的是一个 To Do 应用,它包含一个 Item 类。咱们想要提供:
+ (instancetype)itemWithTitleFormat:(NSString *)format, ...
如此咱们就可使用:
Item *item = [Item itemWithFormat:@"Need to buy %@ for %@", food, pet];
这种类型的方法能够接受可变数量的参数,因此被称为可变参数方法。咱们必须使用一个定义在stdarg.h里的宏来使用可变参数。上面方法的实现代码可能会像下面这样:
+ (instancetype)itemWithTitleFormat:(NSString *)format, ...; { va_list ap; va_start(ap, format); NSString *title = [[NSString alloc] initWithFormat:format locale:[NSLocale currentLocale] arguments:ap]; va_end(ap); return [self itemWithTitle:title]; }
进一步,咱们要添加 NS_FORMAT_FUNCTION
到方法的定义里(在头文件中),以下所示:
+ (instancetype)itemWithTitleFormat:(NSString *)format, ... NS_FORMAT_FUNCTION(1,2);
NS_FORMAT_FUNCTION
展开为一个方法 __attribute__
,它会告诉编译器在索引1处的参数是一个格式化字符串,而实际参数从索引2开始。这将容许编译器检查格式化字符串并且会像 NSLog() 和 -[NSString stringWithFormat:] 同样输出警告信息。
若有一个字符串 “bird” ,找出组成它的独立字母是很简单的。第二个字母是“i”(Unicode: LATIN SMALL LETTER I)。而对于像Åse这样的字符串就没那么简单了。看起来像三个字母的组合可有多种方式,例如:
A LATIN CAPITAL LETTER A ̊ COMBINING RING ABOVE s LATIN SMALL LETTER S e LATIN SMALL LETTER E
或者
Å LATIN CAPITAL LETTER A WITH RING ABOVE s LATIN SMALL LETTER S e LATIN SMALL LETTER E
从 Ole 写的这篇关于 Unicode 的文章 里能够读到更多关于联合标记(combining marks)的信息,其余语言文字有更多复杂的代理对(complicated surrogate pairs)。
若是咱们要在字符层面处理一个字符串,那咱们就要当心翼翼。苹果官方文档中 String Programming Guide 有一节叫作 “Characters and Grapheme Clusters”,里面有更多关于这一点的细节。
NSString有两个方法:
-rangeOfComposedCharacterSequencesForRange: -rangeOfComposedCharacterSequenceAtIndex:
上面这两个方法在有的时候颇有帮助,例如,分开一个字符串时保证咱们不会分开被称为代理对(surrogate pairs)的东西。
若是咱们要在字符串的字符上作工做, NSString 有个叫作 -enumerateSubstringsInRange:options:usingBlock: 的方法。
将 NSStringEnumerationByComposedCharacterSequences 做为选项传递,咱们就能扫描全部的字符。例如,用下面的方法,咱们可将字符串 “International Business Machines” 变成 “IBM”。
- (NSString *)initials; { NSMutableString *result = [NSMutableString string]; [self enumerateSubstringsInRange:NSMakeRange(0, self.length) options:NSStringEnumerationByWords | NSStringEnumerationLocalized usingBlock:^(NSString *word, NSRange wordRange, NSRange enclosingWordRange, BOOL *stop1) { __block NSString *firstLetter = nil; [self enumerateSubstringsInRange:NSMakeRange(0, word.length) options:NSStringEnumerationByComposedCharacterSequences usingBlock:^(NSString *letter, NSRange letterRange, NSRange enclosingLetterRange, BOOL *stop2) { firstLetter = letter; *stop2 = YES; }]; if (letter != nil) { [result appendString:letter]; }; }]; return result; }
如文档所示,词和句的分界可能基于地区的变化而变化。所以有 NSStringEnumerationLocalized选项。
编译器的确有一个隐蔽的特性:把空格分隔开的字符串衔接到一块儿。这是什么意思呢?下面两段代码是彻底等价的:
NSString *limerick = @"A lively young damsel named Menzies\n" @"Inquired: «Do you know what this thenzies?»\n" @"Her aunt, with a gasp,\n" @"Replied: "It's a wasp,\n" @"And you're holding the end where the stenzies.\n";
NSString *limerick = @"A lively young damsel named Menzies\nInquired: «Do you know what this thenzies?»\nHer aunt, with a gasp,\nReplied: "It's a wasp,\nAnd you're holding the end where the stenzies.\n";
前者看起来更舒服,可是有一点要注意千万不要在任意一行末尾加入逗号或者分号。
同时也能够这样作:
NSString * string = @"The man " @"who knows everything " @"learns nothing" @".";
译者注:上面这行代码原文是有误的,原文是 NSString * @"The man " @"who knows everything " @"learns nothing" @"." ,读者能够尝试一下,若是这样写是没法经过编译的。
编译器只是为咱们提供了一个便捷的方式,将多个字符串在编译期组合在了一块儿。
可变字符串有两个常见的使用场景:(1)拼接字符串(2)替换部分字符串
可变字符串能够很轻易地把多个字符串在你须要的时候组合起来。
- (NSString *)magicToken { NSMutableString *string = [NSMutableString string]; if (usePrefix) { [string appendString:@">>>"]; } [string appendFormat:@"%d--%d", self.foo, self.bar]; if (useSuffix) { [string appendString:@">>>"]; } return string; }
这里要注意的是,虽然本来返回值应该是一个 NSString 类型的对象,咱们只是简单地返回一个NSMutableString 类型的对象。
可变字符串除了追加组合以外,还提供了如下4个方法:
-deleteCharactersInRange: -insertString:atIndex: -replaceCharactersInRange:withString: -replaceOccurrencesOfString:withString:options:range:
这些方法和 NSString 的相似:
-stringByReplacingOccurrencesOfString:withString: -stringByReplacingOccurrencesOfString:withString:options:range: -stringByReplacingCharactersInRange:withString:
可是它没有建立新的字符串仅仅把当前字符串变成了一个可变的类型,这样让代码更容易阅读,以及提高些许性能。
NSMutableString *string; // 假设咱们已经有了一个名为 string 的字符串 // 如今要去掉它的一个前缀,作法以下: NSString *prefix = @"WeDon’tWantThisPrefix" NSRange r = [string rangeOfString:prefix options:NSAnchoredSearch range:NSMakeRange(0, string.length) locale:nil]; if (r.location != NSNotFound) { [string deleteCharactersInRange:r]; }
一个看似微不足道但很常见的状况是字符串链接。好比如今有这样几个字符串:
Hildr Heidrun Gerd Guðrún Freya Nanna Siv Skaði Gróa
咱们想用它们来建立下面这样的一个字符串:
Hildr, Heidrun, Gerd, Guðrún, Freya, Nanna, Siv, Skaði, Gróa
那么就能够这样作:
NSArray *names = @["Hildr", @"Heidrun", @"Gerd", @"Guðrún", @"Freya", @"Nanna", @"Siv", @"Skaði", @"Gróa"]; NSString *result = [names componentsJoinedByString:@", "];
若是咱们将其显示给用户,咱们就要使用本地化表达,确保将最后一部分替换相应语言的 , and
:
@implementation NSArray (ObjcIO_GroupedComponents) - (NSString *)groupedComponentsWithLocale:(NSLocale *)locale; { if (self.count < 1) { return @""; } else if (self.count < 2) { return self[0]; } else if (self.count < 3) { NSString *joiner = NSLocalizedString(@"joiner.2components", @""); return [NSString stringWithFormat:@"%@%@%@", self[0], joiner, self[1]]; } else { NSString *joiner = [NSString stringWithFormat:@"%@ ", [locale objectForKey:NSLocaleGroupingSeparator]]; NSArray *first = [self subarrayWithRange:NSMakeRange(0, self.count - 1)]; NSMutableString *result = [NSMutableString stringWithString:[first componentsJoinedByString:joiner]]; NSString *lastJoiner = NSLocalizedString(@"joiner.3components", @""); [result appendString:lastJoiner]; [result appendString:self.lastObject]; return result; } } @end
那么在本地化的时候,若是是英语,应该是:
"joiner.2components" = " and "; "joiner.3components" = ", and ";
若是是德语,则应该是:
"joiner.2components" = " und "; "joiner.3components" = " und ";
结合组件的逆过程能够用 -componentsSeparatedByString: ,这个方法会将一个字符串变成一个数组。例如,将 “12|5|3” 变成 “12”、“5” 和 “3”。
在许多面向对象编程语言里,对象有一个叫作 toString() 或相似的方法。在 Objective C 里,这个方法是:
- (NSString *)description
以及它的兄弟方法:
- (NSString *)debugDescription
当自定义模型对象时,覆写 -description 方法是一个好习惯,在UI上显示该对象时调用的就是description方法的返回值。假定咱们有一个 Contact类,下面是它的 description方法实现。
- (NSString *)description { return self.name; }
咱们能够像下面代码这样格式化字符串:
label.text = [NSString stringWithFormat:NSLocalizedString(@"%@ has been added to the group “%@”.", @""), contact, group];
由于该字符串是用来作UI显示的,咱们可能须要作本地化,那么咱们就须要覆写descriptionWithLocale:(NSLocale *)locale
方法。
- (NSString *)descriptionWithLocale:(NSLocale *)locale;
%@ 会首先调用 -descriptionWithLocale,若是没有返回值,再调用 -description,在调试时,打印一个对象,咱们用 po这个命令(它是print object的缩写)
(lldb) po contact
若是在调试窗口的终端下输入 po contact, 它会调用对象的 debugDescription方法。默认状况下debugDescription是直接调用 description。若是你但愿输出不一样的信息,那么就分别覆写两个方法。大多数状况下,尤为是对于非数据模型的对象,你只须要覆写 -description就能知足需求了。
实际上对象的标准格式化输出是这样的:
- (NSString *)description; { return [NSString stringWithFormat:@"<%@: %p>", self.class, self]; }
NSObject就是这么干的。当你覆写该方法时,也能够像这样写。假定咱们有一个DetailViewController,在它的UI上要显示一个 contact ,咱们可能会这样覆写该方法:
- (NSString *)description; { return [NSString stringWithFormat:@"<%@: %p> contact = %@", self.class, self, self.contact.debugDescription]; }
咱们将特别注意向 NSManagedObject 的子类添加 -description / -debugDescription 的状况。因为 Core Data的惰性加载机制(faulting mechanism)容许未加载数据的对象存在,因此当咱们调用 -debugDescription 咱们并不但愿改变咱们的应用程序的状态,所以我要确保检查 isFault 这个属性。例如,咱们可以下这样实现它:
- (NSString *)debugDescription; { NSMutableString *description = [NSMutableString stringWithFormat:@"<%@: %p>", self.class, self]; if (! self.isFault) { [description appendFormat:@" %@ \"%@\" %gL", self.identifier, self.name, self.metricVolume]; } return description; }
再次,由于它们是模型对象,重载 -description 简单地返回描述实例的属性名就能够了。
简单来讲就是咱们不该该使用 NSString来描述文件路径。对于 OS X 10.7 和 iOS 5, NSURL更便于使用,并且更有效率,它还能缓存文件系统的属性。
再者, NSURL 有八个方法来访问被称为 resource values 的东西。它们提供给咱们一个稳定的接口来获取和设置文件与目录的多种属性,例如本地化文件名( NSURLLocalizedNameKey)、文件大小(NSURLFileSizeKey),以及建立日期( NSURLCreationDateKey),等等。
尤为是在遍历目录内容时,使用 -[NSFileManagerenumeratorAtURL:includingPropertiesForKeys:options:errorHandler:]
附带一个关键词列表,而后用 -getResourceValue:forKey:error: 检索它们,能带来显著的性能提高。
下面是一个简短的例子展现了如何将它们组合在一块儿:
NSError *error = nil; NSFileManager *fm = [[NSFileManager alloc] init]; NSURL *documents = [fm URLForDirectory:NSDocumentationDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:NO error:&error]; NSArray *properties = @[NSURLLocalizedNameKey, NSURLCreationDateKey]; NSDirectoryEnumerator *dirEnumerator = [fm enumeratorAtURL:documents includingPropertiesForKeys:properties options:0 errorHandler:nil]; for (NSURL *fileURL in dirEnumerator) { NSString *name = nil; NSDate *creationDate = nil; if ([fileURL getResourceValue:&name forKey:NSURLLocalizedNameKey error:NULL] && [fileURL getResourceValue:&creationDate forKey:NSURLCreationDateKey error:NULL]) { NSLog(@"'%@' was created at %@", name, creationDate); } }
咱们把属性的键传给 -enumeratorAtURL: 方法中,在遍历目录内容时,这个方法能确保用很是高效的方式获取它们。在循环中,调用 -getResourceValue:… 能简单地从 NSURL 获得已缓存的值,而不用去访问文件系统。
由于 Unicode 很是复杂,同一个字母有多种表示方式,因此咱们须要很当心地传递路径给UNIX API。在这些状况里,必定不能使用 UTF8String ,正确地作法是使用 -fileSystemRepresentation 方法,以下:
NSURL *documentURL = [[NSFileManager defaultManager] URLForDirectory:NSDocumentDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:NO error:NULL]; documentURL = [documentURL URLByAppendingPathComponent:name]; int fd = open(documentURL.fileSystemRepresentation, O_RDONLY);
与 NSURL 相似,一样的状况也发生在 NSString 上。若是咱们不这么作,在打开一个文件名或路径名包含合成字符的文件时咱们将看到随机错误。在 OS X 上,当用户的短名恰好包含合成字符时就会显得特别糟糕。
咱们须要一个 char const *
版本的路径的一些常见状况是UNIX open() 和 close() 指令。但这也可能发生在 GCD / libdispatch 的 I/O API 上。
dispatch_io_t dispatch_io_create_with_path(dispatch_io_type_t type, const char *path, int oflag, mode_t mode, dispatch_queue_t queue, void (^cleanup_handler)(int error));
若是咱们要使用 NSString 来作,那咱们要保证像下面这样作:
NSString *path = ... // 假设咱们已经有一个名为 path 的字符串 io = dispatch_io_create_with_path(DISPATCH_IO_STREAM, path.fileSystemRepresentation, O_RDONLY, 0, queue, cleanupHandler);
-fileSystemRepresentation 所作的是它首先将这个字符串转换成文件系统的规范形式而后用UTF-8编码。
原文 Working with Strings
翻译 iosinit
via 伯乐在线