iOS9 with Swift 集合类型(一)
集合类型:
Swift,和其他现代编程语言一样,有内置的集合类型数组(Array)和字典(Dictionary),还增加了第三种类型集合(Set)。数组和字典非常重要以至于Swift提供了很多特殊的语法。同时像其他的大部分Swift类型,它也提供了一些相关的的函数;NSArray和NSDictionary也会补充一些丢失的函数。而集合类型则是桥接至Cocoa的NSSet。
数组(Array):
数组(结构体)是有序的对象实例的集合(数组的元素),可以使用Int类型的序号来取出元素。而且序号是从0开始的。比如一个数组有四个元素,那么它就是从0到3。Swift的数组是非空的,所以如果有Index3那么就要有0—2.
最显著的Swift数组特性是其严格类型检验。不想有些其他的语言,Swift数组的元素必须统一,也就是说,其必须是由确定的一种类型的元素所组成的。即便是空数组也必须要有确定的类型。元素类型不同的两个数组被认为是两种不同的类型,数组类型依照元素类型遵循多态:如果NoisyDog是Dog的子类,那么元素是NoisyDog的数组就可以用在Dog元素数组的地方。哈哈,没错这个和可选值一样也是泛型声明,即Array<Element>,Element就是元素的类型。
事实上数组类型同一限制,可能没有你想象的那么宴客。数组的元素必须是一种类型,但是类型却是十分领会的。通过选择合适的数组类型你可以制作出实际上内部有多种类型的数组,比如:
1、如果NoisyDog是Dog的子类,那么Dog的数组就可以包含NoisyDog的元素。
2、如果Bird和Insect都采用了Flier协议,Flier为元素的数组就可以包含Bird或者Insect。
3、AnyObject元素数组就可以包含任何Swift的类的实例和桥接类的实例,比如Int,String、Dog。
4、一种类型可能承载多个类型,比如枚举类型就可以在关联值中有Int、String等等类型。
声明数组的类型既可以用泛型声明:Array<Int>,也可以用语法糖声明:[Int]。后者你会经常使用到。
字面数组可以使用包含在方括号中的一系列被逗号分隔的元素来表示,空数组的字面表示形式就是:[]。
数组的默认构造器init(),你可以在后面加一对空的圆括号来表示空数组:
1如果引用量类型可以提前知道,那么该空数组可以推迟其类型,因此你也可以这样来创建空数组:
2如果你用非空的字面元素来开始,那么你就不需要声明数组的类型,因为Swift会通过元素类型来推测出数组的类型。比如Swift会推测[1,2,3]为Int数组。如果数组包括父类和子类,那么Swift将会推测其为父类类型数组。甚至[1, "howdy"]这样的数组也是可以的,不过其类型是NSObject。然而在某些情况下,即使你用字面量来声明,你最好也现将引用量的类型显式声明:
3数组也有参数是序列的构造器,这就说明如果是序列类型,你就可以将它分开来充当数组的元素。比如:
1、Array(1...3)就能产生Int类型的数组[1,2,3]。
2、Array("hey".character)就会产生["h", "e","y"]。
3、Array(d) 其中d是字典,就会产生key-value对的元组类型数组。
还有一种构造器 init(count:repeatValue:),可以用来初始化重复值。比如我要初始化一百个nil值在String?数组中。
let strings : [String?] = Array(count:100, repeatedValue: nil)
这是你在Swift中能得到的最接近于缺失数组的方法:你有100个空位可以放对象。
数组转型和类型检查:
当你将一个数组分配、传递以及转型到另一个数组的时候,你是在每个元素的层面上去操作的:
4这段代码事实上是简写:将Int类型的数组赋给Int?类型数组,实际上是,每一个原数组中的元素都要进行封包。事实上是这样的情况:
5类似地,假如NoisyDog是Dog的子类,下面的代码是合法的:
6在第三行,我们建立了一个Dog数组。第四行我们将Dog数组转型为NoisyDog数组,这就意味着我们将每个元素分别转为NoisyDog类。
你可以用is运算符测试元素类型来测试数组的类型:
7如果每个元素均为NoisyDog,那么结果就是True。
同样的,as?运算符将把一个数组转为包在可选值中的数组,如果无法下行转换,结果将是nil。
8数组转型的目的和其他类型的转型是类似的——你可以发送合适的消息给转型后的数组。如果NoisyDog声明了Dog类没有的方法,你就不能向Dog类型的数组发送该方法。这样你就需要将元素转型为NoisyDog,然后编译器才会让你去进行发送方法消息。你既可以将其中的元素进行转型,也可以将整个数组进行转型。
数组的比较:
如果两个数组元素数一样且全部相等的话,那么就是相等的。
9 10数组是值类型:
由于数组是结构体,所以其不是引用类型,而是值类型。这就意味着,每一次数组被分配到变量或者作为函数参数,它都会被复制一份。然而我不是在暗示这种复制的代价很大。如果数组的引用是一个常量,那么显然复制就不是必要的了,甚至从其他数组产生新数组的操作或者转变数组的操作已经是非常高效的了。
你只需相信Swift的设计者已经考虑过这个问题,而且在后台处理数组是十分高效的。
虽然数组本身是值类型,但是其元素是引用类型的还是按照引用类型来对待。特别是类的实例为元素的数组,如果被多个变量引用。那么就会产生多个引用这一个相同的实例。
数组下标:
数组结构体利用下标方法允许通过在引用变量后面添加包含值的方括号来取出元素。值可以是Int比如arr[1] 就是取出arr数组的第二个值。值还可以是Int的范围,比如,arr[1...2]就是去取出第二个和第三个值,严格的说,这会产生叫做ArraySlice的东西,其非常类似于Array,比如你可以用和数组一样的下标来取出ArraySlice的元素;而且它也可以用在数组能用的地方。一般地,你就把他当做是数组就可以了。
如果数组的引用是可变的(即var,而不是let),那么下标的表达方法也可以用来被分配值。这会改变那个被下标表达元素的值,被分配的值必须是与数组元素的类型一致。
11如果下标是一段区间,那么赋的值就必须是一个数组,这有可能改变数组的长度:
12下标超过应有的范围就会引起运行错误。
嵌套数组:
数组的元素是数组是合法的。比如:
13它是Int类型数组的数组。它的类型声明是[[Int]]。(没有规定说,内部的数组必须是相同长度,上面只是为可了便于表达)
为了得到内部的数组元素,你可以使用下标链:
14如果外部数组的引用是可变的,你还可以进行写入:
15你可以用其他方法改编内部数组,比如你可以为它添加新的元素。
基本数组属性和方法:
数组是一个集合(采用CollectionType协议),而集合又是一个序列(采用SequenceType协议)。如果这些项有熟悉的情景,如同String的characters。从这个方面来看,它与字符序列是很像的。
作为集合,数组的count只读属性表示元素的个数。当count为0时候,isEmpty属性就是true。
数组的first和last只读属性返回了它的第一个和最后一个元素,但是它们均被可选值包裹,因为数组有可能为空,此时它们的值就是nil。这是在Swift中比较少见的双重可选的情况,比如,当你声明一个类型为Int?的数组的时候,此时如果为nil,那么就是Int??。
数组的最大下标比count要少1,有时你需要引用count来计算得出下标是多少。比如你可以这样来得到数组的后两位,
16Swift不允许你使用负数作为这种计算的捷径,而另一方面,如果你想要得出倒数几位元素,不妨使用suffix方法:
17对于suffix和prefix方法,都允许范围超出:
18通过前缀的个数多少来取出元素,还可以用前缀的序数来表示取出元素:
19数组的starIndex属性是0,endIndex属性则是count的值。此外,indices属性是以starIndex和endIndex为端点的半开区间。也就是可以取出全部元素的范围。如果你的数组的引用是可变的,你可以改变startIndex和endIndex来获得新的区间。
20indexOf方法会返回某个元素第一次出现的下标,要注意此值是可选值。如果该数组的元素是可比较相等的,那么就可以用==来识别是不是要找的元素。
21即使数组不是可比较相等的元素,你也是可以用自己的函数(用元素为参数,返回bool值)来比较。
22作为序列,数组还有contains方法来验证它是否包含某个元素,同样如果元素是可等的,你也可以用==运算符,或者你可以提供自己的函数(参数为元素类型,返回值为Bool)。
23starWith方法则可以用来检验一个数组的开始元素是不是与给定的序列符合,或者你也可以用==运算符,或者你可以提供自己的函数(参数为元素类型,返回值为Bool):
24elementsEqual方法则从序列的一般原则比较数组是否相同:两个序列必须等长,而且每个元素相等。你也可以用==运算符,或者你可以提供自己的函数(参数为元素类型,返回值为Bool)。
25minElement和maxElement返回最小或者最大的元素,同样会被包在可选值中以防为空数组。如果元素可比,你也可以用<运算符,或者你可以提供自己的函数(参数为元素类型,返回值为Bool)。第三行是验证是否绝对值较小的数是两个中的前者。
26如果数组的引用是可变的,append和appendContentsOf这两个实例方法都可以在数组末尾添加新元素。两者的差别是,append参数只是元素类型的单个值,而appendContentsOf则需要一个序列作为参数。比如:
27+运算符被重载为类似于appendContentsOf(不是append)。当+前的运算元是数组时,它不会产生新的数组,所以即使引用的数组是常量也可以用。如果引用是变量,就可以用+=来代替:
28如果数组的引用量是可变的,实例方法insert(atIndex:)将单个元素插入到所给的下标处。如果要一次插入多个元素,可以使用insertContentsOf(at:)方法。
如果数组的引用量是可变的,实例方法removeAtIndex可以将该下标的元素移除,实例方法removeLast则会移除最后一个元素,removeFirst则会移除第一个元素。这些方法也会将移除的元素返回,如果你不需要返回值,就不用理他们了。这些值不会被包在可选值里面,并且如果越界会引起崩溃。removeLast还可以引入Int参数,来移除多个值,但是不会返回值,而且如果越界会引起崩溃。
另一方面,popFirst和poplast将会返回可选值,所以即使是空数组也是安全的。如果引用量是不可变的,你可以用dropFirst和dropLast来返回改变了的数组(确切的说是ArraySlice)。
实例方法joinWithSeparator以嵌套数组开头,它会取出单独的数组元素,然后将参数数组插入到原数组元素之间。最后的结果就是被分割的数组,即JoinSeparater,最后在强制转换成数组:
29调用JoinWithSeparater,以空数组为参数可以“夷平”数组:
30还有专门的实例方法flatten,它会返回JoinSeparater,然后在强制转换为数组。
reverse实例方法会产生新的数组,其元素是倒过来的。
sortInplace和sort分别能将原始数组整理顺序,和产生新的数组。同样你有两个选择,若果是可比较的就可以用<运算符,或者提供一个函数接受两个参数并返回一个bool值,
31在最后一行,我提供了一个匿名函数,或者你可以传递一个已经声明了的函数。在Swift中,比较运算符就是它的函数名,因此我可以更简洁地这样做:
32split实例方法将数字分成嵌套数组,并可以传递测试,通过测试的将会被删除。
33数组枚举和变形:
数组是序列,所以你可以将它枚举化,通过按顺序检查和操作每个元素。最简单的就是for in循环。
34或者还可以使用forEach实例方法,它的参数是一个函数,该函数的参数是数组的一个元素,没有返回值。它其实就是与for in功能类似的函数。
35如果你还需要元素的下标,调用enumerate实例方法并在结果处循环,这样每次循环得到的都是一个元组。
36Swift还提供了,三个有效的数组变形实例方法。比如forEach,这些方法都会为你枚举化数组,所以循环是隐含在方法调用中的,这会使你的代码更加简洁。
我们先从map实例方法开始,它会产生新数组,其中的新元素都是原数组的元素经过我们所提供函数处理的结果。该函数的参数是数组元素的类型,然后返回的值是有可能为其他类型的值。Swift会自动推断返回的数组元素的类型。
比如:可以为数组的每个元素都乘以2
37下面这个例子是说map可以产生不同类型的数组:
38实际开发中的例子:想要将UITableView中的一个section的cell放到同一个NSObject数组中去,sec是section的序号:
39或者可以用更简洁的map方法:
40由于map实际上是CollectionType的实例方法,而区间(本身就是CollectionType),所以可以进一步简写:
41filter实例方法也会产生一个新数组,然而其中的元素是经过过滤的原数组元素,你可以提供具体的过滤函数,该函数接受一个元素类型值并返回一个bool值来验证是否要把该元素过滤掉。
42最后就是reduce实例方法,如果你接触过LISP或者Scheme,你可能对其很熟悉,否则你可能会觉得有点晦涩。这是一种将序列中所有元素都结合在一起的方法,它会产生唯一的值,该值不一定要与原数组元素的类型一样。你需要提供的函数的值有两个参数,第一个参数是结果类型的值,第二个参数是原数组元素的类型的值。每次迭代的结果会自动成为下一次的第一个参数,第二个参数则为下一个数组元素。不过你需要提供第一次迭代时的第一个参数。最后结果就是该值和原数组元素的积累,并且其类型为该值的类型。
每一个参数对都会被加总在一起,然后作为下一次迭代的第一个参数,如果我们要的是全部数组元素的加总,那么显然这个值应该是0。
43再一次,我们可以精简这段代码,因为+就是需要函数的名字:
44在实际编程中,我相当以来这几个方法,经常用两个以上,将他们嵌套或者链起来。这里有个例子,虽然看起来有点复杂,但是却很好地解释了如何使用Swift数组。
45Swift Array&Oc NSArray:
当你在进行iOS编程的时候,你会将Foundation框架import(或者可以导入UIKit,此框架是已经导入Foundation的),这样Oc的NSArray也会被导入。Swift的Array桥接了Oc的NSArray,但是这种桥接只有在数组类型是可以桥接的时才可以。Oc中能成为数组元素的条件比于Swift既是宽松的又是严格的。NSArray的元素不一定都是一样的类型,另一方面Oc的数组元素必须是对象,这和Oc如何理解对象有关。一般地,如果它可以被向下转型AnyObject,它就可以进行桥接。这就是说要不它是类,要不它是特殊的可以进行桥接的结构体,如Int、Double或String。
由此将Array传递给Oc是容易的。若你的Array的元素均可转型AnyObject,那么你可以直接进行传递。通过分配或者作为函数的参数。
若要在Swift的Array中调用NSArray的方法,你可以把它转型为NSArray:
47Swift的Array只要引用类型为var声明就是可改变的,而NSArray无论是声明什么都是不可以改变的。NSMutableArray才是正解,它是NSArray的子类。你无法转型,分配,传递一个Swift Array到NSMutableArray。你需要先强制变换类型。最好方法就是直接调用NSMutableArray 构造器 init(array:),你可以直接将Array作为参数传入。
48将NSMutableArray变为Array,则可以用转型(Cast),如果你想要一个原始Swift类型的数组,你需要进行两次转型:
49如果一个Swift对象类型无法转型为AnyObject,那么他就无法桥接至Oc,如果你试着将一个包含该类型实例的数组传递到需要NSArray的地方,编译器就会阻止你。在这种情况,你需要自己进行桥接。
比如有一个类型是CGPoint的Array,虽然在Swift中没有任何问题,但是CGPoint是一个结构体,在Oc中就无法使用,你也就不能将它放在NSArray中,如果你试着将它传递到需要NSArray的地方,就会有“[CGPoint]is not convertible to NSArray.”的错误报告。对策是:将每个CGPoint包进一个NSValue中(Oc的对象类型,专门是非对象类型的容器),这样我们就可以有一个类型是NSValue的数组,并可以提交给OC。
50另一种情况就是,可选的Swift数组,Oc的集合类型是不能包含nil的,毕竟在Oc中nil就不是一个对象。所以你就不能把这种包含可选值的数组直接传给需要NSArray的地方。在传递之前,你需要做点事:如果可选值包含值,你可以先进行解包,但是如果它不包含值,就没法解包(nil)。对此,你可以用Oc的NSNull类,它的唯一实例就是NSNull( ),可以代表nil。因此对于一个可选值包着字符串的数组就可以在不是nil的元素上进行解包,在是nil的地方进行NSNull()代替。
51现在再来谈谈Oc的NSArray到了Swift的情形,这样方向的桥接是没有任何问题的:NSArray可以很安全地变为Array。但是这样转变来的数组的类型是什么呢?要知道NSArray并没有带着其元素的类型信息。因此默认地NSArray将会变成类型是AnyObject的数组。
不过幸运的是,从Xcode7开始,Oc改变了其NSArray, NSDictionary, or NSSet这三种可桥接至Swift的集合类型,使其变得能携带元素类型信息了(OC叫这个是lightweight generic)。而在iOS9中,Cocoa API也修改为可以包含该信息。因此,绝大多数你从Cocoa接受的数组可以直接被识别类型。
比如,这种简洁的代码在以前是无法实现的:
52其结果是一个包含字符串数组的数组,分类列出了所有可用的字体。这在以前是不可能的,因为UIFont的这两种方法之前都是不可见的(他们以前会被认为是AnyObject数组),而现在会被认为是返回字符串数组的函数。
不过还是很有可能遇到从Oc接受AnyObject数组的情况的,如果真的遇到了,你就需要将其转型或者变为某种特定的Swift类型。
下面就是一个Oc类包含一个返回NSArray的方法(没有被类型标记):
53调用该方法并转换结果,就需要将结果转型为String的数组,
54Oc的数组可以包含多种类型的对象,不要强制将这种数组变为其中元素不能转换的类型,否则就会崩溃。对于这种情况要对症下药!