Swift5.2 拾遗笔记(三)

本文为私人学习笔记,仅仅做为记录使用,详情内容请查阅 中文官方文档。


泛型

先看一段代码。

func swapTwoInts(_ a: inout Int, _ b: inout Int) {
    let temporaryA = a
    a = b
    b = temporaryA
}

上述代码的作用是交换两个 Int 的值。那么,当我们现在需要交换两个 String 的值时,我们需要重写编写类似的交换方法。那如果还需要交换其他类型的值,又该如何呢?

泛型能让你根据自定义的需求,编写出适用于任意类型的、灵活可复用的函数及类型。你可以避免编写重复的代码,而是使用一种清晰抽象的方式来表达代码的意图。

泛型是 Swift 最强大的特性之一,很多 Swfit 标准库是基于泛型代码构建的。例如,Swift 的 ArrayDictionary 都是泛型集合。

泛型函数

func swapTwoValues(_ a: inout T, _ b: inout T) {
    let temporaryA = a
    a = b
    b = temporaryA
}

泛型函数适用于任意类型,它使用 占位符 类型名(这里叫做 T),而不是实际类型。泛型函数的函数名后面跟着占位类型名 T,并使用尖括号包裹起来,这个将括号告诉 Swift 那个 T 是函数定义内的一个占位类型名,因此 Swift 不会去查找名为 T 的实例类型。
T 类型参数由传入的值的类型推断出来,这点和 Any 类型有着很大的差别。

类型参数的命名

字典 Dictionary 中的 KeyValue 及数组 Array 中的 Element,这能告诉阅读代码的人这些参数类型与泛型类型或函数之间的关系。然而,当它们之间没有有意义的关系时,通常使用单个字符来表示,例如 TUV

泛型类型

除了泛型函数,Swift 的标准库中还有很多泛型类型,例如 DictionaryArray 。Swift 还允许你自定义泛型类型,这些自定义的类、结构体和枚举可以适用于任意类型。

struct Stack {
    var items = [Element]()
    mutating func push(_ item: Element) {
        items.append(item)
    }
    mutating func pop() -> Element {
        return items.removeLast()
    }
}

自定义结构体 Stack 使用了占位类型 Element,这个类型参数包裹在紧随结构体的一对尖括号里。

类型约束

类型约束指定类型参数必须继承自指定类或者遵循特定的协议。例如,Swift 的 Dictionary 类型对字典的键的类型做了限制,字典的 key 必须是可哈希的(hashable),Swift 的基本类型默认都是可哈希的。可哈希的目的是为了便于检查字典中是否已经包含某个特定键的值。如果没有这个约束,那么字典将无法判断是否可以插入或者替换某个指定键的值,也不能查找到已经存储在字典中的指定键的值。

约束语法:

在一个类型参数后面放置一个类名或者协议名,并用冒号进行分割,来定义类型约束。

func someFunction(someT: T, someU: U) {
    // 这里是泛型函数的函数体部分
}

关联类型

定义一个协议时,声明一个或者多个关联类型作为协议的一部分将会非常有用。关联类型为协议中的某个类型提供了一个占位符名称,所代表的实际类型在协议被遵循实现时才会被指定。关联类型通过关键字 associatedtype 来指定。

protocol Container {
    associatedtype Item
    mutating func append(_ item: Item)
    var count: Int { get }
    subscript(i: Int) -> Item { get }
}

Container 协议定义了一个关联类型 Item,该类型并没有指定实际类型,这个类型的确定留给了遵循该协议的类型来提供。

struct Stack: Container {
    // Stack 的原始实现部分
    var items = [Element]()
    mutating func push(_ item: Element) {
        items.append(item)
    }
    mutating func pop() -> Element {
        return items.removeLast()
    }
    // Container 协议的实现部分
    mutating func append(_ item: Element) {
        self.push(item)
    }
    var count: Int {
        return items.count
    }
    subscript(i: Int) -> Element {
        return items[i]
    }
}

Stack 结构体遵循了 Container 协议,其中占位类型参数 Element 被用作 append(_:) 方法的 Item 参数和下标的返回类型。Swift 可以据此推断出 Element 的类型即是 Item 的类型。

除此之外,如果没有在协议中涉及到关联类型,即 Swift 无法自动推断出关联类型的实际类型,你也可以在遵循协议的类型中手动完成关联类型的实际类型。例如:

typealias Item = Int

扩展现有类型来指定关联类型

我们可以通过扩展来让一个已经存在的类型遵循一个协议,然后就可以将该类型充当这个协议来使用,这里的协议包括了使用了关联类型协议。

Swift 的 Array 类型已经提供了 append(_:) 方法,count 属性,以及使用 Int 索引的下标来检索其元素,这些功能都满足了 Container 协议的要求,因此我们可以通过扩展声明其遵循了 Container 协议。你可以通过一个空扩展来实现这点:

extension Array: Container {}

Arrayappend(_:) 方法和下标确保了 Swift 可以推断出 Item 具体的实际类型,并且定义了这个扩展之后,你可以将任意的 Array 当作 Container 来使用。

给关联类型添加约束

关联类型同样可以添加约束。

associatedtype Item: Equatable

泛型 Where 语句

泛型约束 让你能够为泛型函数、下标、类型的类型参数定义一些强制要求。

对关联类型添加约束通常是非常有用的,你可以通过定义一个泛型 where 子句来实现。通过泛型 where 子句让关联类型遵从某个特定的协议,以及某个特定的类型参数和关联类型必须类型相同。

你可以通过将 where 关键字紧跟在类型参数列表后面来定义 where 子句, where 子句后跟一个或者多个针对关联类型的约束,以及一个或者多个类型参数和关联类型间的相等关系。你可以在函数体或者类型的大括号之前添加 where 子句。

func allItemsMatch
    (_ someContainer: C1, _ anotherContainer: C2) -> Bool
    where C1.Item == C2.Item, C1.Item: Equatable {

        // 检查两个容器含有相同数量的元素
        if someContainer.count != anotherContainer.count {
            return false
        }

        // 检查每一对元素是否相等
        for i in 0..

具有泛型 Where 子句的扩展

可以使用泛型 where 子句作为扩展的一部分。

extension Stack where Element: Equatable {
    func isTop(_ item: Element) -> Bool {
        guard let topItem = items.last else {
            return false
        }
        return topItem == item
    }
}

上述使用了泛型 where 子句为 Stack 扩展添加了新的条件,只有当 Stack 中的元素符合 Equatable 协议时,扩展才会添加 isTop(_:) 方法。

如果尝试在其元素不符合 Equatable 协议的 Stack 对象上调用该方法则会收到编译错误。

再比如,你可以扩展 Container 协议。

extension Container where Item: Equatable {
    func startsWith(_ item: Item) -> Bool {
        return count >= 1 && self[0] == item
    }
}

新的扩展方法会确保容器至少有一个元素,然后检查容器中的第一个元素是否与给定的元素相等。

具有泛型 Where 子句的关联类型

可以在关联类型后面加上具有泛型 where 的子句。例如,建立一个包含迭代器(Iterator)的容器。

protocol Container {
    associatedtype Item
    mutating func append(_ item: Item)
    var count: Int { get }
    subscript(i: Int) -> Item { get }

    associatedtype Iterator: IteratorProtocol where Iterator.Element == Item
    func makeIterator() -> Iterator
}

迭代器(Iterator)的泛型 where 子句要求:无论迭代器是什么类型,迭代器中的元素类型,必须和容器项目的类型保持一致。makeIterator() 则提供了容器的迭代器的访问接口。

一个协议继承了另一个协议,你通过在协议声明的时候,包含泛型 where 子句,来添加了一个约束到被继承协议的关联类型。

protocol ComparableContainer: Container where Item: Comparable { }

泛型下标

下标可以是泛型,它们能够包含泛型 where 子句。

extension Container {
    subscript(indices: Indices) -> [Item]
        where Indices.Iterator.Element == Int {
            var result = [Item]()
            for index in indices {
                result.append(self[index])
            }
            return result
    }
}

这个 Container 协议的扩展添加了一个下标方法,接收一个索引的集合,返回每一个索引所在的值的数组。这个泛型下标的约束如下:

  • 在尖括号中的泛型参数Indices,必须是符合标准库中的Sequence 协议的类型。
  • 下标使用的单一的参数,indices,必须是Indices 的实例。
  • 泛型where 子句要求 Sequence(Indices) 的迭代器,其所有的元素都是 Int 类型。这样就能确保在序列(Sequence)中的索引和容器(Container)里面的索引类型是一致的。

综合一下,这些约束意味着,传入到 indices 下标,是一个整型的序列。

不透明类型

返回不透明类型

不透明类型和泛型相反。泛型允许调用一个方法时,为这个方法的形参和返回值指定一个与实现无关的类型。不透明类型允许函数实现时,选择一个与调用代码无关的返回类型。

// 协议
protocol Shape {
    func draw() -> String
}
// 输出图形:正方形
struct Square: Shape {
    var size: Int
    func draw() -> String {
        let line = String(repeating: "*", count: size)
        let result = Array(repeating: line, count: size)
        return result.joined(separator: "\n")
    }
}
// 返回不透明类型
func makeGraphics() -> some Shape {
    let square = Square(size: 2)
    return square
}

makeSquare() 函数将返回值类型定义为 some Shape ,因此,该函数返回遵循 Shape 协议的给定类型,而不需要指定任何具体类型。换句话说,该函数可以表明它公共接口的基本性质 - 返回值是一个几何图形,而不是由公共接口协议生成的特殊类型。如 Square

不透明类型和协议类型的区别

虽然使用不透明类型作为函数返回值,看起来和返回协议类型非常的相似,但是这两者有一个重要的却别:是否需要保证类型一致性。

// 反转形状
struct FlippedShape: Shape {
    var shape: T
    func draw() -> String {
        let lines = shape.draw().split(separator: "\n")
        return lines.reversed().joined(separator: "\n")
    }
}

func invalidFlip(_ shape: T) -> some Shape {
    if shape is Square {
        return shape // 错误:返回类型不一致
    }
    return FlippedShape(shape: shape) // 错误:返回类型不一致
}

由于 invalidFlip(_:) 方法返回值可能存在两种,所以该方法是不正确的,为了修正该方法,我们可以将针对 Square 的特殊处理移入到 FlippedShape 中,以确保函数的返回值唯一。

struct FlippedShape: Shape {
    var shape: T
    func draw() -> String {
        if shape is Square {
            return shape.draw()
        }
        let lines = shape.draw().split(separator: "\n")
        return lines.reversed().joined(separator: "\n")
    }
}
func invalidFlip(_ shape: T) -> some Shape {
    return FlippedShape(shape: shape) 
}

一个不透明类型只能对应一个具体的类型,即便函数调用者并不能知道是哪一种类型;协议类型则可以同时对应多个类型,只要它们都遵循同一协议。总的来说,协议类型更具灵活性,底层类型可以存储更多的值,而不透明类型对这些底层类型有着更强的限制。

不透明类型的作用

具有不透明返回类型的函数或方法会隐藏返回值的类型信息。函数不再提供具体的类型作为返回类型,而是根据它支持的协议来描述返回值。

在处理模块和调用代码之间的关系时,隐藏类型信息非常重要,因为返回的底层数据类型仍然可以保持私有。而且不同于返回协议类型,不透明类型能保证类型一致性,即:编译器能获取到类型信息,同时模块使用者却不能获取到。

闭包的循环强引用

在定义闭包的同时定义捕获列表作为闭包的一部分,通过这种方式可以解决闭包和类实例之间的循环引用。

捕获列表定义类闭包体内捕获的一个或者多个引用类型的规则。根据具体的应用场景来使用弱引用还是无主引用。

捕获列表

捕获列表中的每一项都是由一对元素组成,一个元素是 weakunowner 关键字,另一个元素是类实例的引用。

如果闭包有参数列表和返回类型,那么把捕获列表放在它们的前面。

lazy var someClosure = {
    [unowned self, weak delegate = self.delegate]
    (index: Int, stringToProcess: String) -> String in
    // 这里是闭包的函数体
}

如果闭包没有参数列表或者返回类型,它们会通过上下文推断,那么可以把捕获列表和关键字 in 放在闭包最开始的地方。

lazy var someClosure = {
    [unowned self, weak delegate = self.delegate] in
    // 这里是闭包的函数体
}

弱引用 vs 无主引用

在闭包和捕获列表的实例总是相互引用并且总是同时销毁时,将闭包内的捕获定义为 无主引用。相反的,如果被捕获的引用具有更短的生命周期,可能随时变为 nil ,那么将闭包内的捕获定义为 弱引用

弱引用总是可选类型,并且当引用的实例被销毁后,弱引用的值会自动置为 nil

你可能感兴趣的:(Swift5.2 拾遗笔记(三))