Swift学习笔记--Array篇

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 数组是否可以被修改完全是通过varlet关键字决定的,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)
    }
}

其中,withUnsafeBufferPointerArray的一个方法,它可以把保存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这两个类型来决定的
  • NSArrayNSMutableArray都是类对象,复制他们执行的是引用语义
// 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]

当我们使用NSArrayNSMutableArray时,Swift中的varlet关键字就和数组是否可以被修改没关系了。它们只控制对应的变量是否可以被赋值成新的NSArrayNSMutableArray对象

用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则考虑的面面俱到。例如:
访问数组中第一个和最后一个元素的firstlast属性,当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

使用minmax很安全,因为当数组为空时,这两个方法将返回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的实现和拓展

这一章着重了解三个比较重要的ArrayAPI,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

除了求和以外,我们还可以把fibonaccireduce成一个字符串

let str = fibonacci.myReduce("") { str, num in
    return str + "\(num)"
}
// "0 1 1 2 3 5"

甚至,我们还可以用reduce模拟mapfilter的实现:

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学习中的笔记。如果有什么错误,请在下方评论处给出,我会及时进行修改。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容