首页 » iOS编程基础:Swift、Xcode和Cocoa入门指南 » iOS编程基础:Swift、Xcode和Cocoa入门指南全文在线阅读

《iOS编程基础:Swift、Xcode和Cocoa入门指南》10.4 Foundation类精讲

关灯直达底部

Cocoa的Foundation类提供了一些基本数据类型与辅助方法,它们构成了使用Cocoa的基础。显然,我无法将其一一列举,更不必说完整介绍它们了,但我可以介绍一些你在编写最简单的iOS程序前所需要了解的内容。要想了解更多信息,请从Foundation Framework Reference的Foundation类列表开始。

10.4.1 常用的结构体与常量

NSRange是个C结构体(参见附录A),它对于处理我将要介绍的一些类是非常重要的。其组成都是整数,location与length。比如,location为1的NSRange表示从第2个元素开始(因为元素计数总是基于0的),如果length为2,那就表示这个元素与下一个。

Cocoa提供了各种便捷函数来处理NSRange;比如,你可以调用NSMakeRange通过两个整数创建NSRange(注意到名字NSMakeRange向后类比于CGPointMake与CGRectMake等名字)。Swift通过将NSRange桥接为Swift结构体来解决遇到的问题。你可以对NSRange与Swift Range(端点为Int)进行转换:Swift为NSRange增加了一个初始化器,它接收一个Swift Range,另外还有一个toRange方法。

NSNotFound是个整型常量,表示找不到所请求的元素。NSNotFound真正的数值是什么并不重要;只要与NSNotFound本身进行比较即可,从而判断结果是否有意义。比如,如果获取某个对象在NSArray中的索引,但该对象不存在,那么结果就是个NSNotFound:


let arr = ["hey"] as NSArraylet ix = arr.indexOfObject("ho")if ix == NSNotFound {    print("it wasn't found")}  

Cocoa为何要以这种方式依赖于拥有特殊含义的整型值呢?这是因为它只能这么做。表示对象不存在的结果不能为0,因为0表示数组的第一个元素。结果也不能是-1,因为NSArray索引值总是正数。它也不能为nil,因为当需要返回一个整数时,Objective-C不能返回nil(即便可以,那它也会被看作0的另一种表示方式)。相反,Swift的indexOf方法会返回一个包装了Int的Optional,这样就可以返回nil来表示目标对象没有找到了。

如果搜索返回一个范围,但并没有找到任何结果,那么生成的NSRange的location就为NSNotFound。第3章曾经介绍过,Swift有时会自动帮你做一些聪明且自动化的桥接工作,从而无需再与NSNotFound进行比较了。典型示例就是NSString的rangeOfString:方法。在Cocoa的定义中,它会返回一个NSRange;Swift则将其改造为返回一个包装了Swift Range(String.Index)的Optional,如果NSRange的location为NSNotFound,那就会返回nil:


let s = "hello"let r = s.rangeOfString("ha") // nil; an Optional wrapping a Swift Range  

如果你需要的是个Swift Range,那就正合适,适合于进一步切割Swift String;但如果需要的是个NSRange,想要返回给Cocoa,那就需要将原来的Swift String转换为NSString,这样结果依然是Cocoa类:


let s = " hello" as NSStringlet r = s.rangeOfString("ha") // an NSRangeif r.location == NSNotFound {    print("it wasn't found")}  

10.4.2  NSString及相关类

NSString是字符串的Cocoa对象版本。NSString与Swift String会彼此桥接,你常常会不自觉地在这两者间切换,需要NSString时就将Swift String传递给Cocoa,在Swift String上调用Cocoa NSString方法,诸如此类。比如:


let s = "hello"let s2 = s.capitalizedString  

在上述代码中,s是个Swift String,s2也是个Swift String,但capitalizedString属性实际上是Cocoa的。在执行上述代码时,Swift String会被桥接到NSString并传递给Cocoa,Cocoa则会处理它并得到大写的字符串;这个大写的字符串是个NSString,不过它可以桥接到Swift String。你基本意识不到桥接过程;capitalizedString就像是原生String的属性一样,不过它并不是,可以在没有导入Foundation的环境下使用它来证明这一点(其实是不行的)。

在某些情况下,你需要进行显式类型转换来桥接。Swift可能会在桥接时失败;比如,如果s是个Swift字符串,那你就不能直接对其调用stringByAppendingPathExtension::


let s = "MyFile"let s2 = s.stringByAppendingPathExtension("txt") // compile error  

你需要显式将其转换为NSString:


let s2 = (s as NSString).stringByAppendingPathExtension("txt")  

此外,对字符串使用索引时会出现问题。比如:


let s = "hello"let s2 = s.substringToIndex(4) // compile error  

问题在于桥接是你自己做的。Swift并不会阻止你在Swift String上调用substring-ToIndex:方法,不过索引值必须是个String.Index,这很难构建(参见第3章):


let s2 = s.substringToIndex(s.startIndex.advancedBy(4))  

如果不想这么做,那就需要提前将String强制类型转换为NSString;现在处理的都是Cocoa了,字符串索引是整型值:


let s2 = (s as NSString).substringToIndex(4)  

不过,正如第3章所介绍的那样,这两个调用实际上并不是等价的:其结果是不同的!原因在于从根本上来说,String与NSString在字符串的元素构成上拥有完全不同的表示方式。String会将元素解析为字符,这意味着它会遍历字符串,将任何可合并的代码点聚合起来;NSString的行为就好像它是个UTF16代码点的数组。从Swift的角度来看,String.Index的增加都对应于真正的字符,不过通过索引或范围访问却需要遍历字符串;从Cocoa的角度来看,通过索引或范围访问是非常快的,不过可能无法对应上字符边界(参见Apple String Programming Guide的“Characters and Grapheme Clusters”一章)。

Swift String与Cocoa NSString之间的另一个主要差别在于NSString是不可变的。这意味着对于NSString,你可以做到根据一个字符串来获得另一个新的字符串(就像capitalizedString与substringToIndex:所做的那样),不过不能就地修改字符串。要想做到这一点,你需要另一个类NSMutableString,它是NSString的子类。NSMutableString有很多有用的方法,你可以充分利用这些方法;不过Swift String并没有桥接到NSMutableString,因此无法仅通过类型转换将String转换为NSMutableString。要想得到NSMutableString,你需要创建一个。最简单的方式是使用NSMutableString的初始化器init(string:),它接收一个NSString,这意味着你可以传递一个Swift String进去。这样,只需一步就可以将NSMutableString转换为Swift String。


let s = "hello"let ms = NSMutableString(string:s)ms.deleteCharactersInRange(NSMakeRange(ms.length-1,1))let s2 = (ms as String) + "ion" // now s2 is a Swift String  

正如第3章所介绍的,原生Swift String方法数量并不多。所有的字符串处理能力都依赖于桥接的另一方Cocoa。因此,你会经常通过桥接完成一些功能!这并不是只针对NSString与NSMutableString类的。很多其他的常见类都与之相关。

比如,假设要查找某个字符串中的子字符串。最佳做法都来自于Cocoa:

·可以通过各种rangeOfString:...方法搜索NSString,同时还可以使用大量的选项,比如,忽略临界值、忽略大小写、从尾部开始,以及待搜索的子字符串一定要位于被搜索字符串的起始或结束位置处。

·也许不太确定要搜索的是什么:你需要描述出其结构。可以通过NSScanner遍历字符串,查找满足某些条件的子字符串;比如,借助NSScanner(以及NSCharacterSet),你可以跳过以数字开头的子字符串并提取出数字。

·通过指定选项.RegularExpressionSearch,你可以使用正则表达式搜索。正则表达式也是通过单独一个类NSRegularExpression得到支持的,它会使用NSTextCheckingResult描述匹配结果。

·更加复杂的自动化文本分析是通过其他一些类得到支持的,比如,NSDataDetector,它是NSRegularExpression的子类,可以迅速找到某些类型的字符串表达式,如URL或电话号码;还有NSLinguisticTagger,它会根据文法词性规则分析文本。

在该示例中,我们要将所有“hello”替换为“heaven”。我们并不希望见到子字符串“hell”就替换,比如,“hello”就不应该替换。搜索需要智能一些,知道单词的边界是什么。这看起来是正则表达式的事情。Swift并没有提供正则表达式支持,因此一切都要通过Cocoa来完成:


let s = NSMutableString(string:"hello world, go to hell")let r = try! NSRegularExpression(    pattern: "//bhell//b",    options: .CaseInsensitive)r.replaceMatchesInString(    s, options: , range: NSMakeRange(0,s.length),    withTemplate: "heaven")// s is "hello world, go to heaven"  

NSString还提供了一些便捷的功能用以处理文件路径字符串,常用于NSURL,这是另一个值得探究的Foundation类。此外,NSString(就像本节介绍的其他类一样)提供了写到文件以及从文件读取的方法;可以通过NSString文件路径或NSURL来指定文件。

NSString并没有字体与大小等信息。显示字符串的界面对象(如UILabel)有一个类型为UIFont的font属性;不过,它只用于确定该组件上所显示的字符串的字体与大小。如果需要带样式的文本(不同的文本有不同的样式属性,如大小、字体及颜色等),那么可以使用NSAttributedString及其支持类:NSMutableAttributedString、NSParagraphStyle与NSMutableParagraphStyle。你可以通过它们从各个方面为文本和段落增加样式。显示文本的内建界面对象可以显示NSAttributedString。

可以通过NSString(参见String UIKit Additions Reference)及NSAttributedString(参见NSAttributedString UIKit Additions Reference)上的NSStringDrawing类别所提供的方法在图形上下文中绘制字符串。

10.4.3  NSDate及相关类

NSDate是个日期与时间,内部表示为从某个参考日期开始所经过的秒数(NSTimeInterval)。调用NSDate的初始化器init()(即NSDate())会生成一个代表当前日期与时间的日期对象。很多日期操作还会用到NSDateComponents,NSDate与NSDateComponents之间的转换需要传递一个NSCalendar。下述示例展示了如何根据日历值构建一个日期:


let greg = NSCalendar(calendarIdentifier:NSCalendarIdentifierGregorian)!let comp = NSDateComponentscomp.year = 2016comp.month = 8comp.day = 10comp.hour = 15let d = greg.dateFromComponents(comp) // Optional wrapping NSDate  

与之类似,NSDateComponents提供了进行日期计算的正确方式。如下示例展示了如何为给定的日期增加一个月:


let d = NSDate // or whateverlet comp = NSDateComponentscomp.month = 1let greg = NSCalendar(calendarIdentifier:NSCalendarIdentifierGregorian)!let d2 = greg.dateByAddingComponents(comp, toDate:d, options:)  

你可能还会考虑以字符串表示的日期。如果不对日期的字符串表示进行显式的处理,那么其字符串表示格式会让你吃惊的。比如,如果print一个NSDate,那么它会以GMT时区的形式表示日期,如果不在这儿住,那么这个结果会让你感到困惑。一个简单的解决办法就是调用descriptionWithLocale:;它会考虑到用户当前的时区、语言、区域格式以及日历设置等:


print(d)// 2016-08-10 22:00:00 +0000print(d.descriptionWithLocale(NSLocale.currentLocale))// Wednesday, August 10, 2016 at 3:00:00 PM Pacific Daylight Time  

请使用NSDateFormatter来创建并解析日期字符串,它使用了类似于NSLog(以及NSString的stringWithFormat:)的格式化字符串。在该示例中,我们完全使用了用户的区域设置,通过dateFormatFromTemplate:options:locale:与当前区域设置生成一个NSDateFormatter。“模板”是个字符串,列出了将要使用的日期组件,不过其顺序、标点符号和语言则留给区域设置来处理:


let df = NSDateFormatterlet format = NSDateFormatter.dateFormatFromTemplate(    "dMMMMyyyyhmmaz", options:0, locale:NSLocale.currentLocale)df.dateFormat = formatlet s = df.stringFromDate(NSDate)  

生成的日期会使用用户的时区和语言,并使用正确的语言规范。这涉及区域设置格式与语言的组合,它们是两个单独的设置。这样:

·在我的设备上,结果是“July 16,2015,7:44 AM PDT.”。

·如果将设备的区域设置修改为France,那么结果就变成了“16 July 20157:44 AM GMT-7.”。

·如果再将设备的语言修改为French,那么结果又会变成“16 juillet 20157:44 AM UTC-7.”。

10.4.4  NSNumber

NSNumber是个包装了数值的对象。被包装的值可以是任何标准的Objective-C数值类型(包括BOOL,这是Objective-C中与Swift Bool的对应类型)。让Swift用户感到惊讶的是竟然还需要NSNumber。不过,Objective-C中的普通数字并不是对象(它是标量,参见附录A),因此无法在需要对象的地方使用。这样,NSNumber就解决了一个重要问题,可以将数字转换为对象,反之亦然。

Swift会尽一切努力不让你直接使用NSNumber。它通过两种不同方式桥接了Swift数值类型与Objective-C:

·如果需要普通的数字,那么Swift数字就会桥接到普通的数字(标量)。

·如果需要对象,那么基本的数字类型的Swift数字就会桥接到NSNumber。基本的数字类型有Int、UIInt、Float、Double以及Bool,因为NSNumber能够包装Objective-C BOOL。

看看下面这个示例:


let ud = NSUserDefaults.standardUserDefaultslet i = 0ud.setInteger(i, forKey: "Score") ①ud.setObject(i, forKey: "Score") ②  

后两行看起来很像,不过Swift对待Int值i的方式却是不同的:

①setInteger:forKey:的第1个参数需要一个整型(标量),因此Swift会将Int结构体值i转换为普通的Objective-C数字。

②setObject:forKey:的第1个参数需要一个对象,因此Swift会将Int结构体值i转换为NSNumber。

自然,如果想要显式跨过这种桥接,那也是可行的。可以将Swift数字(基本的数字类型)强制转换为NSNumber:


let n = 0 as NSNumber  

要想更好地控制NSNumber所包装的数值类型,你可以调用NSNumber的初始化器:


let n = NSNumber(float:0)  

从Objective-C回到Swift,值一般会作为AnyObject,你需要进行向下类型转换。NSNumber拥有一些属性可根据数字类型访问被包装的值。回忆一下第5章的示例,它会从NSNotification的userInfo字典中将值提取出来并作为NSNumber返回:


if let prog = (n.userInfo?["progress"] as? NSNumber)?.doubleValue {    self.progress = prog}  

NSNumber还可以向下类型转换为Swift数值类型。因此,包装NSNumber的AnyObject也可以。这样,该示例可以改写成下面这样,不会显式用到NSNumber:


if let prog = n.userInfo?["progress"] as? Double {    self.progress = prog}  

在第2个版本中,Swift实际上在背后做的是与第1个示例相同的事情,将AnyObject当作NSNumber,并通过doubleValue属性提取出被包装的数字。

NSNumber对象只是一个包装器而已。无法将其直接用在数字计算上;它并非数字,而是包装了一个数字。不管怎样,如果需要数字,那就需要从NSNumber中提取。

另外,NSNumber的子类NSDecimalNumber可用于计算,这多亏了大量的数学计算方法:


let dec1 = NSDecimalNumber(float: 4.0)let dec2 = NSDecimalNumber(float: 5.0)let sum = dec1.decimalNumberByAdding(dec2) // 9.0  

NSDecimalNumber在取整方面非常有用,因为它提供了一种便捷的方式来指定所需的取整方式。

NSDecimalNumber底层是NSDecimal结构体(它是NSDecimalNumber的decimalValue)。

NSDecimal拥有一些C函数,速度上要比NSDecimalNumber方法快很多。

10.4.5  NSValue

NSValue是NSNumber的父类。它用于在需要对象的时候包装非数字的C值,比如,C结构体。它所解决的问题类似于NSNumber:Swift结构体是个对象,不过C结构体不是,因此在Objective-C中,如果需要对象,那么使用结构体是行不通的。

可以通过NSValue上的NSValueUIGeometryExtensions类别所提供的便捷方法(参见NSValue UIKit Additions Reference)轻松包装和展开CGPoint、CGSize、CGRect、CGAffineTransform、UIEdgeInsets与UIOffset;还有其他一些类别可以轻松包装和展开NSRange、CATransform3D、CMTime、CMTimeMapping、CMTimeRange、MKCoordinate与MKCoordinateSpan。一般不需要在NSValue中存储其他类型的C值,不过如果需要也是可以的。

Swift并不会神奇地桥接这些C结构体类型与NSValue。你需要显式对其进行管理,正如使用Objective-C代码所做的那样。如下示例使用Core Animation实现界面上的按钮从一个位置到另一个位置的移动;按钮的起止位置都表示为CGPoint,不过动画的fromValue与toValue必须是对象。CGPoint并非Objective-C对象,因此需要将CGPoint值包装到NSValue对象中:


let ba = CABasicAnimation(keyPath:"position")ba.duration = 10ba.fromValue = NSValue(CGPoint:self.oldButtonCenter)ba.toValue = NSValue(CGPoint:goal)self.button.layer.addAnimation(ba, forKey:nil)  

与之类似,可以在Swift中创建CGPoint的数组,这是因为CGPoint变成了一个Swift对象类型(Swift结构体),而Swift Array可以持有任意类型的元素;不过不能将该数组传递给Objective-C,因为Objective-C NSArray中的元素必须是对象,而Objective-C中的CGPoint并不是对象。这样,首先就需要将CGPoints包装到NSValue对象中。下面是另一个动画示例,我通过将CGPoints数组转换为NSValues数组来设置关键帧动画的values数组(NSArray)。


anim.values = [oldP,p1,p2,newP].map{NSValue(CGPoint:$0)}  

10.4.6  NSData

NSData是个字节序列;基本上,它是个缓存,占据了一块内存。它是不可变的;其可变版本是其子类NSMutableData。

在实际开发中,NSData主要用在如下两种情况当中:

·从Internet上下载数据。比如,NSURLConnection与NSURLSession会将从Internet上接收到的东西当作NSData。你可以根据需要将其转换为字符串,并指定正确的编码。

·将对象存储为文件或用户首选项(NSUserDefaults)。比如,你无法直接将UIColor值存储为用户首选项。如果用户选择了某个颜色,你需要将其保存起来,那么你可以将UIColor转换为NSData(使用NSKeyedArchiver)并保存:


let ud = NSUserDefaults.standardUserDefaultslet c = UIColor.blueColorlet cdata = NSKeyedArchiver.archivedDataWithRootObject(c)ud.setObject(cdata, forKey: "myColor")  

10.4.7 相等与比较

在Swift中,如果对象类型使用了Equatable与Comparable协议,那么我们就可以针对该对象类型重写相等与比较运算符。不过Objective-C运算符则不行,在Objective-C中,相等与比较运算符只能用于标量。

要想对两个对象进行“相等”判断(无论对于该对象类型来说相等意味着什么),Objective-C类必须要实现isEqual:,它继承自NSObject。Swift则会将NSObject看作Equatable,并且允许使用==运算符,从而解决了各种问题,它会隐式将==运算符转换为isEqual:调用。这样,如果一个类实现了isEqual:,那么我们就可以使用普通的Swift比较。比如:


let n1 = NSNumber(integer:1)let n2 = NSNumber(integer:2)let n3 = NSNumber(integer:3)let ok = n2 == 2 // true ①let ok2 = n2 == NSNumber(integer:2) // true ②let ix = [n1,n2,n3].indexOf(2) // Optional wrapping 1 ③  

上述代码似乎做了3件不可能的事情:

①我们直接比较了Int与NSNumber,并且得到了正确的结果,就好像比较的是Int与NSNumber所包装的那个整数一样。

②我们直接比较了两个NSNumber对象,并且得到了正确的结果,就好像比较的是这两个NSNumber对象所包装的整数一样。

③我们将NSNumber数组看作Equatables数组,并调用了indexOf方法,最后成功找出“等于”那个实际值的NSNumber对象。

这种魔法分为两块:

·数字被包装到了NSNumber对象中。

·==运算符(背后也被indexOf方法所用)会被转换为isEqual:调用。

NSNumber实现了isEqual:来比较两个NSNumber对象,这是通过比较所包装的数值来实现的;因此,相等比较可以正常使用。

如果NSObject子类没有实现isEqual:,那么它会继承NSObject的实现,比较两个对象的相等性(就像Swift的===运算符一样)。比如,两个Dog对象可以通过==运算符进行比较,虽然Dog并未使用Equatable也是可以的,因为它们都继承自NSObject,但Dog没有实现isEqual:,因此==默认将会使用NSObject的相等比较:


class Dog : NSObject {    var name : String    init(_ name:String) {self.name = name}}let d1 = Dog("Fido")let d2 = Dog("Fido")let ok = d1 == d2 // false  

很多实现了isEqual:的类还实现了更为具体和高效的测试。对于Objective-C,判断两个NSNumber对象是否相等(即包装了相同的数字)的常见做法是调用isEqualToNumber:。与之类似,NSString有isEqualToString:、NSDate有isEqualToDate:,诸如此类。不过,这些类还实现了isEqual:,因此我觉得最好的方式还是使用Swift==运算符。

与之类似,在Objective-C中,提供排序比较方法是每个类的职责。标准方法是compare:,它会返回3个NSComparisonResult case之一:

.OrderedAscending

接收者小于参数。

.OrderedSame

接收者等于参数。

.OrderedDescending

接收者大于参数。

Swift比较运算符(<之类的)并不会神奇地调用compare:。你不能直接比较两个NSNumber值:


let n1 = NSNumber(integer:1)let n2 = NSNumber(integer:2)let ok = n1 < n2 // compile error  

你常常需要自己调用compare:,就像在Objective-C中所做的那样:


let n1 = NSNumber(integer:1)let n2 = NSNumber(integer:2)let ok = n1.compare(n2) == .OrderedAscending // true  

10.4.8  NSIndexSet

NSIndexSet表示不重复的数字集合;其目的在于表示出有序的集合元素数字,如NSArray。这样,比如,我们要从数组中同时获取多个对象,那么你就需要将所需要的索引指定为NSIndexSet。它还可以用在类似于数组的结构中;比如,可以向UITableView传递一个NSIndexSet来指定要插入或删除哪个部分。

来看个具体的示例。假设一个NSArray中包含了元素1、2、3、4、8、9与10。NSIndexSet以一种更加简洁的实现来表达这个概念,并且很容易查询。真正的实现我们是看不到的,不过你可以认为这个NSIndexSet包含了两个NSRange结构体:{1,4}与{8,3},NSIndexSet的方法实际上会让你觉得一个NSIndexSet是由多个范围构成的。

NSIndexSet是不可变的;其可变的子类是NSMutableIndexSet。你可以通过向indexSetWithIndexesInRange:传递一个NSRange来直接构造只有一个连续范围的NSIndexSet;但要想构造更加复杂的索引集合,你就需要使用NSMutableIndexSet,这样就可以附加更多的范围了。


let arr = ["zero", "one", "two", "three", "four", "five",    "six", "seven", "eight", "nine", "ten"]let ixs = NSMutableIndexSetixs.addIndexesInRange(NSRange(1...4))ixs.addIndexesInRange(NSRange(8...10))let arr2 = (arr as NSArray).objectsAtIndexes(ixs)  

可以通过for...in来遍历(枚举)NSIndexSet所指定的索引值;此外,还可以通过调用enumerateIndexesUsingBlock:、enumerateRangesUsingBlock:等方法来遍历NSIndexSet的索引或范围。

10.4.9  NSArray与NSMutableArray

NSArray是Objective-C的数组对象类型。基本上,它相当于Swift Array,并且可以彼此桥接。不过,NSArray的元素必须是对象(类与类的实例),这些对象的类型可以不同。要想完整理解Swift Array与Objective-C NSArray之间的隐式桥接与类型转换,请参见4.12.1的“Swift Array与Objective-C NSArray”部分。

在iOS 9中,如果NSArray对象只有一种元素类型,那么Objective-C就可以在其声明中标记出其类型。Swift 2.0可以读取该标记。这意味着你不会再像过去那样接收到[AnyObject]了(不得不向下类型转换为真正的类型)。这一点对于NSSet及NSDictionary来说也是一样的。

NSArray的长度是其count,可以通过objectAtIndex:根据索引号获得特定的对象。与Swift Array一样,第一个对象的索引是0,因此最后一个对象的索引是其count减1。

相对于调用objectAtIndex:,你可以对NSArray使用下标。这并非因为NSArray会桥接到Swift Array,而是NSArray实现了objectAtIndexedSubscript:。该方法是Swift subscript getter在Objective-C中的对应之物,Swift知道这一点。实际上,当NSArray头文件转换为Swift时,该方法会表示为一个subscript声明!这样,该头文件的Objective-C版本声明如下所示:


- (ObjectType)objectAtIndexedSubscript:(NSUInteger)idx;  

不过,相同头文件的Swift版本则如下所示:


subscript (idx: Int) -> AnyObject { get }  

(要想理解Objective-C声明中ObjectType的含义,请参见附录A。)

可以通过indexOfObject:或indexOfObjectIdenticalTo:查找数组中的某个对象;前者会调用isEqual:,后者则会使用对象同一性(类似于Swift中的===)。如前所述,如果在数组中找不到对象,那么结果就是NSNotFound。

与Swift Array不同,但类似于Objective-C NSString,NSArray是不可变的。这并不意味着你无法修改其所包含的任何对象;相反,这表示一旦构建好了NSArray,你就不能再从中删除对象、向其插入对象,或替换掉指定索引处的对象。要想在Objective-C中完成这些事情,你可以创建一个新数组,里面包含着原来的数组元素再加上或减去一些对象,或使用NSArray的子类NSMutableArray。Swift Array并未桥接到NSMutableArray;如果需要NSMutableArray,那就得创建它。最简单的方式是使用NSMutableArray的初始化器init()或是init(array:)。

有了NSMutableArray后,你就可以调用NSMutableArray的addObject:及replaceOb-jectAtIndex:withObject:之类的方法了;还可以通过下标给NSMutableArray赋值。这是因为NSMutableArray实现了一个特殊的方法setObject:atIndexedSubscript:,Swift将其看作subscript setter。

此外,除了[AnyObject],你无法直接将任何类型的NSMutableArray转换为Swift Array;通常的做法是从NSMutableArray向上类型转换为NSArray,然后再向下类型转换为特定类型的Swift Array:


let marr = NSMutableArraymarr.addObject(1) // an NSNumbermarr.addObject(2) // an NSNumberlet arr = marr as NSArray as! [Int]  

Cocoa提供了通过块来搜索或过滤数组的方式。还可以使用排序数组,并通过各种方式提供排序规则;如果是可变数组,那么还可以直接对其排序。你可能更希望在Swift Array中执行这些操作,不过了解如何通过Cocoa的方式做到这一点也是很有意义的。比如:


let pep = ["Manny", "Moe", "Jack"] as NSArraylet ems = pep.objectsAtIndexes(    pep.indexesOfObjectsPassingTest {        obj, idx, stop in        return (obj as! NSString).rangeOfString(                "m", options:.CaseInsensitiveSearch            ).location == 0    }) // ["Manny", "Moe"]  

10.4.10  NSDictionary与NSMutableDictionary

NSDictionary是Objective-C的字典对象类型。它基本上类似于Swift Dictionary,并且二者之间会彼此桥接。不过,NSDictionary的键值必须是对象(类与类的实例),这些对象的类型可以不同;键必须要遵循NSCopying,并且是可以散列的。请参见4.12.2节的“Swift Dictionary与Objective-C NSDictionary”部分了解关于如何桥接Swift Dictionary与Objective-C NSDictionary以及类型转换的详细信息。

NSDictionary是不可变的;其可变子类是NSMutableDictionary。Swift Dictionary并没有桥接到NSMutableDictionary;你可以通过初始化器init()或init(dictionary:)方便地创建一个NSMutableDictionary。

NSDictionary的键是不同的(使用isEqual:进行比较)。如果向NSMutableDictionary添加一个键值对,键要是不在其中,那么这个键值对就会被添加进去;但如果键已经存在了,那么相应的值就会被替换掉。这与Swift Dictionary的行为类似。

NSDictionary的基本用法是通过键来获取一个条目的值(使用objectForKey:);如果键不存在,那么结果就是nil。在Objective-C中,nil并非对象,因此它不可能是NSDictionary中的值;这个结果的含义是非常明确的。Swift通过将objectForKey:的结果看作AnyObject?来解决这一问题,即包装了AnyObject的Optional。

我们也可以对NSDictionary和NSMutableDictionary使用下标,原因与可以对NSArray和NSMutableArray使用下标一样。NSDictionary实现了objectForKeyedSubscript:,Swift将其看作subscript getter。此外,NSMutableDictionary实现了setObject:for-KeyedSubscript:,Swift将其看作subscript setter。

可以从NSDictionary获取键的列表(allKeys)、值的列表(allValues),以及根据值排序的键的列表。还可以通过块来遍历键值对,甚至可以通过比较值来过滤NSDictionary。

10.4.11  NSSet及相关类

NSSet是个由不同对象构成的无序集合。“不同”意味着在使用isEqual:比较集合中的两个对象时不会返回true。判断集合中是否存在某个对象要比在数组中搜索高效得多,你可以判断某个集合是否是另一个集合的子集或两个集合是否相交。你可以使用for...in结构遍历(枚举)集合,当然,顺序是不确定的。你可以过滤集合,就像过滤NSArray一样。实际上,你对集合所能进行的操作类似于数组,当然,你不能对集合执行任何涉及排序含义的操作。

要想摆脱这个限制,可以使用有序集合。有序集合(NSOrderedSet)非常类似于数组,并且操作有序集合的方法也非常类似于数组的,你甚至可以通过下标获取元素(因为实现了objectAtIndexedSubscript:)。不过,有序集合的元素必须是不同的。有序集合提供了很多优势:比如,与NSSet一样,判断一个对象是否位于有序集合中要比判断数组高效得多,你可以对集合进行并集、交集与差集等运算。既然要求元素不同,这个约束基本上算不上什么约束(因为元素无论如何也得是不同的),因此请尽量使用NSOrderedSet而非NSArray。

将数组传递给有序集合会去重,这意味着顺序不会发生变化,但只有相同对象的第1个才会被添加到集合中。

NSSet是不可变的。你可以通过添加或删除元素从另一个NSSet生成一个新的,还可以使用其子类NSMutableSet。与之类似,NSOrderedSet也有其可变版本NSMutableOrderedSet(可以通过下标插入,因为它实现了setObject:atIndexed-Subscript:)。向集合中添加一个对象时,如果这个对象已经在集合中了,那么是不会有什么副作用的;结果就是什么也不会添加进去(唯一性规则会起作用),但也不会报错。

NSCountedSet是NSMutableSet的子类,它是个可变无序的对象集合,并且集合中的对象可以是相同的(这个概念通常也叫作Bag)。它被实现为一个集合,同时还会记录下每个元素被添加的次数。

Swift Set会被桥接到NSSet。不过,NSSet的元素必须是对象(类与类的实例),这些对象的类型可以不同。请参见4.12.3节“Swift Set与Objective-C NSSet”部分了解详情。Swift中并没有与NSMutableSet、NSCountedSet、NSOrderedSet及NSMutableOrderedSet对应的桥接之物,不过可以通过初始化器从集合或数组轻松构建出来。此外,你可以将NSMutableSet或NSCountedSet向上类型转换为NSSet,然后再向下类型转换为Swift Set(类似于NSMutableArray)。NSOrderedSet带有一个“门面”属性,可以将其表示为数组或集合。不过由于其特殊的行为,你更倾向于以Objective-C的形式来使用NSCountedSet与NSOrderedSet。

10.4.12  NSNull

NSNull类什么都不做,但却提供了一个指向单例对象的指针NSNull()。有时,我们需要一个实际的Objective-C对象,但不能为nil,这个单例对象就表示nil。比如,不能将nil作为Objective-C集合元素值(如NSArray、NSSet及NSDictionary),因此需要使用NSNull()。

可以通过普通的相等运算符判断一个对象是否等于NSNull(),因为它会使用NSObject的isEqual:,它进行的是同一性比较。这是个单例实例,因此可以使用同一性比较。

10.4.13 不变与可变

初学者有时难以理解Cocoa Foundation中成对出现的不变与可变类,其中父类都是不变的,子类都是可变的。这不禁令人想起Swift对于常量(let)与真正的变量(var)的区分,它们也有类似的结果。比如,NSArray是“不变的”,这与我们使用let来表示Swift Array是一个意思:你不能向该数组追加或插入元素,也不能替换或删除该数组中的元素,不过如果数组中的元素是引用类型(当然,对于NSArray来说,其元素肯定是引用类型),那么你可以就地修改元素。

Cocoa需要这些不变/可变类的原因在于防止非法修改。这些都是普通的类,NSArray对象是个普通的类实例——引用类型。如果类有一个NSArray属性,并且该数组是可变的,那么该数组就有可能在该类不知情的情况下被其他对象修改。为了防止这种情况发生,类在内部会临时使用一个可变实例,然后将其存储起来并提供给其他类一个不可变的实例,这样可以保护值不被意外修改(Swift中就没有这个问题,因为其基本的内建对象类型如String、Array与Dictionary等都是结构体,因此它们是值类型,无法就地修改;它们只能被替换,这可以通过setter观察者进行防护或检测)。

文档中可能没有明确表示出可变类已经重写了其不可变父类的方法。比如,NSMutableArray的很多方法就没有列在NSMutableArray类的文档页面中,因为它们都继承自NSArray。当这种方法被可变子类继承下来时,它们会被重写以符合可变子类的需要。比如,NSArray的init(array:)会生成一个不可变数组,不过NSMutableArray的init(array:)(它甚至都没有列在NSMutableArray的文档页面中,因为它继承自NSArray)会生成一个可变数组。

这也回答了如何让不可变数组成为可变以及如何让可变数组成为不可变的问题。如果发送给NSArray类的init(array:)生成了一个新的不可变数组,新数组中包含了与原始数组相同的对象且顺序相同,那么发送给NSMutableArray类的相同的初始化器init(array:)就会生成一个可变数组,其中包含了与原始数组相同的对象且顺序相同。因此,这个方法可以在不变与可变这两个方向传递数组。还可以使用copy(生成一个不可变副本)与mutableCopy(生成一个可变副本),它们都继承自NSObject;不过这两个方法都不是很方便,因为它们生成的都是AnyObject,你还需要进行类型转换。

这些不变/可变类都实现为了类簇,这意味着Cocoa会使用一个秘密类,这个类与文档中所记录的那个类是不同的。可以通过查看底层代码了解到这一点;就拿NSStringFromClass(s.dynamicType)来说,其中s是个NSString,它会生成一个神秘值"__NSCFString"。你不应该在这个秘密类上花太多时间。随着时间的流逝,这个类可能会发生变化,但却不会通知你,而且与你也没有任何关系;你永远都不需要知道它。

10.4.14 属性列表

属性列表是数据的字符串(XML)表示。只有Foundation类NSString、NSData、NSArray与NSDictionary才能被转换为属性列表。此外,只有当NSArray与NSDictionary中的类是这些类以及NSDate与NSNumber时,它们才能被转换为属性列表。(如前所述,这正是你需要将UIColor转换为NSData才能在user defaults中存储的原因所在;user defaults就是个属性列表。)

属性列表的主要作用是将数据存储为文件。它是值序列化的一种方式,即将值以一种形式存储到磁盘中,然后还可以将这种形式的值重建。NSArray与NSDictionary提供了便捷方法writeToFile:atomically:与writeToURL:atomically:来分别根据给定的路径名与URL生成属性列表文件;相反,它们还提供了初始化器,可以根据给定文件的属性列表内容来创建NSArray对象与NSDictionary对象。出于这个原因,在创建属性列表时,你可以从这些类开始。((NSString与NSData的方法writeToFile:...与writeToURL:...只是将数据直接写到文件中,而非属性列表。)

当通过这种方式从属性列表文件重建NSArray或NSDictionary对象时,集合、字符串对象与集合中的数据对象都是不可变的。如果希望它们是可变的,或是想将一个属性列表类的实例转换为另一个属性列表,你需要使用NSPropertyListSerialization类。(参见Property List Programming Guide。)