主要参考书:《Advanced Swift》- objc

内建集合类型

  • 数组
  • 数组是值语义
  • 意味着当为由let 声明的数组调用append 时,编译将会不通过;
  • 且把一个已经存在的数组赋值给另一个变量时,这个数组的内容将会被复制。不过好在Swift 标准库中的所有集合类型都采用“写时复制“「Copy-On-Write」技术,因此在新或旧变量被写入新值之前,二者在内存空间中共享唯一存储。
  • 数组的常见方法
  • 迭代数组 —— 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并不鼓励做索引计算,因为手动计算和使用索引将会带来很多潜在bug
  • 虽然无效的下表操作会造成可控的崩溃,但从内存安全的角度来看,下标操作是绝对安全的,因为标准库中的集合总是会执行边界检查,并禁止越界索引对内存的访问
  • 数组变形
  • Map
// 求斐波那契数列前五项平方
var fibs:[Int] = {0,1,1,2,3,5}

// 遍历式
var squared: [Int] = []
for fib in fibs{
  squared.append(fib * fib)
}
squared //[0,1,1,4,9,25]

// 函数式
let squares = fibs.map{fib in fib * fib}
squares //[0,1,1,4,9,25]

// 二者完全等价
// 但使用map 时,使用者有了更大的空间使得原本不得不被声明为var 的值改为let 声明

// 实现
extension Array{
  func map<T>(_ transform:(Element) -> T) -> [T]{
    var result: [T] = []
    result.reverserCapacity(count)
    for x in self{
      result.append(transform(x))
    }
    return result
  }
}
  • 标准库中集合的其他常见方法
// map 和 flatMap — 对元素进行变换。
// filter — 只包含特定的元素。
// allSatisfy — 针对一个条件测试所有元素。 
// ★reduce — 将元素聚合成一个值。
// forEach — 访问每个元素。
// sort(by:), sorted(by:), lexicographicallyPrecedes(_:by:), 和 partition(by:) — 重排元素。
// firstIndex(where:), lastIndex(where:), first(where:), last(where:), 和contains(where:) — 一个元素是否存在?
// min(by:) 和 max(by:) — 找到所有元素中的最小或最大值。
// elementsEqual(_:by:) 和 starts(with:by:) — 将元素与另一个数组进行比较。
// split(whereSeparator:) — 把所有元素分成多个数组。
// prefix(while:) — 从头取元素直到条件不成立。
// drop(while:) — 当条件为真时,丢弃元素;一旦不为真,返回其余的元素 (和 prefix 类 似,不过返回相反的集合)。
// removeAll(where:) — 删除所有符合条件的元素。
// accumulate — 累加,和 reduce 类似,不过是将所有元素合并到一个数组中,并保留合并时每一步的值。
// count(where:) — 计算满足条件的元素的个数 (它应该是 Swift 5 标准库的一部分,但 由于和 count 属性的名字冲突而延迟了,它可能会在随后的版本中被重新引入)。
// indices(where:) — 返回一个包含满足某个条件的所有元素的索引列表,和 index(where:) 类似,但是不会在遇到首个元素时就停止。
  • filter:选出一个数组中满足条件的元素组成新数组
// 寻找0 - 10 以内所有数平方中的偶数项
(1..<10).map{$0 * $0}.filter{$0 % 2 == 0}

// 实现
extension Array{
  func filter(_ isIncluded:(Element) -> Bool -> [Element]){
    var result: [Element] = []
    for x in self where isIncluded(x){
      result.append(x)
    }
    return result
  }
}
  • reduce:将数组中的所有元素合并为一个新的值
let fibs = [0,1,1,2,3,5]

let sum = fibs.reduce(0){total, num in total + num} // 12

// 运算符本质上也是函数,因此上述表达式与下述表达式功能一致
let sum = fibs.reduct(0, +)

// reduce 的结果与运算过程不必拘泥于数据类型
let sum = fibs.reduce(str, num in str + "\(nums),") // 得到一个字符串 "0,1,1,2,3,5,"

// 实现
extension Array{
  func reduce<Result>(_ initialResult: Result,
                     _ nextPartialResult: (Result, Element) -> Result) -> Result
  {
    var result = initialResult
    for x in self{
      result = nextPartialResult(result, x)
    }
    return result
  }
}
  • flatMap:一次性解决数组的提取和展平
// 读取一个Markdown 文件,返回包含该文件中所有链接的URL 数组
func extractLinks(markdownFile: String) -> [URL]

let markdownFiles: [String]
// 普通的通过map 实现,获得的是[[URL]]
let nextedLinks = markdownFiles.map(extractLinks)
// 将[[URL]] 展开,获得[URL]
let links = nestedLinks.joined

// 等价于直接的一个flatMap
let links_0 = markdownFiles.flatMap(links)

// 实现
extension Array{
  func flatMap<T>(_ transform:(Element) -> [T]) -> [T]{
    var result: [T] = []
    for x in self{
      result.append(contentsOf: transform(x))
    }
    return result
  }
}

// 将不同数组里的元素进行合并
let suits = ["♠", "♥", "♣", "♦"]
let ranks = ["J","Q","K","A"]
let result = suits.flatMap{suit in
	ranks.map{rank in
    (suit, rank)
  }
}/*
[("♠", "J"), ("♠", "Q"), ("♠", "K"), ("♠", "A"), ("♥", "J"), ("♥","Q"), ("♥", "K"), ("♥", "A"), ("♣", "J"), ("♣", "Q"), ("♣", "K"), ("♣", "A"), ("♦", "J"), ("♦", "Q"), ("♦", "K"), ("♦", "A")]
*/
  • 数组迭代 - forEach
for element in [1,2,3]{
  print(element)
}

[1,2,3].forEach{element in
	print(element)
}

// for 循环和forEach 闭包并没有太大区别,但当需要在循环内对函数进行return 时,二者将有大不同
(1..<10).forEach{number in
	print(number)
	if(number > 2) {return}		
}
// 该return 实际上并不会终止循环,它做的仅仅是从闭包中返回
// 因此在forEach 的实现中会开始下一个循环的迭代
  • 当希望对数组中的每个元素调用某一函数时,通过结合forEach 传递函数名将使代码看上去更加简洁
  • 数组切片
let slice = fibs[1..]		//[1,1,2,3,5]
type(of: slice)		// ArraySlice<Int>
  • 切片类型只是数组的一种表示方式,背后的数据仍然是原来的数组,只不过用ArraySlice 的方式来表示。因此数组的元素不会被复制,所以创建一个切片的代价非常小。
  • ArraySlice 和Array 满足了相同的协议(最重要的是Collection协议),所以两者具有的方法一致,因此可以将切片当成数组来处理。
  • 如果需要将切片转为数组,可以通过将其传递给数组的构建方法:let new Array = Array(slice)
  • ★ 谨记:切片和背后的数组通过相同的索引来引用元素,因此切片索引不需要从0 开始。
  • 字典
  • 基本特性
  • 无序
  • 返回的总是Optional 值,当指定的键不存在时,返回nil。在数组中若使用越界下标将导致程序崩溃。
  • 基本方法
enum Setting{
	case text(String)
  case int(Int)
  case bool(Bool)
}

let defaultSettings: [String: Setting] = [
  "Airplane Mode": .bool(false)
  "Name": .text("My iPhone")
]
  • 移除:
  • 将对应键的值设为nil
  • removeValue() 将会返回被移除的值
  • 更新:
  • updateValue
  • 常见的其他方法
  • merge
var settings = defaultSettings
let overriddenSettings: [String: Setting] = ["Name": .text("Jane's iPhone")]
settings.merge(overriddenSettings, uniquingKeysWith:{$1})
settings //["Name": Setting.text("Jane\'s iPhone"), "Airplane Mode": Setting.bool(false)]
//合并settings 和overrideenSettings,策略{$1}表示当某个键同时存在于settings 和overridenSettings 时,采用后者中的值
  • 从一个字典构建另一个字典
// 对能保证键唯一性的使用Dictionary(uniqueKeysWithValues: )
// 对无法保证的使用Dictionary
extension Sequence where Element: Hashable{
  var frequencies: [Element: Int]{
    let frequencyPairs = self.map{($0, 1)}
    // 从Dictionary 构建Dictionary
    return Dictionary(frequencyPairs, uniquingKeysWith: +)
  }
}

let frequencies = "hello".frequencies		//["0":1, "h":1, "e":1, "l":2]
frequencies.filter{$0.value > 1}		//["l": 2]
  • 对字典的值做映射
let settingsAsStrings = settings.mapValues { setting -> String in switch setting {
	case .text(let text): return text
	case .int(let number): return String(number)
	case .bool(let value): return String(value)
	} 
}
settingsAsStrings // ["Name": "Jane\'s iPhone", "Airplane Mode": "false"]
  • Hashable
  • 对于结构体和枚举,只要它们是由可哈希的类型组成的,那么Swift 就可以帮我们自动合成Hashable 协议所需要的实现。
  • 如果不能利用自动Hashable 合成,那么需要首先让类型实现Equatable协议,然后可以实现 hash(into: ) 方法来满足Hashable 协议。
  • 该方法接受一个Hasher 类型参数,这个类型封装了一个通用的哈希函数,并在使用者向其提供数据时捕获哈希函数状态。
  • 应避免使用不具有值语义(如可变的对象)作为字典的键。当将一个对象用作字典键后,改变了它的内容,它的哈希值和/或相等特性往往也会改变。此时将无法在字典中再次找到它。此时字典会在错误的位置存储对象,这将导致字典内部存储的错误。
  • 而对于具有值语义的键,因为键不会和复制的值公用存储,因此它不会被从外部改变,因此不存在这个问题
  • 集合
  • 基本特性
  • 无序
  • 元素需实现 Hashable
  • 测试是否包含复杂度为常数
  • 常用方法
let iPods: Set = ["iPod touch", "iPod nano", "iPod mini", "iPod shuffle", "iPod Classic"]
let discontinuedIPods: Set = ["iPod mini", "iPod Classic", "iPod nano", "iPod shuffle"]

// 补集
let currentIPods = iPods.subtracting(discontinuedIPods) // ["iPod touch"]

// 交集
let touchscreen: Set = ["iPhone", "iPad", "iPod touch", "iPod nano"]
let iPodsWithTouch = iPods.intersection(touchscreen) //["iPod nano", "iPod touch"]

// 并集
var discontinued: Set = ["iBook", "Powerbook", "Power Mac"] 
discontinued.formUnion(discontinuedIPods)
discontinued
/*
["iPod shuffle", "iBook", "iPod Classic", "Powerbook", "iPod mini", "iPod nano", "Power Mac"]
*/
  • IndexSet:高效的索引存储集合
  • 对于连续的索引,仅需存储开头和结尾两个值,而不需要存储所有备选索引。因此,虽然功能上等价于Set<Int>,但实际运行中较之更为高效
  • CharacterSet:搞笑的存储Unicode 编码点的集合。
  • 经常被用来检查一个特定字符串是否只包含某个字符子集
  • 在闭包中使用集合
// 筛选只出现过一次的元素
extension Sequence where Element: Hashable{
  func unique() -> [Element]{
    var seen: Set<Element> = []
    return filter{element in
			if seen.contais(element){
        return false
      }else{
        seen.insert(element)
        return true
      }
		}
  }
}

[1,2,3,12,1,3,4,5,6,4,6].unique() //[1,2,3,12,4,5,6]
  • Range
  • a…b 表示两端闭
  • a…<b 表示左闭右开
  • a… 表示左闭右无穷
  • …b 表示左无穷右闭