与大多数现代计算机语言一样,Swift也拥有内建的集合类型,Array与Dictionary,以及第3种类型Set。Array与Dictionary是非常重要的,语言也对其提供了特殊的语法支持。同时,就像大多数Swift类型一样,Swift为其提供的相关函数也是有限的,一些缺失的功能是通过Cocoa的NSArray与NSDictionary补充的,Array与NSArray,Dictionary与NSDictionary之间是彼此桥接的。Set集合类型则与Cocoa的NSSet桥接。
4.12.1 Array
数组(Array,是个结构体)是对象实例的一个有序集合(即数组元素),可以通过索引号进行访问,索引号是个Int,值从0开始。因此,如果一个数组包含了4个元素,那么第1个元素的索引就为0,最后一个元素的索引为3。Swift数组不可能是稀疏数组:如果有一个元素的索引为3,那么肯定还会有一个元素的索引为2,以此类推。
Swift数组最显著的特征就是其严格的类型。与其他一些计算机语言不同,Swift数组的元素必须是统一的,也就是说,数组必须包含相同类型的元素。甚至连空数组都必须要有确定的元素类型,尽管此时数组中并没有元素存在。数组本身的类型与其元素的类型是一致的。所包含的元素类型不同的数组被认为是两个不同类型的数组:Int元素的数组与String元素的数组就是不同类型的数组。数组类型与其元素类型一样也是多态的:如果NoisyDog是Dog的子类,那么NoisyDog的数组就可以用在需要Dog数组的地方。如果这些让你想起了Optional,那就对了。就像Optional一样,Swift数组也是泛型。它被声明为Array<Element>,其中占位符Element是特定数组元素的类型。
一致性约束并没有初看起来那么苛刻。数组只能包含一种类型的元素,不过类型却是非常灵活的。通过精心选取类型,你所创建的数组中,内部元素的类型可以是不同的。比如:
·如果Dog类有个NoisyDog子类,那么Dog数组既可以包含Dog对象,也可以包含NoisyDog对象。
·如果Bird与Insect都使用了Flier协议,那么Flier数组既可以包含Bird对象,也可以包含Insect对象。
·AnyObject数组可以包含任何类以及任何Swift桥接类型的实例,如Int、String和Dog等。
·类型本身也可以是不同的可能类型的载体。本章之前介绍的Error枚举就是一个例子;其关联值可能是Int或是String,这样Error元素的数组就可以包含Int值与String值。
要想声明或表示给定数组元素的状态,你应该显式解析出泛型占位符;Int元素的数组就是Array<Int>。不过,Swift提供了语法糖来表示数组的元素类型,通过将方括号包围元素类型名来表示,如[Int]。你在绝大多数时候都会使用这种语法。
字面值数组表示为一个方括号,里面包含着用逗号分隔的元素列表(以及可选的空白字符):如[1,2,3]。空数组的字面值就是空的方括号。
数组的默认初始化器init()是通过在数组类型后面使用一对空的圆括号来调用的,它会生成该类型的一个空数组。因此,你可以像下面这样创建一个空的Int数组:
var arr = [Int]
另外,如果提前已经知道了引用类型,那么空数组就可以推断为该类型。因此,你还可以像下面这样创建一个Int类型的空数组:
var arr : [Int] =
如果从包含元素的字面值数组开始,那么通常无须声明数组的类型,因为Swift会根据元素推断出其类型。比如,Swift会将[1,2,3]推断为Int数组。如果数组元素类型包含了类及其子类,如Dog与NoisyDog,那么Swift会将父类推断为数组的类型。甚至[1,"howdy"]也是合法的数组字面值;它会被推断为NSObject数组。不过,在某些情况下,你还是需要显式声明数组引用的类型,同时将字面值赋给该数组:
let arr : [Flier] = [Insect, Bird]
数组也有一个初始化器,其参数是个序列。这意味着,如果类型是个序列,那么你可以将其实例分割到数组元素中。比如:
·Array(1...3)会生成数组Int[1,2,3]。
·Array("hey".characters)会生成数组Character["h","e","y"]。
·Array(d)(其中d是个Dictionary)会生成d键值对的元组数组。
另一个数组初始化器init(count:repeatedValue:)可以使用相同的值来装配数组。在该示例中,我创建了一个由100个Optional字符串构成的数组,每个Optional都被初始化为nil:
let strings : [String?] = Array(count:100, repeatedValue:nil)
这是Swift中最接近稀疏数组的数组创建方式;我们有100个槽,每个槽都可能包含或不包含一个字符串(一开始都不包含)。
1.数组转换与类型检测
在将一个数组类型赋值、传递或转换为另一个数组类型时,你操作的实际上是数组中的每个元素。比如:
let arr : [Int?] = [1,2,3]
上述代码实际上是个简写:将Int数组看作包装了Int的Optional数组意味着原始数组中的每个Int都必须要包装到Optional中。如下代码所示:
let arr : [Int?] = [1,2,3]print(arr) // [Optional(1), Optional(2), Optional(3)]
与之类似,假设有一个Dog类及其NoisyDog子类;那么如下代码就是合法的:
let dog1 : Dog = NoisyDoglet dog2 : Dog = NoisyDoglet arr = [dog1, dog2]let arr2 = arr as! [NoisyDog]
在第3行,我们有一个Dog数组。在第4行,我们将该数组向下类型转换为NoisyDog数组,这意味着我们将第1个数组中的每个Dog都转换为了NoisyDog(这么做应用并不会崩溃,因为第1个数组中的每个元素实际上都是个NoisyDog)。
你可以将数组的所有元素与is运算符进行比较来判断数组本身。比如,考虑之前代码中的Dog数组,可以这么做:
if arr is [NoisyDog] { // ...
如果数组中的每个元素都是NoisyDog,那么结果就为true。
与之类似,as?运算符会将数组转换为包装数组的Optional,如果底层的转换无法进行,那么结果就为nil:
let dog1 : Dog = NoisyDoglet dog2 : Dog = NoisyDoglet dog3 : Dog = Doglet arr = [dog1, dog2]let arr2 = arr as? [NoisyDog] // Optional wrapping an array of NoisyDoglet arr3 = [dog2, dog3]let arr4 = arr3 as? [NoisyDog] // nil
对数组进行向下类型转换与对任何值进行向下类型转换的原因相同,这样就可以向数组的元素发送恰当的消息了。如果NoisyDog声明了一个Dog没有的方法,那么你就不能向Dog数组中的元素发送该消息。有时需要将元素向下类型转换为NoisyDog,这样编译器就允许你发送该消息了。你可以向下类型转换单个元素,也可以转换整个数组;你要做的就是选择一种安全并且在特定上下文中有意义的方式。
2.数组比较
数组相等与你想的是一样的:如果两个数组包含相同数量的元素,并且相同位置上的元素全都相等,那么这两个数组就是相等的:
let i1 = 1let i2 = 2let i3 = 3if [1,2,3] == [i1,i2,i3] { // they are equal!
如果比较两个数组,那么这两个数组不必非得是相同类型的,不过除非它们包含的对象都彼此相等,否则这两个数组就不会相等。如下代码比较了一个Dog数组和一个NoisyDog数组;它们实际上是相等的,因为它们实际上是以相同顺序包含了相同的狗:
let nd1 = NoisyDoglet d1 = nd1 as Doglet nd2 = NoisyDoglet d2 = nd2 as Dogif [d1,d2] == [nd1,nd2] { // they are equal!
3.数组是值类型
由于数组是个结构体,因此它是个值类型而非引用类型。这意味着每次将数组赋给变量或作为参数传递给函数时,数组都会被复制。不过,我并不是说仅仅赋值或传递数组就是代价高昂的操作,这些复制每次都会发生。如果对数组的引用是个常量,那显然就没必要执行复制了;甚至从另一个数组生成新数组,或是修改数组的操作都是非常高效的。你要相信Swift的设计者肯定已经考虑过这些问题了,并且在背后会高效地实现数组。
虽然数组本身是个值类型,但其元素却会按照元素本身的情况来对待。特别地,如果一个数组是类实例数组,将其赋给多个变量,那么结果就是会有多个引用指向相同的实例。
4.数组下标
Array结构体实现了subscript方法,可以通过在对数组的引用后使用方括号来访问元素。你可以在方括号中使用Int。比如,在一个包含着3个元素的数组中,如果数组是通过变量arr引用的,那么arr[1]就可以访问第2个元素。
还可以在方括号中使用Int的Range。比如,如果arr是个包含3个元素的数组,那么arr[1...2]就表示第2与第3个元素。从技术上来说,如arr[1...2]之类的表达式会生成一个ArraySlice。不过,ArraySlice非常类似于数组;比如,你可以像对数组一样对ArraySlice使用下标,在需要数组的地方也可以传递ArraySlice过去。一般来说,你可以认为ArraySlice就是个数组。
如果对数组的引用是可变的(var而非let),那么就可以对下标表达式赋值。这么做会改变槽中的值。当然,赋的值一定要与数组元素的类型保持一致:
var arr = [1,2,3]arr[1] = 4 // arr is now [1,4,3]
如果下标是个范围,那么赋的值就必须是个数组。这会改变被赋值的数组的长度:
var arr = [1,2,3]arr[1..<2] = [7,8] // arr is now [1,7,8,3]arr[1..<2] = // arr is now [1,8,3]arr[1..<1] = [10] // arr is now [1,10,8,3] (no element was removed!)
如果访问数组元素时使用的下标大于最大值或小于最小值,那就会产生运行时错误。如果arr有3个元素,那么arr[-1]与arr[3]从语义上来说并没有错,但程序将会崩溃。
5.嵌套数组
数组元素也可以是数组,比如:
let arr = [[1,2,3], [4,5,6], [7,8,9]]
这是个Int数组的数组。因此,其类型声明是[[Int]]。(被包含的数组不一定非得是相同长度的,我这么做只是为了清晰。)
要想访问嵌套数组中的每个Int,你可以将下标运算符链接起来:
let arr = [[1,2,3], [4,5,6], [7,8,9]]let i = arr[1][1] // 5
如果外层数组引用是可变的,那么你还可以对嵌套数组赋值:
var arr = [[1,2,3], [4,5,6], [7,8,9]]arr[1][1] = 100
还可以通过其他方式修改内部数组;比如,可以插入新的元素。
6.基本的数组属性与方法
数组是一个集合(CollectionType协议),集合本身又是个序列(SequenceType协议)。你可能对此有所了解:String的characters就是这样的,我在第3章将其称作字符序列。出于这个原因,数组与字符序列是非常相似的。
作为集合,数组的count是个只读属性,返回数组中的元素个数。如果数组的count为0,那么其isEmpty属性就为true。
数组的first与last只读属性会返回其第一个和最后一个元素,不过这些元素会被包装到Optional中,因为数组可能为空,因此这些属性就会为nil。这会出现Swift中很少会遇到的将一个Optional包装到另一个Optional中的情况。比如,考虑包装Int的Optional数组,如果获取该数组最后一个属性会发生什么。
数组最大的可访问索引要比其count小1。你常常会使用对count的引用来计算索引值;比如,要想引用arr的最后两个元素,可以这样做:
let arr = [1,2,3]let arr2 = arr[arr.count-2...arr.count-1] // [2,3]
Swift并未使用现在普遍采用的通过负数来计算索引这一约定。另一方面,如果想要访问数组的最后n个元素,你可以使用suffix方法:
let arr = [1,2,3]let arr2 = arr.suffix(2) // [2,3]
suffix与prefix有一个值得注意的特性,那就是超出范围后并不会出错:
let arr = [1,2,3]let arr2 = arr.suffix(10) // [1,2,3] (and no crash)
相对于通过数量来描述后缀或前缀的大小,你可以通过索引来表示后缀或前缀的限制:
let arr = [1,2,3]let arr2 = arr.suffixFrom(1) // [2,3]let arr3 = arr.prefixUpTo(1) // [1]let arr4 = arr.prefixThrough(1) // [1,2]
数组的startIndex属性值是0,其endIndex属性值是其count。此外,数组的indices属性值是个半开区间,其端点是startIndex与endIndex,即访问整个数组的范围。如果通过可变引用来访问这个范围,那就可以修改其startIndex与endIndex来生成一个新的范围。第3章对字符序列就是这么做的;不过,数组的索引值是Int,因此你可以使用普通的算术运算符:
let arr = [1,2,3]var r = arr.indicesr.startIndex = r.endIndex-2arr2 = arr[r] // [2,3]
indexOf方法会返回一个元素在数组中首次出现位置处的索引,不过它被包装到了Optional中,这样如果数组中不存在这个元素就会返回nil。如果数组包含了Equatables,那比较时就可以通过==来识别待寻找的元素:
let arr = [1,2,3]let ix = arr.indexOf(2) // Optional wrapping 1
即便数组没有包含Equatables,你也可以提供自定义的函数,它接收一个元素类型并返回一个Bool,你会得到返回true的第一个元素。在如下示例中,Bird结构体有一个name String属性:
let aviary = [Bird(name:"Tweety"), Bird(name:"Flappy"), Bird(name:"Lady")]let ix = aviary.indexOf {$0.name.characters.count < 5} // Optional(2)
作为序列,数组的contains方法会判断它是否包含了某个元素。如果元素是Equatables,那么你依然可以使用==运算符;也可以提供自定义函数,该函数接收一个元素类型并返回一个Bool:
let arr = [1,2,3]let ok = arr.contains(2) // truelet ok2 = arr.contains {$0 > 3} // false
startsWith方法判断数组的起始元素是否与给定的相同类型序列的元素相匹配。这里依然可以使用==运算符来比较Equatables;你也可以提供自定义函数,该函数接收两个元素类型值,并返回表示它们是否匹配的Bool:
let arr = [1,2,3]let ok = startsWith(arr, [1,2]) // truelet ok2 = arr.startsWith([1,-2]) {abs($0) == abs($1)} // true
elementsEqual方法是数组比较的序列泛化:两个序列长度必须相同,其元素要么是Equatables,要么你自己提供匹配函数。
minElement与maxElement方法分别返回数组中最小与最大的元素,并且被包装到Optional中,目的是防止数组为空。如果数组包含了Comparables,那么你可以使用<运算符;此外,你可以提供一个返回Bool的函数,表示两个给定元素中较小的那个是不是第一个:
let arr = [3,1,-2]let min = arr.minElement // Optional(-2)let min2 = arr.minElement {abs($0)<abs($1)} // Optional(1)
如果对数组的引用是可变的,那么append与appendContentsOf实例方法就会将元素添加到数组末尾。这两个方法的差别在于append会接收单个元素类型值,而appendContentsOf则接收元素类型的一个序列。比如:
var arr = [1,2,3]arr.append(4)arr.appendContentsOf([5,6])arr.appendContentsOf(7...8) // arr is now [1,2,3,4,5,6,7,8]
若+运算符左侧是个数组,那么+就会被重载,其行为类似于appendContentsOf(而非append!),只不过它会生成一个新数组,因此即便对数组的引用是个常量,这么做也是可行的。如果对数组的引用是可变的,那么你可以通过+=运算符对其进行扩展。比如:
let arr = [1,2,3]let arr2 = arr + [4] // arr2 is now [1,2,3,4]var arr3 = [1,2,3]arr3 += [4] // arr3 is now [1,2,3,4]
如果对数组的引用是可变的,那么实例方法insert(atIndex:)就会在指定的索引处插入一个元素。要想同时插入多个元素,请使用范围下标数组进行赋值,就像之前介绍的那样(此外,还有一个insertContentsOf(at:)方法)。
如果对数组的引用是可变的,那么实例方法removeAtIndex会删除指定索引处的元素;实例方法removeLast会删除最后一个元素,removeFirst则会删除第一个元素。这些方法还会返回从数组中删除的值;如果不需要,那么可以忽略返回值。这些方法不会将返回值包装到Optional中,访问越界的索引会导致程序崩溃。另一种形式的removeFirst可以指定删除的元素数量,不过它不返回值;如果数组中没有那么多元素,那么程序将会崩溃。
另外,popFirst与popLast则会展开Optional中的返回值;这样,即便数组为空也是安全的。如果引用不可变,那么你可以通过dropFirst与dropLast方法返回删除了最后一个元素后的数组(实际上是个切片)。
joinWithSeparator实例方法接收一个数组的数组。它会提取出每个元素,并将参数数组中的元素插入到提取出的每个元素序列之间。结果是个叫作JoinSequence的中间序列;如果必要,还需要将其转换为Array。比如:
let arr = [[1,2], [3,4], [5,6]]let arr2 = Array(arr.joinWithSeparator([10,11]))// [1, 2, 10, 11, 3, 4, 10, 11, 5, 6]
调用joinWithSeparator时将空数组作为参数可以将数组的数组打平:
let arr = [[1,2], [3,4], [5,6]]let arr2 = Array(arr.joinWithSeparator())// [1, 2, 3, 4, 5, 6]
还有一个flatten实例方法也可以做到这一点。它会返回一个中间序列(或集合),你可能需要将其转换为Array:
let arr = [[1,2], [3,4], [5,6]]let arr2 = Array(arr.flatten)// [1, 2, 3, 4, 5, 6]
reverse实例方法会生成一个新数组,其元素顺序与原始数组的相反。
sortInPlace实例方法会对原始数组排序(如果对数组的引用是可变的),而sort实例方法则会根据原始数组生成一个新数组。你有两个选择:如果是Comparables数组,那就可以通过<运算符指定新顺序;此外,你可以提供一个函数,该函数接收两个元素类型参数并返回一个Bool,表示第1个参数是否应该位于第2个参数之前(就像minElement与maxElement一样)。比如:
var arr = [4,3,5,2,6,1]arr.sortInPlace // [1, 2, 3, 4, 5, 6]arr.sortInPlace {$0 > $1} // [6, 5, 4, 3, 2, 1]
在最后一行代码中,我提供了一个匿名函数。当然,你还可以将一个声明好的函数名作为参数传递进来。在Swift中,比较运算符其实就是函数名。因此,我能以更加简洁的方式完成相同的功能,比如:
var arr = [4,3,5,2,6,1]arr.sortInPlace(>) // [6, 5, 4, 3, 2, 1]
split实例方法会在通过测试的元素位置处将一个数组分解为数组的数组,这里的测试指的是一个函数,它接收一个元素类型值并返回Bool;通过测试的元素会被去除:
let arr = [1,2,3,4,5,6]let arr2 = arr.split {$0 % 2 == 0} // split at evens: [[1], [3], [5]]
7.数组枚举与转换
数组是一个序列,因此你可以对其进行枚举,并按照顺序查看或操纵每个元素。最简单的方式是使用for...in循环;第5章将会对此进行更为详尽的介绍:
let pepboys = ["Manny", "Moe", "Jack"]for pepboy in pepboys { print(pepboy) // prints Manny, then Moe, then Jack}
此外,你还可以使用forEach实例方法。其参数是个函数,该函数接收数组(或是其他序列)中的一个元素并且没有返回值。你可以将其看作命令式for...in循环的函数式版本:
let pepboys = ["Manny", "Moe", "Jack"]pepboys.forEach {print($0)} // prints Manny, then Moe, then Jack
如果既需要索引号又需要元素,那么请调用enumerate实例方法并对结果进行循环;每次迭代得到的将是一个元组:
let pepboys = ["Manny", "Moe", "Jack"]for (ix,pepboy) in pepboys.enumerate { print("Pep boy /(ix) is /(pepboy)") // Pep boy 0 is Manny, etc.}// or:pepboys.enumerate.forEach {print("Pep boy /($0.0) is /($0.1)")}
Swift还提供了3个强大的数组转换实例方法。就像forEach一样,这些方法都会枚举数组,这样循环就被隐藏到了方法调用中,代码也变得更加紧凑和整洁。
首先来看看map实例方法。它会生成一个新数组,数组中的每个元素都是将原有数组中相应元素传递给你所提供的函数进行处理后的结果。该函数接收一个元素类型的参数,并返回可能是其他类型的结果;Swift通常会根据函数返回的类型推断出生成的数组元素的类型。
比如,如下代码演示了如何将数组中的每个元素乘以2:
let arr = [1,2,3]let arr2 = arr.map {$0 * 2} // [2,4,6]
如下示例演示了map实际上可以生成不同元素类型的新数组:
let arr = [1,2,3]let arr2 = arr.map {Double($0)} // [1.0, 2.0, 3.0]
下面是个实际的示例,展示了使用map后代码将会变得多么简洁和紧凑。为了删除UITableView中一个段中的所有单元格,我需要将单元格定义为NSIndexPath对象的数组。如果sec是段号,那么我就可以像下面这样分别构建这些NSIndexPath对象:
let path0 = NSIndexPath(forRow:0, inSection:sec)let path1 = NSIndexPath(forRow:1, inSection:sec)// ...
这里有个规律!我可以通过for...in循环行值来生成NSIndexPath对象的数组。不过,要是使用map,那么表示相同的循环就会变得更加紧凑(ct是段中的行数):
let paths = Array(0..<ct).map {NSIndexPath(forRow:$0, inSection:sec)}
实际上,map是CollectionType的一个实例方法,Range本身是个CollectionType。因此,我不需要转换为数组:
let paths = (0..<ct).map {NSIndexPath(forRow:$0, inSection:sec)}
filter实例方法也会生成一个新数组。新数组中的每个元素都是老数组中的,顺序也相同;不过,老数组中的一些元素可能会被去除,它们被过滤掉了。起过滤作用的是你所提供的函数;它接收一个元素类型的参数并返回一个Bool,表示这个元素是否应该被放到新数组中。
比如:
let pepboys = ["Manny", "Moe", "Jack"]let pepboys2 = pepboys.filter{$0.hasPrefix("M")} // [Manny, Moe]
最后来看看reduce实例方法。如果学过LISP或Scheme,那么你对reduce就会很熟悉;否则,初学起来会觉得很困惑。这是一种将数组中(实际上是序列)所有元素合并为单个值的方式。这个值的类型(结果类型)不必与数组元素类型相同。你提供了一个函数,它接收两个参数;第1个是结果类型,第2个是元素类型,结果是这两个参数的组合,它们作为结果类型。每次迭代的结果会作为下一次迭代的第一个参数,同时数组的下一个元素会作为第二个参数。因此,组合对不断累积的输出,以及最终的累积值就是reduce函数最终的输出。不过,这并没有说明第一次迭代的第一个参数来自哪里。答案就是你需要自己提供reduce调用的第一个参数。
通过一个简单的示例说明会加强理解。假设有一个Int数组,接下来我们可以通过reduce得到数组中所有元素的和。如下伪代码省略了reduce调用的第一个参数,这样你就可以思考它应该是什么了:
let sum = arr.reduce(/*???*/) {$0 + $1}
每一对参数都会一起添加进去,从而得到下一次迭代时的第一个参数。每次迭代时的第2个参数都是数组中的元素。那么问题来了,数组的第一个元素会与什么相加呢?我们想要得到所有元素的和,既不多也不少;显然,数组的第一个元素应该与0相加!下面是具体代码:
let arr = [1, 4, 9, 13, 112]let sum = arr.reduce(0) {$0 + $1} // 139
上述代码可以更加简洁一些,因为+运算符是所需类型的函数名:
let sum = arr.reduce(0, combine:+)
在我的iOS编程生涯中,我大量使用了这些方法,通常使用其中2个,或3个都用,将它们嵌套起来、链接起来,或二者结合起来使用。下面来看个示例;这个示例很复杂,但却能非常好地展现出通过Swift来处理数组是多么简洁。我有一个表视图,它将数据以段的形式展现出来。在底层,数据是个String数组的数组,每个子数组都表示段的行。现在,我想要过滤该数据,去除不包含某个子字符串的所有字符串。我想要保持段的完整性,不过如果删除字符串导致一个段的所有字符串都被删除,那么我需要删除整个段数组。
其核心是判断一个字符串是否包含了子字符串。这里使用的是Cocoa方法,因为可以通过它们执行不区分大小写的搜索。如果s是数组中的字符串,并且target是我们要搜索的子字符串,那么判断s是否不区分大小写地包含了target的代码如下所示:
let options = NSStringCompareOptions.CaseInsensitiveSearchlet found = s.rangeOfString(target, options: options)
回忆一下第3章介绍的rangeOfString。如果found不为nil,那就说明找到了子字符串。下面是具体代码,前面加上了一些示例数据:
let arr = [["Manny", "Moe", "Jack"], ["Harpo", "Chico", "Groucho"]]let target = "m"let arr2 = arr.map { $0.filter { let options = NSStringCompareOptions.CaseInsensitiveSearch let found = $0.rangeOfString(target, options: options) return (found != nil) }}.filter {$0.count > 0}
前两行代码设定了示例数据,剩下的是一条命令:一个map调用,其函数包含了一个filter调用,后面又链接了一个filter调用。如果上述代码都说明不了Swift有多么酷,那就没别的能够说明了。
8.Swift Array与Objective-C NSArray
在编写iOS应用时,你会导入Foundation框架(或是UIKit,因为它会导入Foundation),它包含了Objective-C NSArray类型。Swift的Array类型与Objective-C的NSArray类型是桥接的;不过,前提是数组中的元素类型可以桥接。相比于Swift,Objective-C对于NSArray中可以放置什么元素的规则既宽松又严格。一方面,NSArray中的元素不必是相同类型的。另一方面,NSArray中的元素必须是对象,因为只有对象才能为Objective-C所理解。一般来说,如果类型能够向上转换为AnyObject(这意味着它是个类类型),或是如Int、Double及String这样特殊的桥接结构体,那么它才可以桥接到Objective-C。
将Swift数组传递给Objective-C通常是很简单的。如果Swift数组包含了可以向上类型转换为AnyObject的对象,那么直接传递数组即可;要么通过赋值,要么作为函数调用的实参:
let arr = [UIBarButtonItem, UIBarButtonItem]self.navigationItem.leftBarButtonItems = arrself.navigationItem.setLeftBarButtonItems(arr, animated: true)
要想在Swift数组上调用NSArray方法,你需要将其转换为NSArray:
let arr = ["Manny", "Moe", "Jack"]let s = (arr as NSArray).componentsJoinedByString(", ")// s is "Manny, Moe, Jack"
Swift数组可以看出var引用是可变的,不过无论怎么看,NSArray都是不可变的。要想在Objective-C中获得可变数组,你需要NSArray的子类NSMutableArray。你不能将Swift数组通过类型转换、赋值或传递的方式赋给NSMutableArray,必须要强制进行。最佳方式是调用NSMutableArray的初始化器init(array:),你可以直接向其传递一个Swift数组:
let arr = ["Manny", "Moe", "Jack"]let arr2 = NSMutableArray(array:arr)arr2.removeObject("Moe")
将NSMutableArray转换回Swift数组只需直接转换即可;如果需要一个拥有原始Swift类型的数组,那就需要转换两次才能编译通过:
var arr = ["Manny", "Moe", "Jack"]let arr2 = NSMutableArray(array:arr)arr2.removeObject("Moe")arr = arr2 as NSArray as! [String]
如果Swift对象类型不能向上转换为AnyObject,那么它就无法桥接到Objective-C;如果需要一个NSArray,但你传递了一个包含这种类型的Array,那么编译器就会报错。在这种情况下,你需要手工“桥接”数组元素。
比如,我有一个Swift的CGPoint数组。这在Swift中是没问题的,不过由于CGPoint是个结构体,而Objective-C并不会将其视为一个对象,因此你不能将其放到NSArray中。如果在需要NSArray的地方传递了这个数组,那就会导致编译错误:“[CGPoint]is not convertible to NSArray.”。解决办法就是将每个CGPoint包装为NSValue,这是个Objective-C对象类型,专门用作各种非对象类型的载体;现在,我们有了一个Swift的NSValue数组,接下来就可以由Objective-C进行处理了:
let arrNSValues = arrCGPoints.map { NSValue(CGPoint:$0) }
另一种情况是Swift的Optional数组。Objective-C集合不能包含nil(因为在Objective-C中,nil不是对象)。因此,你不能将Optional放到NSArray中。在需要NSArray时如果传递Optional数组,那就需要事先对这些Optional进行处理。如果Optional包装了值,那么你可以将其展开。不过,如果Optional没有包装值(它是个nil),那么就无法将其展开。一种解决办法就是采取Objective-C中的做法。Objective-C NSArray不能包含nil,因此Cocoa提供了一个特殊的类NSNull,当需要一个对象时,其单例NSNull()可以代替nil。这样,如果有一个包装了String的Optional数组,那么我可以将那些不为nil的元素展开,并使用NSNull()替代nil元素:
let arr2 : [AnyObject] = arr.map{if $0 == nil {return NSNull} else {return $0!}}
(第5章将会进一步简化上述代码。)
现在来看看将NSArray从Objective-C传递给Swift时会发生什么。跨越桥接不会有任何问题:NSArray会安全地变成Swift Array。不过,这是个什么类型的Swift Array呢?就其本身来说,NSArray并没有携带关于它所包含的元素类型的任何信息。因此,默认就是Objective-C NSArray会转换为Swift的AnyObject数组。
幸好,现在不会像之前那样再遇到这种默认情况了。从Xcode 7开始,Objective-C语言发生了变化,NSArray、NSDictionary与NSSet的声明(这3种集合类型会桥接到Swift)已经包含了元素类型信息(Objective-C称为轻量级泛型)。在iOS 9中,Cocoa API也得到了改进,它们包含了这些信息。这样,在大多数情况下,从Cocoa接收到的数组都是带有类型的。
比如,如下优雅的代码在之前是不可能的:
let arr = UIFont.familyNames.map { UIFont.fontNamesForFamilyName($0)}
结果是一个String数组的数组,根据字体系列列出了所有可用的字体。上述代码可以编译通过,因为Swift会看到UIFont的这两个类方法返回了一个String数组。之前,这两个数组是没有类型信息的(它们都是AnyObject数组),你需要将其向下类型转换为String数组。
虽然不常见,但你还是有可能从Objective-C接收到AnyObject数组。如果出现了这种情况,那么通常你会将其进行向下类型转换,或是将其转换为特定Swift类型的数组。如下Objective-C类包含了一个方法,其NSArray返回类型不带元素类型:
@implementation Pep- (NSArray*) boys { return @[@"Mannie", @"Moe", @"Jack"];}@end
要想调用该方法并对结果进行处理,你需要将结果向下类型转换为String数组。如果确信这一点,那就可以执行强制类型转换:
let p = Peplet boys = p.boys as! [String]
不过,与任何类型转换一样,请确保你的转换是正确的!Objective-C数组可以包含多种对象类型。不要将这样的数组强制向下类型转换为并非每个元素都能转换的类型,否则一旦转换失败,程序将会崩溃;在排除或转换有问题的元素时要深思熟虑。
4.12.2 Dictionary
字典(Dictionary,是个结构体)是成对对象的一个无序集合。对于每一对对象来说,第1个对象是键,第2个对象是值。其用法是通过键来访问值。键通常是字符串,不过并不局限为字符串;形式上的要求是键的类型要使用Hashable协议,这意味着它们使用了Equatable,并且有一个hashValue属性(一个Int),这样两个相等的键就会拥有相等的散列值,而两个不相等的键的散列值也不等。因此,背后可以通过散列值实现键的快速访问。Swift的数字类型、字符串与枚举都是Hashable。
就像数组一样,给定的字典类型必须是统一的。键类型与值类型不必是相同类型,通常情况下其类型也不相同。不过在任何字典中,所有键的类型都必须是相同的,所有值的类型也必须是相同的。字典其实是个泛型,其占位符类型先是键类型,然后是值类型:Dictionary<Key,Value>。不过与数组一样,Swift为字典类型的表示提供了语法糖,通常情况下都会这么用:[Key:Value],即方括号中包含了一个冒号(以及可选的空格),两边是键类型与值类型。如下代码创建了一个空的字典,其键(如果存在)是String,值(如果存在)也是String:
var d = [String:String]
冒号还用于字典字面值语法中,用于分隔每一对键值。键值对位于方括号中,中间用逗号分隔,就像数组一样。如下代码通过字面值方式创建了一个字典(字典的类型[String:String]会被推断出来):
var d = ["CA": "California", "NY": "New York"]
空字典的字面值是个里面只包含了一个冒号[:]的方括号。如果通过其他方式获悉了字典的类型,那就可以使用这个符号表示。下面是创建空字典([String:String])的另一种方式:
var d : [String:String] = [:]
如果通过不存在的键获取值,那么不会出现错误,不过Swift需要通过一种方式告知你这个操作失败了;因此,它会返回nil。这反过来又会表示,如果成功通过一个键访问到了值,那么返回的值一定是个包装真实值的Optional!
我们常常通过下标访问字典的内容。要想根据键获取其值,请对字典引用应用下标,下标中是键:
let d = ["CA": "California", "NY": "New York"]let state = d["CA"]
不过请记住,在上述代码执行后,state并不是String,它是个包装了String的Optional!忘记这一点是很多初学者常犯的错误。
如果对字典的引用是可变的,那么你还可以对键下标表达式赋值。如果键已经存在,那么其值就会被替换。如果键不存在,那么它会被创建,并且将值关联到键上:
var d = ["CA": "California", "NY": "New York"]d["CA"] = "Casablanca"d["MD"] = "Maryland"// d is now ["MD": "Maryland", "NY": "New York", "CA": "Casablanca"]
还可以调用updateValue(forKey:);好处在于它会将旧值包装到Optional中返回,如果键不存在则会返回nil。
作为一种便捷方式,如果键存在,那么将nil赋给键下标表达式会将该键值对删除:
var d = ["CA": "California", "NY": "New York"]d["NY"] = nil // d is now ["CA": "California"]
还可以调用removeValueForKey;好处在于在删除键值对之前它会返回被删除的值。返回的被删除的值会包装到一个Optional中;因此,如果返回nil,那就表示这个键本来就不在字典中。
与数组一样,字典类型也可以进行向下类型转换,这意味着其中的每个元素都会进行向下类型转换。通常只有值类型会不同:
let dog1 : Dog = NoisyDoglet dog2 : Dog = NoisyDoglet d = ["fido": dog1, "rover": dog2]let d2 = d as! [String : NoisyDog]
与数组一样,is可用于测试字典中的实际类型,as?可用于测试并安全地进行类型转换。与数组相等性一样,字典相等性的工作方式与你想的是一样的。
1.基本的字典属性与枚举
字典有个count属性,它会返回字典中所包含的键值对数量;还有一个isEmpty属性,用于判断这个数量是否为0。
字典有个keys属性,它会返回字典中所有的键;还有一个values属性,它会返回字典中所有的值。它们都是没有对外公开的结构体(实际类型是LazyForwardCollection),不过在通过for...in枚举它们时,你会得到期望的类型:
var d = ["CA": "California", "NY": "New York"]for s in d.keys { print(s) // s is a String}
字典是无序的!你可以枚举它(键或值),但不要期望元素会以任何特定的顺序返回。
可以通过将keys属性或values属性转换为数组一次性获得字典的键或值:
var d = ["CA": "California", "NY": "New York"]var keys = Array(d.keys)
还可以枚举字典本身。你可能已经想到了,每次迭代都会得到一个键值对元组:
var d = ["CA": "California", "NY": "New York"]for (abbrev, state) in d { print("/(abbrev) stands for /(state)")}
可以通过将字典转换为数组,从而一次性以数组(键值对元组)的形式获得字典的全部内容:
var d = ["CA": "California", "NY": "New York"]let arr = Array(d) // [("NY", "New York"), ("CA", "California")]
就像数组一样,字典、其keys属性与values属性都是集合(CollectionType)与序列(SequenceType)。因此,上面所介绍的关于作为集合与序列的数组的一切也都适用于字典!比如,如果字典d有Int值,那么你可以通过reduce实例方法求出其和:
let sum = d.values.reduce(0, combine:+)
可以获取其最小值(包装在Optional中):
let min = d.values.minElement
可以列出符合某个标准的值:
let arr = Array(d.values.filter{$0 < 2})
(这里需要转换为Array,因为filter得到的序列是延迟的:直到枚举它或将其放到数组中后,它里面才会有内容)。
2.Swift Dictionary与Objective-C NSDictionary
Foundation框架中的字典类型是NSDictionary,而Swift的Dictionary类型会与其桥接。在双方之间传递字典就像之前介绍的数组那样。NSDictionary的无类型信息的桥接API的类型是[NSObject:AnyObject],它使用Objective-C Foundation对象基类作为键;之所以这么做有几个原因,不过从Swift的视角来看,主要的原因在于AnyObject并非Hashable。另一方面,NSObject被Swift API扩展了,并且使用了Hashable;由于NSObject是Cocoa类的基类,因此任何Cocoa类型都是NSObject。这样,NSDictionary就可以桥接了。
就像NSArray一样,NSDictionary的键与值类型现在可以在Objective-C中标记了。在实际的Cocoa NSDictionary中,最常使用的键类型是NSString,因此接收到的NSDictionary会是个[String:AnyObject]。不过,NSDictionary中拥有特定类型值的情况并不多见;传给Cocoa或从Cocoa接收的字典通常具有不同类型的值。一个字典的键是字符串,但值包含了字符串、数字、颜色以及数组这一情况是非常常见的。出于这一原因,你通常不会对整个字典的类型进行向下类型转换;相反,你在使用字典时会将值看作AnyObject,在从字典中获取到单个值时才进行类型转换。由于从下标键中返回的值本身是个Optional,所以常常需要先展开值,然后再进行类型转换。
下面是个示例。Cocoa NSNotification对象有个userInfo属性。它是个NSDictionary,本身可能为nil,因此Swift API是这样描述它的:
var userInfo: [NSObject : AnyObject]? { get }
假设这个字典包含一个"progress"键,其值是个NSNumber,值里面包含着一个Double。我的目标是将该NSNumber提取出来,并将其包含到Double赋给属性self.progress。下面是一种安全的做法,使用可选展开与可选类型转换(n是个NSNotification对象):
let prog = (n.userInfo?["progress"] as? NSNumber)?.doubleValueif prog != nil { self.progress = prog!}
这是个Optional链,链的最后会获取NSNumber的doubleValue属性;因此,prog的隐式类型是个包装了Double的Optional。上述代码是安全的,因为如果没有userInfo属性,或字典中不包含"progress"键,或键的值不是个NSNumber,那么什么都不会发生,prog值就是nil。接下来判断prog是否为nil;如果不是,那我就可以安全地强制展开它了,并且展开的值就是我所期望的Double。
(第5章将会介绍完成相同事情的另一种语法,使用了条件绑定。)
与之相反,下面这个示例会创建一个字典并将其传递给Cocoa。该字典是个混合体:其值包含了UIFont、UIColor及NSShadow;其键都是字符串,并且是从Cocoa中获取的常量。我以字面值形式构造这个字典,然后将其传递过去,整个过程一步搞定,完全不需要类型转换:
UINavigationBar.appearance.titleTextAttributes = [ NSFontAttributeName : UIFont(name: "ChalkboardSE-Bold", size: 20)!, NSForegroundColorAttributeName : UIColor.darkTextColor, NSShadowAttributeName : { let shad = NSShadow shad.shadowOffset = CGSizeMake(1.5,1.5) return shad }]
与NSArray和NSMutableArray一样,如果希望Cocoa能够修改字典,那就需要将其转换为NSMutableDictionary。在如下示例中,我想要连接两个字典,因此使用了NSMutableDictionary,它有一个addEntriesFromDictionary:方法:
var d1 = ["NY":"New York", "CA":"California"]let d2 = ["MD":"Maryland"]let mutd1 = NSMutableDictionary(dictionary:d1)mutd1.addEntriesFromDictionary(d2)d1 = mutd1 as NSDictionary as! [String:String]// d1 is now ["MD": "Maryland", "NY": "New York", "CA": "California"]
这种事情经常会遇到,因为并没有将一个字典的元素添加到另一个字典中的原生方法。实际上在Swift中,与字典相关的原生辅助方法数量是非常少的:其实根本就没有。Cocoa与Foundation框架还可以为我们所用,也许Apple觉得没必要在Swift标准库中重复Foundation中已经存在的那些功能。如果觉得使用Cocoa很麻烦,那么你可以编写自己的库;比如,我们可以通过扩展轻松将addEntriesFromDictionary:重新实现为Swift Dictionary的实例方法:
extension Dictionary { mutating func addEntriesFromDictionary(d:[Key:Value]) { // generic types for (k,v) in d { self[k] = v } }}
4.12.3 Set
集合(Set,是个结构体)是不重复对象的一个无序集合。它非常类似于字典的键!其元素必须是相同类型的,它有一个count属性和一个isEmpty属性;可以通过任意序列进行初始化;你可以通过for...in遍历其元素。不过,元素的顺序是不确定的,你不应该假定元素的顺序。
Set元素的唯一性是通过限制其类型使用Hashable协议来做到的,就像Dictionary的键一样。因此,背后可以使用散列值来加速访问。你可以通过contains实例方法判断一个集合中是否包含了给定的元素,其效率要比对数组进行相同的操作高很多。因此,如果元素的唯一性是可以接受的(或需要这样),并且不需要索引,也不需要确保顺序,那么相比于数组,Set会是一个更好的选择。
集合的元素是Hashables,这意味着它们一定也都是Equatables。这是非常有意义的,因为唯一性这个概念取决于能够回答给定对象在集合中是否已经存在这一问题。
Swift中并没有Set字面值,你也不需要,因为在需要集合的地方可以传递一个数组字面值。Swift也没有提供集合类型表示的语法糖,因为Set结构体是一个泛型,因此可以通过显式特化泛型来表示类型信息:
let set : Set<Int> = [1, 2, 3, 4, 5]
不过在上述示例中,我们无须特化泛型,因为Int类型可以通过数组推断出来。
很多时候你想要获取到集合中的某一个元素作为样本。由于顺序是无意义的,因此获取任意一个元素就可以,如第一个元素。如果出于这个目的,你可以使用first实例属性;它会返回一个Optional,以防止集合为空,没有第一个元素。
集合的标志性特性在于其对象的唯一性。如果将对象添加到集合中,同时集合中已经包含了该对象,那么它就不会被再次添加进去。将数组转换为集合,然后又将集合转换为数组是一种快速且可靠的确保数组元素唯一性的方式,不过数组元素的顺序并不会保留下来:
let arr = [1,2,1,3,2,4,3,5]let set = Set(arr)let arr2 = Array(set) // [5,2,3,1,4], perhaps
Set是一种集合(CollectionType),也是个序列(SequenceType),这类似于数组与字典,之前介绍的关于这两种类型的一切也都适用于Set。比如,Set有map实例方法;它返回一个数组,当然,如果需要也可以将其转换回Set:
let set : Set = [1,2,3,4,5]let set2 = Set(set.map {$0+1}) // {6, 5, 2, 3, 4}, perhaps
如果对集合的引用是可变的,那就有很多实例方法可供使用了。你可以通过insert向集合添加对象;如果对象已经在集合中,那就什么都不会发生,但也没有问题。可以通过remove方法从集合中删除指定对象并返回;它会返回包装在Optional中的对象,如果对象不存在,那么该方法会返回nil。可以通过removeFirst方法删除并返回集合中的第1个对象(无论第1个指的是什么);如果集合为空,那么应用就会崩溃,因此请小心行事(或使用安全的popFirst)。
集合的相等性比较(==)与你期望的是一致的;如果一个集合中的每个元素都与另一个集合中的元素相等,那么这两个集合就是相等的。
如果集合的概念让你想起了小学时学到的文氏图,那就太好了,因为集合提供的实例方法可以让你实现当初学到的所有集合操作。参数可以是集合,也可以是序列(会被转换为集合);比如,可以是数组、范围,甚至是字符序列:
intersect、intersectInPlace
找出该集合与参数中都存在的元素。
union、unionInPlace
找出该集合与参数中元素的合集。
exclusiveOr、exclusiveOrInPlace
找出在该集合,但不在参数中的元素,以及在参数,但不在该集合中的元素的合集。
subtract、subtractInPlace
找出在该集合,但不在参数中的元素。
isSubsetOf、isStrictSubsetOf
isSupersetOf、isStrictSupersetOf
返回一个Bool值,判断该集合中的元素是否都在参数中,或判断参数中的元素是否都在该集合中。如果两个集合包含了相同的元素,那么“strict版本”就会返回false。
isDisjointWith
返回一个Bool值,判断该集合和参数是否没有相同的元素。
如下示例演示了如何优雅地使用Set,它来自于我所编写的一个应用。应用中有很多带编号的图片,我们要从中随机选取一个。不过,我不想选取最近已经选取过的图片。因此,我维护了一个最近选取过的所有图片的编号列表。在选取新的图片时,我会将所有编号的列表转换为一个Set,同时将最近选取过的图片的编号列表转换为一个Set,然后二者相减得到一个未使用过的图片编号列表!现在,我可以随机选取一个图片编号,并将其添加到最近使用过的图片编号列表中:
let ud = NSUserDefaults.standardUserDefaultsvar recents = ud.objectForKey(RECENTS) as? [Int]if recents == nil { recents = }var forbiddenNumbers = Set(recents!)let legalNumbers = Set(1...PIXCOUNT).subtract(forbiddenNumbers)let newNumber = Array(legalNumbers)[ Int(arc4random_uniform(UInt32(legalNumbers.count)))]forbiddenNumbers.insert(newNumber)ud.setObject(Array(forbiddenNumbers), forKey:RECENTS)
1.Option Set
Option Set(从技术上来说是OptionSetType)是Swift提供的将Cocoa中常用的一些枚举类型当作结构体的一种方式。严格来说,它并不是Set;不过看起来像是个Set,它通过SetAlgebraType协议实现了Set的诸多特性。因此,Option Set也拥有contains、insert、remove方法,以及各种集合操作方法。
Option Set的目的在于帮助你处理Objective-C的位掩码。位掩码是个整型,当同时指定多个选项时,它们用作开关。这种位掩码在Cocoa中用得非常多。在Objective-C以及Swift 2.0之前,我们通过算术按位或和按位与运算符来操纵位掩码。这种操作令人感到不可思议,并且极易出错。多亏了Option Set,在Swift 2.0中,我们可以通过集合操作来操纵位掩码。
比如,在指定UIView动画时,我们可以传递一个options:实参,它的值来自于UIViewAnimationOptions枚举,其定义(在Objective-C中)以如下内容开始:
typedef NS_OPTIONS(NSUInteger, UIViewAnimationOptions) { UIViewAnimationOptionLayoutSubviews = 1 << 0, UIViewAnimationOptionAllowUserInteraction = 1 << 1, UIViewAnimationOptionBeginFromCurrentState = 1 << 2, UIViewAnimationOptionRepeat = 1 << 3, UIViewAnimationOptionAutoreverse = 1 << 4, // ...};
假设一个NSUInteger是8位(实际上不是,这里做了一些简化)。那么,该枚举(在Swift中)定义了如下名值对:
UIViewAnimationOptions.LayoutSubviews 0b00000001UIViewAnimationOptions.AllowUserInteraction 0b00000010UIViewAnimationOptions.BeginFromCurrentState 0b00000100UIViewAnimationOptions.Repeat 0b00001000UIViewAnimationOptions.Autoreverse 0b00010000
可以将这些值组合为单个值(位掩码),并将其作为动画的options:实参传递过去。为了理解你的意图,Cocoa只需查看你所传递的值中哪些位被设为了1。比如,0b00011000表示UIViewAnimationOptions.Repeat与UIViewAnimationOptions.Autoreverse都为true(也就表示其他都是false)。
问题在于如何构造值0b00011000来传递。你可以直接将其构造为字面值,并将options:实参设为UIViewAnimationOptions(rawValue:0b00011000);不过,这么做可不太好,因为极易出错,并且会导致代码难以理解。在Objective-C中,你可以使用算术按位或运算符,这类似于如下Swift代码:
let val = UIViewAnimationOptions.Autoreverse.rawValue | UIViewAnimationOptions.Repeat.rawValuelet opts = UIViewAnimationOptions(rawValue: val)
不过在Swift 2.0中,UIViewAnimationOptions类型是个Option Set结构体(因为它在Objective-C中被标记为NS_OPTIONS),因此可以像Set一样使用它。比如,给定一个UIViewAnimationOptions值,你可以通过insert向其添加一个选项:
var opts = UIViewAnimationOptions.Autoreverseopts.insert(.Repeat)
此外,还可以从数组字面值开始,就像初始化Set一样:
let opts : UIViewAnimationOptions = [.Autoreverse, .Repeat]
要想不设定选项,请传递一个空的Option Set()。这是相对于Swift 1.2及之前的版本一个较大的变化(之前的约定是传递nil),这是不合逻辑的,因为该值永远不会为Optional。
相反的情况是Cocoa传递给你一个位掩码,你想知道是否设置了其中某一位。在这个来自于UITableViewCell子类的示例中,单元格的state以位掩码的形式传递给了我们;我们想要知道表示单元格是否显示其编辑控件的位信息。过去,我们需要提取出原始值并使用按位与运算符:
override func didTransitionToState(state: UITableViewCellStateMask) { let editing = UITableViewCellStateMask.ShowingEditControlMask.rawValue if state.rawValue & editing != 0 { // ... the ShowingEditControlMask bit is set ... }}
这么做太容易出错了。在Swift 2.0中,它是个Option Set,因此使用contains方法即可:
override func didTransitionToState(state: UITableViewCellStateMask) { if state.contains(.ShowingEditControlMask) { // ... the ShowingEditControlMask bit is set ... }}
2.Swift Set与Objective-C NSSet
Swift的Set类型会桥接到Objective-C NSSet,中间类型是Set<NSObject>,因为NSObject会被看作Hashable。当然,同样的规则也适用于数组。Objective-C NSSet要求元素是类实例,Swift则会进行桥接。在实际开发中,你可能会使用一个数组,然后将其转换为集合或传递给需要集合的地方,如下示例来自于我所编写的代码:
let types : UIUserNotificationType = [.Alert, .Sound] // a bitmasklet category = UIMutableUserNotificationCategorycategory.identifier = "coffee"let settings = UIUserNotificationSettings( // second parameter is an NSSet forTypes: types, categories: [category])
如果Objective-C不知道这个Set是什么类型,那么从Objective-C返回的就是一个NSObject Set,在这种情况下,你可以对其进行向下类型转换。不过与NSArray一样,现在可以对NSSet进行标记以表示其元素类型;很多Cocoa API都已经被标记了,因此无需类型转换:
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) { let t = touches.first // an Optional wrapping a UITouch // ...}