木又的《Swift进阶》读书笔记——内嵌集合类型

1,624 阅读6分钟

内嵌集合类型

数组

数组和可变性

  • 数组是一个容器,它以有序的方式存储相同类型的元素,并且允许随机访问每个元素。

  • 和标准库中所有集合类型一样,数组是具有值语义的。

数组索引

  • 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.filter { someCriteria($0) }
  • Swift 3 中传统的 C 风格 for 循环被移除了,这是Swift不鼓励你去做索引计算的另一个标志。手动计算和使用索引值往往会带来很多潜在的bug,所以最好避免这么做。

数组变形

  • Map

    map就像是一个信号,一旦你看到它,就会知道即将有一个函数被作用在数组的每个元素上,并返回另一个数组,它将包含所有被转换后的结果。

  • 使用函数将行为参数化

    map 通过把调用者所提供的变换函数作为参数来将模板代码分离出来

    纵观标准库,这种把行为参数化的设计模式有很多。例如:

    • mapflatMap — 对元素进行变换。
    • 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:) — 删除所有符合条件的元素。
  • 可变和带有状态的闭包

    • 闭包是指那些可以捕获和修改自身作用域之外的变量的函数,当它和高阶函数结合时也就成为了一种强大的工具。
  • filter

    • filter 会创建一个全新的数组,并且会对数组中的每个元素都进行操作。

    • 一般来说,你只应该在需要所有结果时才去选择使用 filter

  • reduce

let fibs = [0,1,1,2,3,5]
fibs.reduce(0,+)
  • 一个展平的map

    flatMap

  • 使用 forEach 进行迭代

    在 forEach 中的return 并不能让外部函数返回,它仅仅只是让闭包本身返回。

数组切片

let slice = fibs[1...]
slice// [1,1,2,3,5]
type(of: slice) // ArraySlice<Int>
  • 切片类型只是数组的一种表示方式,它背后的数据仍然是原来的数组,只不过是用切片的方式来表示。因为数组的元素不会被复制,所以创建一个切片的代价是很小的。
  • 因为 ArraySlice 和 Array 都满足了相同的协议 (当中最重要的就是 Collection 协议),所以两者具有的方法是一致的,因此你可以把切片当做数组来进行处理。
  • 切片和它背后的数组是使用相同的索引来引用元素的。因此,切片索引不需要从零开始。如果操作切片,请基于 startIndex 和 endIndex 属性做索引计算。

字典

  • 字典,通过键来获取值所花费的平均时间是常数量级的。
  • 字典是无序的。
  • 字典查找总是返回一个可选值,当指定的键不存在时,它就返回nil。这点和数组有所不同,在数组中,使用越界下标进行访问将会导致程序奔溃。

可变性

一些有用的字典方法

  • Dictionary 有一个 merge(_: uniquingKeysWith:),它接受两个参数,第一个是要进行合并的键值对,第二个是定义如何合并相同键的两个值的函数。

  • 如果能保证键是唯一的,那么就可以使用 Dictionary(uniqueKeysWithValues:)

extension Sequence where Elemnet: Hashable {
  var frequencies: [Element: Int] {
    let frequencyPairs = self.map{ ($0, 1)}
    return Dictionary(frequencyPairs, uniquingKeysWith: +)
  }
}
let frequencies = "hello".frequencies // ["h":1, "e":1, "0":1, "l":2]
frequencies.filter { $0.value > 1 } // ["l": 2]
  • 对字典的值做映射——map/mapValues

Hashable 要求

  • 标准库中所有的基本数据类型都是遵守 Hashable 协议的,它们包括字符串,整数,浮点数以及布尔值。另外,像是数组,集合和可选值这些类型,如果它们的元素都是可哈希的,那么它们自动成为可哈希的。

Set

  • 和 Dictionary 一样,Set 也是通过哈希表实现的,并拥有类似的性能特性和要求。和字典中的键一样,集合中的元素也必须满足 Hashable。

  • Set 遵守 ExpressibleByArrayLiteral 协议,也就是说,我们可以用数组字面量的方式初始化一个集合:

    let naturals: Set = [1,2,3,2]
    naturals // [1,2,3]
    

集合代数

  • Set支持基本集合操作,求 补集交集并集 ... 想要了解更多的集合操作,可以看看 SetAlgebra 协议

索引集合和字符集合

  • Set 和 OptionSet 是标准库中唯一实现了 SetAlgebra 的类型,但是这个协议在 Foundation 中还被另外两个很有意思的类型实现了:那就是 IndexSet 和 CharacterSet。

  • IndexSet 表示了一个由正整数组成的集合。我们可以用 Set 来做这件事,但是 IndexSet 更加高效,因为它内部使用了一组范围列表进行实现。

  • 同样的,CharacterSet 是一个高效的存储 Unicode 编码点 (code point) 的集合。不过,和 IndexSet 有所不同, CharacterSet 并不是一个集合类型。它的名字,CharacterSet,是从 Objective-C 导入时生成的,在Swift 中它也不兼容 Swift 的 Character 类型。可能 UnicodeScalarSet 会是更好的名字。

在闭包中使用集合

extension Sequence where Element: Hashable {
  func unique() -> [Element] {
    var seen: Set<Element> = []
    return filter { element in
    	if seen.contains(elemnet) {
        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]

上面的这个方法让我们可以找到序列中的所有不重复的元素,并且通过元素必须满足 Hashable 这个约束来维持它们原来的顺序。

Range

  • 范围代表的是两个值的区间,它由上下边界进行定义。

  • 最常用的两种两种类型是 Range (由 ..< 创建的半开范围) 和 ClosedRange (由 ... 创建的闭合范围)。两者都有一个 Bound 的泛型参数: 对于 Bound 的唯一的要求是它必须遵守 Comparable 协议。

  • 半开范围和闭合范围各有作用:

    • 只有半开范围能表达空间隔(也就是下界和上界相等的情况,比如 5..<5)。

    • 只有闭合范围能包括其元素类型所能表达的最大值 (比如 0...Int.max)。而半开范围则要求上界是一个比自身所包含的最大值还要大1的值。

可数范围

  • Range 满足集合类型协议是有条件的,条件是它的元素需要满足 Strideable 协议 (你可以通过增加偏移来从一个元素移动到另一个),并且步长 (stride step) 是整数:

    extension Range: Sequence 
    	where Bound: Strideable, Bound.Stride: SignedInteger {/* ... */}
    
    extension Range: Collection, BidirectionalCollection, RandomAccessCollection 
    	where Bound: Strideable, Bound.Stride: SignedInteger {/* ... */}
    

    换句话说,为了能遍历范围,它必须是可数的。

范围表达式

  • 所有五种范围都满足 RangeExpression 协议。首先,它允许我们询问某个元素是否被包括在该范围中。其次,给定一个集合类型,它能够计算出表达式所指定的完整的 Range:

    public protocal RangeExpression {
      associatedtype Bound:Comparable
      func contains(_ element: Bound) -> Bool
      func relative<C>(to collection: C) -> Range<Bound>
      	where C: Collection, Self.Bound == C.Index
    }