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<T>(_ a: inout T, _ b: inout T) {
    let temporaryA = a
    a = b
    b = temporaryA
}

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

类型参数的命名

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

泛型类型

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

struct Stack<Element> {
    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<T: SomeClass, U: SomeProtocol>(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<Element>: Container {
    // Stack<Element> 的原始实现部分
    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<C1: Container, C2: Container>
    (_ someContainer: C1, _ anotherContainer: C2) -> Bool
    where C1.Item == C2.Item, C1.Item: Equatable {

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

        // 检查每一对元素是否相等
        for i in 0..<someContainer.count {
            if someContainer[i] != anotherContainer[i] {
                return false
            }
        }

        // 所有元素都匹配,返回 true
        return true
}

具有泛型 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: Sequence>(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<String>(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<T: Shape>: Shape {
    var shape: T
    func draw() -> String {
        let lines = shape.draw().split(separator: "\n")
        return lines.reversed().joined(separator: "\n")
    }
}

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

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

struct FlippedShape<T: Shape>: 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<T: Shape>(_ 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

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

推荐阅读更多精彩内容