Swift——数组
数组和可变性
在Swift中最常见的集合类型非数组莫属。数组是一系列相同类型的元素的有序的容器,对于其中每个元素,我们可以使用下标对其直接进行访问(这又被称作随机访问)。举个例子,要创建一个数字的数组,我们可以这么写:
// 斐波那契数列
let fibs = [0,1,1,2,3,5]
要是我们使用像是append(_:)这样的方法来修改上面定义的数组的话,会得到一个编译错误。这是因为在上面的代码中数组使用let
声明为常量的。在很多情景下,这是正确的做法,它可以避免我们不小心对数组做出改变。如果我们想按照变量的方式来使用数组,我们需要将它用var
来进行定义:
var mutableFibs = [0,1,1,2,3,5]
现在我们就能很容易地为数字添加单个或是一系列元素了:
mutableFibs.append(8)
mutableFibs.append(contentsOf: [13, 21])
mutableFibs // [0, 1, 1, 2, 3, 5, 8, 13, 21]
区别使用var
和let
可以给我们带来不少好处。使用let
定义的变量因为其具有不变性,因此更有理由被优先使用。当你读到类似let fibs = ...
这样的声明时,你可以确定fibs的值将永远不变,这一点是由编译器强制保证的。这在你需要通读代码的时候会很有帮助。不过,要注意这只针对那些具有值语义的类型。使用let
定义的类实例对象(也就是说对于引用类型)时,它保证的是这个引用永远不会发生变化,你不能在给这个引用赋一个新的值,但是这个引用所指向的对象却是可以改变的。
数组和标准库中的所有集合类型一样,是具有值语义的。当你创建一个新的数组变量并且把一个已经存在的数组复制给他的时候,这个数组的内容会被复制。举个例子,在下面的代码中,x将不会被更改:
var x = [1,2,3]
var y = x
y.append(4)
y //[1,2,3,4]
x //[1,2,3]
var y = x
语句复制了x,所以在将4添加到y末尾的时候,x并不会发生改变,它的值依然是[1,2,3]。当你把一个数组传递给一个函数时,会发生同样的事情;方法将得到这个数组的一份本地复制,所有对它的改变都不会影响调用者所持有的数组。
对比一下Foundation框架中的NSArray在可变性上的处理方法。NSArray中没有更改方法,想要更改一下数组,你必须使用NSMutableArray。但是,就算你拥有的是一个不可变的NSArray,但是它的引用特性并不能保证这个数组不会被改变:
let a = NSMutableArray(array: [1,2,3])
//我们不想让b发生改变
let b: NSArray = a
//但是事实上他依然能够被a影响并改变
a.insert(4,at: 3)
b//(1,2,3,4)
//正确的方式在赋值时,先手动进行复制:
let c = NSMutableArray(array: [1,2,3])
//我们不想让d发生改变
let d = c.copy() as! NSArray
c.insert(4,at: 3)
d//(1,2,3)
在上面的例子中,显而易见,我们需要进行复制,因为a的声明毕竟是可变的。但是,当把数组在方法和函数之间来回传递的时候,事情可能就不那么明显了。
而在Swift中,相较于 NSArray 和 NSMutableArray俩种类型,数组只有一种统一的类型,那就是Array。使用var
可以将数组定义为可变,但是区别于与NS的数组,当你使用let
定义第二个数组,并将第一个数组赋值给它,也可以保证这个新的数组是不会改变的,因为这里没有共用的引用。
创建如此多的复制有可能造成性能问题,不过实际上Swift标准库中的所有集合类型都使用了“写时复制”这一技术,它能够保证只在必要的时候对数据进行复制。在我们的例子中,直到y.append被调用的之前,x和y都将共享内部的存储。在结构体和类中我们也将仔细的研究值语义,并告诉你如何为你自己的类型实现写时复制特性。
数组和可选值
Swift数组提供了你能想到的所有常规操作方法,像是isEmpty或是count。数组也允许直接使用特定的瞎编直接访问其中的元素,像是fibs[3]。不过要牢记在使用下标回去元素之前,你需要确保索引值没有超出范围。比如取索引值为3的元素,你需要保证数组中至少有4个元素。否则,你的程序会崩溃。
这么设计的主要原因是我们可以数组切片。在Swift中,计算一个索引值这种操作是非常罕见的:
→ 想要迭代数组 ?for x in array
→ 想要迭代除了第一个元素以外的数组其余部分? for x in array.dropFirst()
→ 想要迭代除了最后5个元素以外的数组? for x in array.dropLast(5)
→ 想要列举数组中的元素和对应的下标?for ( num, element) in collection.enumerated()
→想要寻找一个指定元素的位置 if let idx = array.index { someMatchingLogic($0) }
→想要对数组中的所有元素进行变形?array.map{someTransformation($0)}
→想要筛选出符合某个标准的元素? array. filter { someCriteria($0) }
在Swift 3中传统的C风格的for
循环被移除了,这是Swift不鼓励你去做索引计算的另一个标志。手动计算和使用索引值往往可能带来很多潜在的bug,所以最好避免这么做。如果这不可以避免的话,我们可以很容易写一个可重用的通用函数来进行处理,在其中你可以对精心测试后的索引计算进行封装,我们将在泛型一章里看到这个例子。
但是有些时候你然后不得不使用索引。对于数组索引来说,当你这么做时,你应该已经深思熟虑,对背后的索引计算逻辑进行过认真思考。在这个前提下,如果每次都要对获取的结果进行解包的话就显得多余了——因为这意味着你不信任你的代码。但实际上你是信任你自己的代码的,所以你可能会选择将结果进行强制解包,因为你知道这些下标都是有效的,这一方面十分麻烦,另一方面也是一个坏习惯。当强制借呗编程一种习惯后,很可能你会不小心强制解包了本来不应该解包的东西,所以,为了避免这个行为变成习惯,数组根本没有给你可选值的选项。
无效的下标操作会造成可控的崩溃,有时候这种行为可能会被叫做不安全,但是这只是安全性的一方面。下标操作在内存安全的意义上是完全安全的,标准库中的集合总是会执行边界检查,并禁止那些越界索引对内存的访问。
其他操作的行为略有不同。first和last属性返回的是可选值,当数组为空时,他们返回nil。first相当于isEmpty?nil : self[0]
。类似地,如果数组为空时,removeLast将会导致崩溃,而popLast将在数组不为空是删除最后一个元素返回它,在数组为空时,它将不执行任何操作,直接返回nil。你应该根据自己的需要来选取到底使用那个一个:当你将数组当作栈来使用时,你可能总是想要将empty检查和移除最后元素组合起来使用;而另一方面,如果你已经知道数组一定非空,那再去处理可选值就完全没有必要。
数组变形
map
对数组中的每个执行转换操作是一个很常见的任务。每个程序员可能都写过上百次这样的代码:创建一个新的数组,对已有数组中的元素进行循环依次取出其中的元素,对取出的元素进行操作,并把操作的结果加入到新数组的末尾。比如,下面的代码计算了一个整数数组里的元素的平方:
let fibs = [0,1,1,2,3,5]
var squared: [Int] = []
for fib in fibs {
squared.append(fib * fib)
}
squared//[0,1,1,4,9,25]
Swift数组拥有的map方法,这个方法来自函数式编程的世界。下面的例子使用了map来完成同样的操作:
let squares = fibs.map { fib in fib * fib }
squares//[0,1,1,4,9,25]
这种版本有三大优势。首先,他很短。长度短一般意味着错误很少,不过更重要的是,它比原来更清晰。所有无关的内容都被移除了,一旦你习惯了map满天飞的世界,你就会发现map就像是一个信号,一旦你看到它,就会知道即将有一个函数被作用在数组的每个元素上,并返回另一个数组,它将包含所有被转换后的结果。
其次,squared将由map的结果得到,我们不会再改变它的值,所有也就不再需要用var
来进行声明了,我们可以将其声明为let
。另外,由于数组元素的类型可以从传递给map的函数中推断出来,我们也不在需要为squared显示的指明类型了。
最后,创造map函数并不难,你只需要把for循环中的代码模块部分用一个泛型函数分装起来就可以了。下面是一种可能的实现方式(在Swift中,它实际上是Sequence的一个扩展,我们将在之后关于编写泛型算法的章节里面继续Sequence的话题):
extension Array {
func map<T>(_ transform:(Element)->T) -> [T] {
var result: [T] = []
result.reserveCapacity(count)
for x in self {
result.append(transform(x))
}
return result
}
}
Element是数组中包含的元素类型的占位符,T是元素转换之后的类型的占位符。map函数本身并不关系Element和T究竟是什么,它们可以是任意类型的。T的具体类型将由调用者传入给map的transform方法的返回值类型来决定。
实际上,这个函数的签名应该是
func map<T>(_ transform:(Element)->T) -> [T]
也就是说,对于可能抛出错误的变形函数,map将会把错误转发给调用者。我们会在出错误处理一章里覆盖这个细节。在这里,我们选择去掉错误处理的这个修饰,这样看起来会更简单一些。如果你感兴趣,可以看看GitHub上Swift仓库的Sequence.map的源码实现
使用函数将行为参数化
即使你已经很熟悉map了,也请花一点时间来想一想map的代码。是什么让它可以如此通用而且有用?
map可以将模板代码分离出来,这些模板代码并不会随着每次调用发生变动,发生变动的是那些功能代码,也就是如何变换每个元素的逻辑代码。map函数通过接受调用者所提供的变换函数作为参数来做到这一点。
纵观标准库,我们可以发现很多这样将行为进行参数化的设计模式。标准库中有不下十多个函数接受调用者传入的闭包,并将它作为函数执行关键步骤:
→ map和flatMap —— 如何对元素进行变换
→filter——元素是否应该被包含在结果中
→ reduce——如何将元素合并到一个总和的值中
→ sequence——序列中下一个元素应该是什么?
→ forEach——对于一个元素,应该执行怎么样的操作
→ sort,lexicographicCompare 和 partition —— 俩个元素应该以怎么样的顺序进行排列
→ index,first 和 contains ——元素是否符合某个条件
→ min 和 max——两个元素中的最小/最大值是哪个
→ elementsEqual 和 starts——俩个元素是否相等
→ split——这个元素是否是一个分割符
所有这些函数的目的都是为了摆脱代码中那些杂乱无用的部分,比如像是创建新数组,对源数据进行for循环之类的事情。这些杂乱代码都被一个单独的单词替代了。这可以重点突出那些程序员想要表达的真正重要的逻辑代码。
这些函数中有一些拥有默认行为。除非你进行过指定,否则sort默认将会把可以做比较的元素按照升序排列。contains对于可以判断的元素,会直接检查俩个元素是否相等。这些行为让代码变得更加易读。升序排列非常自然,因此array.sort()的意义也很符合直觉。而对于array.index(of:"foo")
这样的表达方式,也要比array.index { $0 == "foo" }
更容易理解。
不过在上面的例子中,它们都只是特殊情况下的简写,集合中的元素并不一定需要可以作比较,也不一定需要可以判等。你可以不对整个元素进行操作,比如,对一个包含人的数组,你可以通过他们的年龄进行排序(people.sort{ $0.age<$1.age}
),或者是检查集合中有没有包含未成年人(people.sort{ $0.age < 18}
)。你也可以对转变后的元素进行比较,比如通过people.sort { $0.name.uppercased() < $1.name.uppercased() }
来进行忽略大小写的排序,虽然这么做的效率不会很高。
还有一些其他类似的很有用的函数,可以接受一个闭包来指定行为。虽然他们并不存在于标准库中,但是你可以很容易地自己定义和实现它们,我们也建议你自己尝试着做做看:
→ accumulate——累加,和reduce 类似,不过是将所有元素合并到一个数组中,而且保留合并时每一步的值。
→ all (matching:) none(matching:) ——测试序列中是不是所有元素都满足某个标准,以及是不是没有任何元素满足某个标准。它们可以通过contains和它进行了精心对应的否定形式来构建。
→ count(where:) —— 计算满足条件的元素的个数,和filter相似,但是不会构建数组。
→ indices(where:)——返回一个包含满足某个标准的所有元素的索引的列表,和index(where:)类似,但是不会再遇到首个元素时就停止。
index(where:)
→ prefix(while:)——当判断为真的时候,将元素滤出道结果中。一旦不为真,就将剩余的抛弃。和filter类似,但是会提前退出。这个函数在处理无序列或者延迟计算(lazily-computed) 的序列时会非常有用。
→ drop(while:)—— 当判断为真的时候,丢弃元素。一旦不为真,返回将其余的元素。和prefix(while:) 类似,不过返回相反的集合
有时候你可能发现你写好了多次同样模式的代码,比如想要在一个逆序数组中寻找第一个满足特定条件的元素:
let names = ["Paula", "Elena", "Zoe"]
var lastNameEndingInA: String?
for name in names.reversed() where name.hasSuffix("a") {
lastNameEndingInA = name
break
}
lastNameEndingInA // Optional("Elena")
在这种情况下,你可以考虑为Sequence添加一个小扩展,来将这个逻辑封装到last(where:)方法中。我们使用闭包来对for循环发生的变化进行抽象描述:
extension Sequence {
func last(where predicate: (Iterator.Element) -> Bool) -> Iterator.Element? {
for element in reversed() where predicate(element) {
return element
}
return nil
}
}
现在我们就能把代码中的for循环换成findElement了:
let match = names.last{ $0.hasSuffix("a")}
match// Optional("Elena")
这么做的好处和我们在介绍map时所描述的是一样的,相较for循环,last(where:)的版本显然更加易读。虽然for循环也很简单,但是在你的头脑里你始终还是要去做个循环,这加重了理解的负担。使用last(where:)可以减少出错的可能性,而且它允许你使用let而不是var来声明变量。
它和guard
一起也能很好地工作,可能你会想要在元素没被找到的情况下提早结束代码:
guard let match = someSequence.last(where:{$0.passesTest()})
else { return }
可变和带有状态的闭包
当遍历一个数组的时候,你可以使用map来执行一些其他操作(比如将元素插入到一个查找表中)。我们不催件这么做,来看看下面这个例子:
array.map { item in
table.insert(item)
}
这将副作用(改变了查找表)隐藏在了一个看起来只是对数组变形的操作中。在上面这样的例子中,使用简单的for循环显然比使用map这样的函数更好的选择。我们有一个叫做forEach的函数,看起来很符合我们的需求,但是forEach本身存在一些问题,我们一会详细讨论。
这种做法和故意给闭包一个局部状态有本质的不同。闭包是指那些可以捕获自身作用域之外的变量的函数,闭包在结合上高阶函数,将成为强大的工具。举个例子,刚才我提到的accumulate函数可以用map结婚一个带有状态的闭包来进行实现:
extension Array {
func accumulate<Result>(_ initialResult: Result, _ nextPartialResult: (Result, Element) -> Result) -> [Result] {
var running = initialResult
return map { next in
running = nextPartialResult(running, next)
return running
}
}
}
这个函数创建了一个中间变量来存储每一步的值,然后使用map来从这个中间值逐步创建结果数组:
[1,2,3,4]. accumulate(0, +) // [1, 3, 6, 10]
要注意的是,这段代码假设了变形函数是以序列原有的顺序执行的。在我们上面的map中,事实确实如此。但是也有可能对于序列的变形是无序的,比如我们可以有并行处理的元素变形的实现。现在标准库中的map版本没有指定它是否会按顺序来处理序列,不过看起来现在这么做是安全的
filter
另一个常见操作是检查一个数组,然后将这个数组中符合一定条件的元素过滤出来并用它们创建一个新的数组。对数组进行循环并且根据条件过滤其中元素的模式可以用数组的filter方法表示:
let nums = [1,2,3,4,5,6,7,8,9,10]
let result = nums.filter{ num in num%2==0}
result//[2,4, 6, 8, 10]
我们可以使用 $0
用来代表参数的简写,这样代码将会更加简短。我们可以不用写出num参数,而上面的代码重写为:
let nums = [1,2,3,4,5,6,7,8,9,10]
let result = nums.filter{ $0 % 2 == 0 }
result//[2,4, 6, 8, 10]
对于很短的闭包来说,这样做有助于提高可读性。但是如果闭包比较复杂的话,更好的做法应该是就像我们之前那个,显式地把参数名字写出来。不过这更多的是一种个人的选择,使用一眼看上去更易读的版本就好。一个不错的原则是,如果闭包可以很好地卸载一行里的话,那么使用简写名会更合适。
通过组合使用map和filter,我们可以轻易完成很多数组操作,而不需要引入中间数组。这会使得最终的代码变得更短更易读。比如寻找100以内同事满足是偶数并且是其他数字的平方的数,我们可以对0..<10进行map来得到所有平方数,然后再用filter过滤出其中的偶数:
(1..<10).map{ $0 * $0 }.filter{ $0 % 2 == 0 }
// [4, 16, 36, 64]
filter的实现看起来和map很类似:
extension Array {
func filter(_ isIncluded:(Element) -> Bool) -> [Element] {
var result: [Element] = []
for x in self where isIncluded(x){
result.append(x)
}
return result
}
}
一个关于性能的小提示:如果你正在写下面这样的代码,请不要这么做
bigArray.filter { someCondition }.count>0
filter会创建一个全新的数组,并且会对数组中的每个元素都进行操作。然而在上面这段代码中,这显然是不必要的。上面的代码仅仅检查了是否有至少一个元素满足条件,在这个情景下,使用contains(where:)
更为合适:
bigArray.contains { someCondition }
这种做法会比原来快得多,主要因为俩个方面:它不会去为了计数而创建一整个全新的数组,并且一旦匹配了第一个元素,它就将提前退出。一般来说,你只应该在需要所有结果时才会去选择使用filter。
有时候你会发现你想用contains完成一些操作,但是写出来的代码很糟糕。比如,要是你想检测一个序列中的所有元素是否全部满足某个条件,你可以用!sequence.contains { !condition }
,其实你可以用一个更具有描述性名字的新函数将它封装起来:
extension Sequence {
public func all ( matching predicate: (Iterator.Element) -> Bool) -> Bool {
// 对于一个条件,如果没有元素不满足它的话,那意味着所有元素都满足它:
return !contains { !predicate($0) }
}
}
let evenNums=nums.lter{$0%2==0}//[2,4, 6, 8, 10]
evenNums.all{$0%2==0}//true
Reduce
map和filter都作用在一个数组上,并产生另一个新的、经过修改的数组。不过有时候,你可能会想把所有元素合并为一个新的值。比如要是我们想将元素的值全部加起来。可以这样写:
var total = 0
let fibs = [1,2,3,4,5,6,7,8,9,10]
for num in fibs {
total = total + num
}
reduce方法对应这种模式,它把一个初始值(在这里是0)以及一个将中间值(total)与序列中的元素(num)进行合并的函数进行了抽象。使用reduce,我们可以将上面的例子重写为这样:
let sum = fibs.reduce(0){ total, num in total + num }
运算符也是函数,所以我们也可以把上面的例子写成这样子:
let sum = fibs.reduce(0, +) // 12
reduce的输出值的类型可以和输入的类型不同。举个例子,我们可以将一个整数的列表转换为一个字符创,这个字符串中每个数字后面跟一个空格:
fibs.reduce("") { str, num in str + "\(num) " }
ruduce的实现是这样的:
extension Array{
func reduce<Result>(_ initialResult:Result, _ nextPartialResult:(Result, Element) -> Result) -> Result {
var result = initalResult
for x in self {
result = nextPartialResult(result,x)
}
return result
}
}
另一个关于性能的小提示:reduce相当灵活,所以在构建数组或者是执行其他操作时看到reduce的话不足为奇。比如,你可以只使用reduce就能实现map和filter:
extension Array {
func map2<T>(_ transform: (Element) -> T) -> [T] {
return reduce([]) {
$0 + [transform($1)]
}
}
func filter2 (_ isIncluded: (Element) -> Bool) -> [Element] {
return reduce([]) {
isIncluded($1) ? $0 + [$1] : $0
}
}
}
这样的实现符合美学,并且不再需要哪些啰嗦的命令式的for循环。但是Swift不是Haskell,Swift的数组不是列表(list)。在这里,每次执行combine函数都会通过在前面的元素之后附加一个变换元素或者是已包含的元素,并创建一个全新的数组。这意味着上面俩个实现的复杂度是O(n^2),而不是O(n),随着数组长度的增加,执行这些函数所消耗的时间将以平方关系增加。
flatMap
有时候我们会想要对一个数组用一个函数进行map,但是这个变形函数返回的是另一个数组而不是单独的元素。
举个例子,加入我们有一个叫extractLinks的函数,它会读取一个Markdown文件,并返回一个包含该文件中所有连接的URL的数组。这个函数的类型是这样的:
func extractLinks(markdownFile: String) -> [URL]
如果我们有一系列的Markdown文件,并且想将这些文件中所有的链接都提取到一个单独的数组中的话,我们可以尝试使用markdownFiles.map(extractLinks) 来构建。不过问题是这个方法返回的是一个包含了URL的数组的数组,这个数组中的每个元素都是一个文件中的URL的数组。为了得到一个包含所有URL的数组,你还要对这个由map取回的数组中的每个数组用joined来进行展平(flatten),将它归并到一个单一数组中去:
let markdownFiles:[String] = //...
let nestedLinks = markdownFiles.map(extractLinks)
let links = nestedLinks.joined()
flatMap将这俩个操作合并为一个步骤。markdownFiles.flatMap(links)
将直接把所有Markdown文件中的所有URL放到一个单独的数组里并返回。
flatMap的实现看起来也和map基本一致,不过flatMap需要的是一个能够返回数组的函数作为变换参数。另外,在附加结果的时候,它使用的是 append(contentsOf:)而不是append(_:),这样它能把结果展平:
extension Array{
func flatMap<T>(_ transform:(Element) -> [T]) -> [T] {
var result: [T] = []
for x in self {
result.append(contentsOf: transform(x))
}
}
}
flatMap的另一个常见使用情景是将不同数组里面的元素进行合并。为了得到俩个数组中的元素的所有配对组合,我们可以对其中一个数组进行flatMap,然后对另一个进行map操作:
let suits = ["♠", "♥", "♣", "♦"]
let ranks = ["J","Q","K","A"]
let result = suits.flatMap { suit in
ranks.map { rank in
(suit, rank)
}
}
使用forEach进行迭代
我们最后要讨论的操作是forEach。它和for循环的作为非常类似:传入的函数对序列中的每个元素执行一次。和map不同,forEach不返回任何值。技术上来说,我们可以不暇思索地将一个for循环替换为forEach:
for element in [1,2,3] {
print(element)
}
[1,2,3]. forEach { element in
print ( element)
}
这没什么特别之处,不过如果你想要对集合中的每个元素都调用一个函数的话,使用forEach会比较合适。你只需要将函数或者方法直接通过参数的方式传递给forEach就行了,这就可以改善代码的清晰度和准确性。比如在一个viewController里你想把一个数组中的视图都加到当前View上的话,只需要写theViews.forEach(view.addSubview)就足够了。
不过,for循环和forEach有些细微的不同,值得我们注意。比如,当一个for循环中有return语句时,将它重写为forEach会造成代码行为上的极大区别。让我们举个例子,下面的代码是通过结合使用带有条件的where和for循环完成的:
extension Array where Element:Equatable {
func index(of element:Element) -> Int?{
for idx in self.indices where self[idx] == element {
return idx
}
return nil
}
}
我们不能直接将where语句加入到forEach中,所以我们可能会用filter来重写这段代码(实际上这段代码是错误的):
extension Array where Element: Equatable {
func index_foreach(of element: Element) -> Int? {
self.indices.filter { idx in
self[idx] == element
}. forEach { idx in
return idx
}
return nil
}
}
在forEach中的return并不能返回到外部函数的作用域之外,它仅仅只是返回到闭包本身之外,这和原来的逻辑就不一样了。在这种情况下,编译器会发现return语句的参数没有被使用,从而给出警告,我们可以找到问题所在。但我们不应该将找到所有这类错误的希望寄托在便一起上。
在思考一下下面这个简单的例子:
(1..<10).forEach { number in
print(number)
if number > 2 { return }
}
你可能一开始还没反应过来,其实这段代码将会把输入的数字全部打印出来。return语句并不会终止循环,它做的仅仅是从闭包中返回。
在某些情况下,比如上面的addSubview的例子里,forEach可能会更好。它作为一系列链式操作使用时可谓使得其所。想象一下,你在同一个语句中有一系列map和filter的调用,这时候你想在调试时打印出操作链中间某个步骤的数组值,插入一个forEach步骤应该是最快的选择。
不过,因为return在其中的行为不太明确,我们建议大多数情况下不要使用forEach。这种时候,使用常规的for循环可能会更好
数组类型
切片
除了通过单独的下标来访问数组中的元素(比如fibs[0]),我们还可以通过下标来获取某个范围中的元素。比如,想要得到数组中除了首个元素的其他元素,我们可以这么做:
let fibs = [0,1,1,2,3,5]
let slice =fibs[1..<fibs.endIndex]
slice // [1, 1, 2, 3, 5]
type(of: slice) // ArraySlice<Int>
它将返回数组的一个切片(slice),其中包含了原数组中从第二个元素到最后一个元素的数组。得到的结构的类型是ArraySlice,而不是Array。切片类型只是数组的一种表示方式,它背后的数据仍然是原来的数组,只不过是用切片的方式来进行表示。这意味着原来的数组并不需要被复制。ArraySlice具有的方法和Array上定义的方法是一致的,因此你可以把它们当做数组来进行处理。如果你需要将切片转换为数组的话,你可以通过将切片传递给Array的构建方法来完成
Array(fibs[1..<fibs.endIndex])// [1, 1, 2, 3, 5]
数组切片
桥接
Swift数组可以桥接到 Objective-C中。实际上它们也能被用在C代码里,不过后面才会涉及到这个问题。因为NSArray只能持有对象,所以对Swift数组进行桥接转换时曾经有一个限制,那就是数组中的元素能被转换为AnyObject。这限制了只有当数组元素是类实例或者是像是Int,Bool,String这样的一小部分能自动桥接到Objective-C对应类型的值类型时,Swift数组才能被桥接。
不过在Swift3中这个限制已经不复存在了。Objective-C中的id类型现在导入Swift中时变成Any,而不再是AnyObject,也就是说,任意的Swift数组都可以被桥接为NSArray了。NSArray本身仍旧只接受对象,所以,编译器和运行时将自动在后台把不适配的那些值用类来进行包装。反方向的解包同样也是自动进行的。
使用统一的桥接当时来处理所有Swift类型到Objective-C的桥接工作,不仅仅使数组的处理变得容易,像是字典(dictionary)或者集合(set)这样的其他集合类型,也能从中受益。除此之外,它还为未来Swift与Objective-C之间互用性的增强带来了可能。比如,现在Swift的值可以桥接到Objective-C的对象,那么在未来的Swift本班中,一个Swift值类型完全有可能可以去遵守一个被标记为
@objc
的协议