Array
[TOC]
如何创建Array
定义空数组
var array1: Array<Int> = Array<Int>()
var array2: [Int] = []
var array3 = array2
定义空数组并指定初始值
//[3,3,3]
var threeInts = [Int](repeating: 3, count: 3)
//[3,3,3,3,3,3]
var sixInts = threeInts + threeInts
//[1,2,3,4,5]
var fiveInts = [1,2,3,4,5]
两个常用的Array属性
.count
获取数组中元素的个数
.isEmpty
数组是否为空
访问Array中的元素
- 使用索引访问数组元素,你必须确保索引的安全性。如果索引超过了数组的范围,程序就会崩溃
- 使用range operator访问数组的一个范围
fiveInts[0...2] //[1,2,3]
fiveInts[0..<2] //[1,2]
用range operator得到的,并不是一个Array
,而是一个ArraySlice
,即Array
某一段内容的View,它不真正保存数组的内容,只保存这个view引用的数组的范围
遍历数组
除了访问单个元素外,另一类常用的需求就是顺序访问数组中的每个成员。在swift里,我们有三种基本的方法遍历一个Array
- for循环:
for value in fiveInts { print(value) }
- 如果我们想在遍历的时候同时获得索引和值,可以只用
enumerated()
方法,他会返回一个Sequence
对象,包含了每个成员的索引和值for (index, value) in fiveInts.enumerated() { print("\(index): \(value)") }
- 借助closure,我们还可以使用
Array
对象的forEach
方法:fiveInts.forEach {print($0)}
添加和删除元素
添加元素:
array1.append(1) //[1]
array1 += [2,3,4] //[1,2,3,4]
要在Array
中间位置添加元素,可以使用insert
方法:
//[1,2,3,4,5]
array1.insert(5, at: array1.endIndex)
删除元素:
array1.remove(at: 4) //[1,2,3,4]
如果你想删除最后一个元素,可以使用removeLast()
方法
array1.removeLast() //[1,2,3]
array2.removeLast() //This will crash!!!
理解Array和NSArray的差异
同样是数组类型,Swift中Array
和Foundation 中的NSArray
有着截然不同的语义和用法。
按值语义实现的Array
在Swift中,Array
是按照值语义实现的,当我们复制一个Array对象时,会拷贝整个Array
的内容
var a = [1,2,3] // [1,2,3]
let copyA = a // [1,2,3]
a.append(4)
// a [1,2,3,4]
// copyA [1,2,3]
// copyA.append(4) Compile error
根据上面的代码,有两点值得说明:
- Swift 数组是否可以被修改完全是通过
var
和let
关键字决定的,Array
类型自身并不解决它是否可以被修改的问题 - 复制
a
并向a
添加内容之后,copyA
的内容并不会修改。但是,Swift在复制Array
时,同样对Array
的性能有所考量,他是用了copy on write的方式。如果你仅仅复制了Array
而不对它修改时,真正的复制是不会发生的,两个数组仍旧引用同一个内存地址。只有当你修改了其中一个Array
的内容时,才会真正让两个Array
对象分开。为了看到这个过程,我们先来实现一个方法,把保存Array
内容的地址变成了一个字符串:
func getBufferAddress<T>(of array: [T]) -> String {
return array.withUnsafeBufferPointer {buffer in
return String(describing: buffer.baseAddress)
}
}
其中,withUnsafeBufferPointer
是Array
的一个方法,它可以把保存Array
内容的地址,传递给它的closure参数。在我们的例子里,这个closure只是把Array
的地址,变成了一个String
对象。
getBufferAddress(of: a)
getBufferAddress(of: copyA)
a.append(4)
getBufferAddress(of: a)
getBufferAddress(of: copyA)
在我们运行之后会发现,只有在给a
添加内容后,它才被重新分配了内存地址。
按引用语义实现的NSArray
在Foundation中,数组这个类型有两点和SwiftArray
是不同的:
- 数组是否可以被修改时通过NSArray和NSMutableArray这两个类型来决定的
-
NSArray
和NSMutableArray
都是类对象,复制他们执行的是引用语义
// Mutable array [1,2,3]
let b = NSMutableArray(array: [1,2,3])
// Const array [1,2,3]
let copyB: NSArray = b
// [0,1,2,3]
b.insert(0, at: 0)
// [0,1,2,3]
copyB
在上面的代码中可以看出,尽管我们在创建copyB时,使用了NSArray
,表明我们不希望它的值被修改,由于这个赋值执行的是应用拷贝,因此,实际上它和b
指向的是同一块内存空间。因此,当我们修改b
的内容时,copyB
也就间接受到了影响。
为了在拷贝NSArray
对象时,执行值语义,我们必须使用它的copy
方法复制所有的元素:
let b = NSMutableArray(array: [1,2,3])
let copyB: NSArray = b
let deepCopyB = b.copy() as! NSArray
b.insert(0, at: 0) //[0,1,2,3]
copyB // [0,1,2,3]
deepCopyB // [1,2,3]
当我们使用NSArray
和NSMutableArray
时,Swift中的var
和let
关键字就和数组是否可以被修改没关系了。它们只控制对应的变量是否可以被赋值成新的NSArray
或NSMutableArray
对象
用Swift的方式使用Array
绝大多数时候,其实你不需要[]
对于下标访问数组元素这种老旧的形式,Swift的开发者应该是不太喜欢的。不喜欢下标操作符的理由是,对于array[index]
这样的访问,甚至都没有使用optional来保护越界的情况
let a = [1,2,3]
type(of: a[1]) //Int.type
a[1]
的类型是Int,而不是Optional<Int>,这说明你必须小心翼翼的使用index来访问Array
中的元素,一旦index的值不正确,你就需要承担运行崩溃的严重后果
我们可以采用其他的手段来替代下标访问数组元素,比如我们想访问数组中的每一个元素时:
a.forEach { print($0) }
//or
for value in a {
}
当我们要获得数组中每一个元素的索引和值时:
for (index,value) in a.enumerated() {}
当我们要获得数组中元素的位置时(例如查找等于1的元素的索引):
a.index( $0 == 1 )
index
会返回一个Option<Int>
,当要查找的元素存在时,就返回该元素的索引,否则,就返回nil
。
当我们要过滤数组中的某些元素时(例如,去掉所有偶数)
a.filter { $0 % 2 == 0 }
话说回来,给[]添加optional保护也不能解决安全问题,因为一旦你force unwrapping 一个optional,就有可能会带来一连串的force unwrapping。这不仅看上去不美观,从代码表现的含义上来说,既然已经准备好要为结果全权负责了。又何必要再让你多执行一不force unwrapping呢。
一些安全周到的方法
和[]
的高风险形成鲜明对比的是,对于那些可以生成优秀代码的方法,Swift则考虑的面面俱到。例如:
访问数组中第一个和最后一个元素的first
和last
属性,当Array
为空时,他们的值都是nil:
a.first //1
a.last //3
type(of: a.first) // Optional<Int>.Type
另外一个值得一提的是在Array
末尾删除元素。Swift为这个动作提供了两个API:
-
removeLast
,你需要自行确保数组中有元素,否则会引发运行时错误 -
popLast
,如果数组为空,会返回nil
为什么要如此呢? 一个通俗的解释就是,为了表意更清晰的代码。
当你基于Array
实现诸如栈这样后入先出的数据结构时,弹出一个元素并判断是否为空是一个常规的操作,所以popLast
返回了一个optional。而对于更一般的"删除数组中最后一个元素"这样的行为,Swift认为这没有任何更具体的使用场景,你应该自己对这样的"低级错误"负责。
通过closure参数化对数组元素的变形操作
当你对Array
做一些处理的时候,像C语言中类似的循环和下标,都不是理想的选择。Swift有一套自己的"现代化"手段。简单来说,就是通过closure来参数化对数组的操作行为。
从循环到map
假设我们有一个简单的Fibonacci序列:[0,1,1,2,3,5]
。如果我们要计算每个元素的平方,怎么办呢?
一个最朴实的做法是for
循环:
var fibonacci = [0,1,1,2,3,5]
var squares = [Int]()
for value in fibonacci {
squares.append(value * value)
}
如果你觉得这还不是个足够引起你注意的问题,那么,当我们要定义一个常量squares
的时候,上面的代码就完全无法胜任了。怎么办呢?先看解决办法:
//[0,1,1,4,9,25]
let constSquares = fibonacci.map { $0 * $0 }
上面的代码,和之前的for
循环执行的结果是相同的。显然,它比for
循环更具表现力,并且也能把我们期望的结果定义成常量。当然,map
并不是什么魔法,无非就是把for
循环执行的逻辑,封装在了函数里,这样我们就可以把函数的返回值赋值给常量了,我们可以通过extension
很简单的自己来实现map
extension Array {
func myMap<T>(_ transform: (Element) -> T) -> [T] {
var tmp: [T] = []
tmp.reserveCapacity(count)
for value in self {
tmp.append(transform(value))
}
return tmp
}
}
虽然和Swift标准库相比,MyMap
的实现中去掉了和异常声明相关的部分。但它已经足以表现map的核心实现过程了。除了在append之前使用了reserveCapacity
给新数组预留了空间之外,它的实现过程和一开始我们使用的for
循环没有任何差别.
如果你还不了解
Element
也没关系,把它理解为Array
中元素类型的替代符就好了再后面我们讲到Sequence类型的时候,会专门提到它。
完成后,当我们在playground里测试的时候:
// [0,1,1,4,9,25]
let constSuquence1 = fibonacci.myMap { $0 * $0 }
就会发现执行结果和之前的constSequence
是一样的了。
参数化数组元素的执行动作
其实,仔细观察myMap
的实现,就会发现它最大的意义,就是保留了遍历Array
的过程,而把执行的动作留给了myApp
的调用者通过参数去定制。而这,就是我们一开始提到的用closure来参数化对数组的操作行为的含义。
有了这样的思路之后,我们就可以把各种常用的带有遍历行为的操作,定制成多种不同的遍历"套路",而把对数组中每一个元素的处理动作留给函数的调用者。但是别急,在开始自动动手造轮子之前,Swift library已经为我们准备了一些,例如:
首先,是找到最小、最大值,对于这类操作来说,只要数组中的元素实现了"Equatable"protocol,我们甚至无需定义对元素的具体操作:
fibonacci.min() // 0
fibonacci.max() // 5
使用min
和max
很安全,因为当数组为空时,这两个方法将返回nil
.
其次,过滤出满足特定条件的元素,我们只要通过参数制定筛选规则就好了:
fibonacci.filter { $0 % 2 == 0 }
第三,比较数组相等或以特定元素开始。对这类操作,我们需要提供两个内容,一个是要比较的数组,另一个则是比较的规则:
//false
fibonacci.elementsEqual([0,1,1], by: { $0 == $1 })
//true
fibonacci.starts(with: [0,1,1], by: { $0 == $1 })
第四,最原始的for
循环的替代品:
fibonacci.forEach { print($0) }
// 0
// 1
// ...
要注意它和map
的一个重要区别:forEach
并不处理closure参数的返回值。因此它只适合用来对数组中的元素进行一些操作,而不能用来产生返回结果。
第五,对数组进行排序,这时,我们需要通过参数制定的是排序规则:
// [0,1,1,2,3,5]
fibonacci.sorted()
// [5,3,2,1,1,0]
fibonacci.sorted(by: >)
let privot = fibonacci.partition(by: { $0 < 1 })
fibonacci[0 ..< privot] // [1,1,2,3,5]
fibonacci[privot ..< fibonacci.endIndex] //[0]
其中 sorted(by:)
的用法是很直接的,它默认采用升序排列。同事,也允许我们通过by
自定义排序规则。在这里>
是 { $0 > $1 }
的简写形式。Swift中很多在不影响语义的情况下的简写形式。
而partition(by:)
则会先对传递给它的数组进行重排,然后根据指定的条件在重排的结果中返回一个分界点位置。这个分界点分开的两部分中,前半部分的元素都不满足指定条件;后半部分都满足指定条件。而后,我们就可以使用range operator来访问者两个区间形成的Array
对象。大家可以根据例子中注释的结果,来理解partition的用法。
第六,是把数组中的所有内容,"合并"成某种形式的值,对这类操作,我们需要指定的,是合并前的初始值,以及"合并"的规则。例如,我们计算fibonacci
中所有元素的和:
fibonacci.reduce(0, +) //12
在这里,初始值为0,和第二个参数+
,则是 { $0 + $1 }
的缩写.
通过这些例子,你应该能感觉到了,这些通过各种形式封装了遍历动作的方法,他们之中的任何一个,都比直接通过for
循环实现具有更强的表现力。这些API,开始让我们的代码从面向机器的,转变成面向业务需求的。因此,在Swift里,你应该试着让自己转变观念,当你面对一个Array
时,你真的几乎可以忘记下标和循环了。
区分修改外部变量和保存内部状态
当我们使用上面提到的这些带有closure参数的Array
方法时,一个不好的做法就是通过closure去修改外部变量,并依赖这种副作用产生的结果。来看一个例子:
var sum = 0
let constSquare2 = fibonacci.map { (fib: Int) -> Int in
sum += fib
return fib * fib
}
在这个例子里,map
的执行产生了一个副作用,就是对fibonacci
中所有的元素求和。这不是一个好的方法,我们应该避免这样。你应该单独使用reduce
来完成这个操作,或者如果一定要在closure参数里修改外部变量,哪怕用forEach
也比map
更好的方案。
但是,在函数实现内部,专门用一个外部变量来保存closure参数的执行状态,则是一个常用的实现技法。例如,我们要创建一个新的数组,其中每个值,都是数组当前位置和之前所有元素的和,可以这样:
extension Array {
func accumulate<T>(_ initial: T, _ nextSum: (T, Element) -> T) -> [T] {
var sum = initial
return map { next in
sum = nextSum(sum, next)
return sum
}
}
}
在上面这个例子里,我们利用map
的closure参数捕获了sum
,这样就保存了每一次执行map
时,之前所有元素的和。
//[0,1,2,4,7,12]
fibonacci.accumulate(0, +)
Filter/Reduce/FlatMap的实现和拓展
这一章着重了解三个比较重要的Array
API,filter
/reduce
/flatMap
,它们和我们在上一节中实现的map一起,形成了各种Array
操作的基础。
filter和filter类似的语义
之前,我们提到过filter
的用法,用于在Array
中,过滤满足特定条件的元素。而这个条件,就是通过filter
的closure参数来确定的:
var fibonacci = [0,1,1,2,3,5]
//[0,2]
fibonacci.filter { %0 % 2 == 0}
按照上一节中实现的map
的思路,我们可以自己来实现一个filter
extension Array {
func myFilter(_ predicate: (Element) -> Bool) -> [Element] {
var temp: [Element] = []
for value in self where predicate(value) {
temp.append(value)
}
return temp
}
}
在上面的实现里,最核心的环节就是通过where
条件的for
循环找到原数组中符合条件的元素,然后把它们一一添加到temp
中,并最终返回给函数的调用者。然后,我们测试下myFilter
fibonacci.myFilter { $0 % 2 == 0 } // [0,2]
结果应该是和标准库中的自带的filter是一样的。理解filter
之后,我们就可以自行定义一些标准库中没有的方法。例如:
剔除掉数组中满足条件的元素:
extension Array {
func reject(_ predicate: (Element) -> Bool) -> [Element]{
return filter { !predicate($0) }
}
}
我们只要把调用转发给filter
,然后把指定的条件取反就好了。这样,提出元素的代码语义上就会更好看一些:
fibonacci.reject { $0 % 2 == 0 } //[1,1,3,5]
另一个基于filter
语义的常用操作是判断数组中是否存在满足条件的元素。下面的代码可以完成任务:
fibonacci.filter { $0 % 2 == 0}.count > 0 //true
但这样做在性能上并不理想,因为即便找到了满足条件的元素,也要遍历完整个数组,这显然是没有必要的。Swift标准库中,提供了一个更方便的方法:
fibonacci.containts { $0 % 2 == 0} //true
contains
的一个好处就是只要遇到满足条件的元素,函数的执行就终止了。基于这个contains
,我们还可以给Array
添加一个新的方法,用来判断Array
中所有的元素是否满足特定的条件
extension Array {
func allMatch(_ predicate: (Element) -> Bool) -> Bool{
return !contains { !predicate:$0 }
}
}
在allMatch
的实现里,只要没有不满足条件的元素,也就是所有的元素都满足条件了。我们可以用下面的代码测试一下:
let events = [2,4,6,8]
events.allMatch { $0 % 2 == 0 } // true
reduce和reduce相关的语义
除了用一个数组生成一个新的数组,有时,我们会希望把一个数组变成某种形式的值。例如,之前我们提到的求和:
fibonacci.reduce(0, +) // 12
了解reduce的进一步用法之前,我们先自己实现一个
extension Array {
func myReduce<T>(_ initial: T, _ next: (T, Element) -> T) -> T {
var temp = initial
for value in self {
temp = next(temp,value)
}
return temp
}
}
从上面可以看出,reduce
的实现也没有什么神奇之处。无非就是把for
循环迭代相加的过程给封装了起来。然后,用下面的代码测试一下,就会发现和标准库里的reduce一样了。
fibonacci.myReduce(0,+) //12
除了求和以外,我们还可以把fibonacci
reduce成一个字符串
let str = fibonacci.myReduce("") { str, num in
return str + "\(num)"
}
// "0 1 1 2 3 5"
甚至,我们还可以用reduce
模拟map
和filter
的实现:
extension Array {
func myMap2<T>(_ transform: (Element) -> T) -> [T] {
return reduce([],{ $0 + transform($1) })
}
func myFilter2(_ predicate: (Element) -> Bool) -> [ELement] {
return reduce([],{ predicate($1) ? $0 + [$1] : $0 })
}
}
然后简单测试一下:
//[0,1,1,4,9,25]
fibonacci.myMap2 { $0 * $0 }
//[0,2]
fibonacci.myFilter2 { $0 % 2 == 0 }
他们的结果和标准库中的map
和 filter是一样的。但是,这种看似优雅的写法却没有想象中的那么好。在他们内部的reduce
调用中,每一次$0
的参数都是一个新建的数组,因此整个算法的复杂度是O(n^2),而不是for
循环版本的O(n)。所以,这样的实现方法最好还是用来作为理解reduce
用法的例子
后面会陆续给出我在Swift学习中的笔记。如果有什么错误,请在下方评论处给出,我会及时进行修改。