Table of Contents

  1. Algorithm
  2. Review
    1. 范型解决的问题
    2. 范型函数
    3. 类型参数
    4. 命名类型参数
    5. 范型类型
    6. 扩展一个范型类型
    7. 类型限制
    8. 类型限制语法
    9. 行为中的类型限制
    10. 关联类型
    11. 行为中的关联类型
    12. 扩展现存的类型指定一个关联类型
    13. 对关联协议添加限制
    14. 协议中关联类型的限制
    15. 范型的 Where 语法
    16. 用范型 Where 做扩展
    17. Where 上下文
    18. 带一个 where 语法的关联类型
    19. 泛化下标
  3. Tips
    1. 不透明类型解决的问题
    2. 返回一个不透明类型
    3. 封装协议类型
    4. 不透明类型和封装协议类型的区别
  4. Share

Algorithm

Leetcode 1072: Flip Columns For Maximum Number of Equal Rows

https://dreamume.medium.com/leetcode-1072-flip-columns-for-maximum-number-of-equal-rows-6bdd6154b0bc

Review

Generics

范型代码使你写可以跟任何根据需求定义的类型、对象搭配的灵活的、可重用的函数和类型。你可写避免重复和已一种清晰抽象的方式表达的代码

范型是 Swift 一个最强大的特性,且多数 Swift 标准库已范型化。事实上,你已经通过语言指导使用范型,甚至你没有意识到。例如,Swift 的 Array 和 Dictionary 类型都是范型聚合类型。你可创建一个持有 Int 类型的数组,或 String 类型的数组,或任何其他可以用 Swift 创建的类型的数组。相似地,你可创建一个字典存储任何特定类型的值,且对于类型没有限制

范型解决的问题

这里有一个标准、非范型函数称为 SwapTwoInts(_: _:), 其交换两个 Int 值:

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

这个函数使用 inout 参数来交换 a 和 b 的值

swapTwoInts(_: _:) 函数交换原始值 b 为 a,原始值 a 为 b。你可以调用这个函数交换两个 Int 变量的值

var someInt = 3
var anotherInt = 107
swaptwoints(&someInt, &anotherInt)
print("someInt is now \(someInt), and anotherInt is now \(anotherInt)")

swaptwoints 函数很有用,但只能用于 Int 值。如果你想要交换两个 String 值,或两个 Double 值,你不得不写更多的函数,比如 swapTwoStrings(_: _:) 和 swapTwoDoubles( _: _:):

func swapTwoStrings(_ a: inout String, _ b: inout String) {
    let temporaryA = a
    a = b
    b = temporaryA
}

func swapTwoDoubles(_ a: inout Double, _ b: inout Double) {
    let temporaryA = a
    a = b
    b = temporaryA
}

你注意到 swapTwoInts(_: _:),swapTwoStrings( _: _:) 和 swapTwoDoubles( _: _:) 内容是一样的。唯一的区别是值的类型不同

范型函数

范型函数可搭配任意类型。下面是一个范型版本的函数

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

范型函数使用一个占位类型替代实际的类型。每次函数 swapTwoValues(_: _:) 在被调用时用实际的类型替代

下面是使用的示例:

var someInt = 3
var anotherInt = 107
swapTwoValues(&someInt, &anotherInt)

var someString = "hello"
var anotherString = "world"
swapTwoValues(&someString, &anotherString)

类型参数

在上述 swapTwoValues(_: _:) 例子中,占位类型 T 是一个类型参数的例子。类型参数指定并命名一个占位类型,且写在紧跟着函数名后面(例如

一旦你指定一个类型参数,你可使用它来定义函数参数的类型,或作为函数的返回类型,或作为一个类型用在函数体中。这些情况下,当函数被调用时类型参数被实际的参数类型替代

你可提供多个类型参数,在尖括号用逗号分割即可

命名类型参数

多数情况下,类型参数有一个描述性的名字,比如 Dictionary<Key, Value> 中的 Key 和 Value,Array 中的 Element,其告诉读者类型参数和范型参数或使用的函数的关系。然而,当它们之间没有一个有意义的关系时,一般使用单字母比如 T、U 和 V 来命名

范型类型

对于范型函数,Swift 允许你定义你自己的范型类型。自定义类、结构和枚举可跟任意类型搭配

本章节显示如何写一个范型聚集类型 Stack。一个 Stack 是一个有序值集合,跟数组相似,但比数组有更多的限制。Stack 只允许新条目添加到集合末尾,并只能从末尾删除

这里是一个含 Int 值的非范型 Stack

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

这个结构使用一个数组属性存储值。下面是一个范型版本的 Stack

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

例如,为创建一个字符串的 Stack,如下:

var stackOfStrings = Stack<String>()
stackOfStrings.push("uno")
stackOfStrings.push("dos")
stackOfStrings.push("tres")
stackOfStrings.push("cuatro")

let fromTheTop = stackOfStrings.pop()

扩展一个范型类型

当你扩展一个范型类型,你不提供一个类型参数列表作为扩展定义的一部分。而是,从原始类型定义的类型参数列表在扩展体中有效,且原始类型参数名称用于从原始定义中引用类型参数

如下例子扩展范型 Stack 类型,添加一个只读计算属性称为 top 条目,返回栈顶元素

extension Stack {
    var topItem: Element? {
        return item.isEmpty ? nil : items[items.count - 1]
    }
}

类型限制

swapTwoValues(_: _:) 函数和 Stack 类型可搭配任意类型。然而,有时候强制某种类型限制对范型函数和范型类型是很有用的。类型限制指明一个类型参数必须继承一个特定的类,或服从一个特殊的协议或协议组合

例如,Swift 的 Dictionary 类型在键类型上有一个限制。一个字典的键类型必须是可哈希的。即,它必须提供一个方法来使它自己唯一化。字典需要它的键能哈希这样它可检查是否一个特别的键已包含一个值。没有这个说明,字典不能确定它应该对一个特殊的键进行插入或是替换值,也不能从一个给定的键在字典中找到对应的值

类型限制语法

把一个类或协议限制放在类型参数名称的后面来定义类型限制,用逗号分割,作为类型参数列表的一部分。范型函数上类型限制的基本语法显示如下:

func someFunction<T: SomeClass, U: SomeProtocol>(someT: T, someU: U) {
}

上述函数有两个参数,第一个类型参数限制需要 T 为 SomeClass 的一个子类,第二个类型参数需要 U 服从 SomeProtocol 协议

行为中的类型限制

这里是一个非范型函数 findIndex(ofString:in:),其给定一个字符串值来在一个字符串值数组中进行查找:

func findIndex(ofString valueToFind: String, in array: [String]) -> Int? {
    for (index, value) in array.enumerated() {
        if value == valueToFind {
            return index
        }
    }

    return nil
}

let strings = ["cat", "dog", "llama", "parakeet", "terrapin"]
if let foundIndex = findIndex(ofString: "llama", in: strings) {
    print("The index of llama is \(foundIndex)")
}
// Prints "The index of llama is 2"

以下是一个 findIndex 的范型版本

func findIndex(of valueToFind: T, in array: [T]) -> Int? {
    for (index, value) in array.enumerated() {
        if value == valueToFind {
            return index
        }
    }

    return nil
}

这个函数不能正确编译。问题在于等式检查中。不是每个类型可用 == 来进行相等判断。如果你创建你自己的类或结构来表达一个复杂的数据模型,例如,该类或结构的相等不是 Swift 猜想的那样。因此,不能保证这个代码对所有可能的类型 T 都能正常工作,当你尝试编译它时会报道一个适合的错误

Swift 标准库定义了一个协议 Equatable,其需要任意服从的类型实现 = 号和 ! 号来比较类型的任意两个值。所有的 Swift 标准类型自动支持 Equatable 协议

任何实现 Equatable 协议的类型可安全地使用 findIndex(of:in:) 函数,因为它保证支持等号操作。为表达这个事实,你写一个 Equatable 的类型限制作为类型参数定义的一部分

func findIndex<T: Equatable>(of valueToFind: T, in array: [T]) -> Int? {
    for (index, value) in array.enumerated() {
        if value == valueToFind {
            return index
        }
    }

    return nil
}

T: Equatable 表示服从 Equatable 协议的任意类型 T

关联类型

当定义一个协议,声明一个或多个关联类型作为协议定义的一部分是很有用的。一个关联类型给类型一个占位名。关联类型使用时的实际类型不会指明直到协议被适配时。关联类型用 associatedtype 关键字指明

行为中的关联类型

以下是一个 Container 协议,其声明了一个关联类型 Item

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

Container 协议定义三个必须提供的需要的能力:

  • 它必须能够用 append(_:) 方法添加一个新的条目到容器里
  • 它必须可以通过一个 count 属性访问容器中的条目数并返回一个 Int 值
  • 它必须能够用一个 Int 值作为下标访问容器里的条目

以下是一个服从 Container 协议的泛化 Stack 类型

struct Stack<Element>: Container {
    // original Stack<Element> implementation
    var items: [Element] = []
    mutating func push(_ item: Element) {
        items.append(item)
    }
    mutating func pop() -> Element {
        return items.removeLast()
    }
    // confirmance to the Container protocol
    mutating func append(_ item: Element) {
        self.push(item)
    }
    var count: Int {
        return items.count
    }
    subscript(i: Int) -> Element {
        return items[i]
    }
}

扩展现存的类型指定一个关联类型

你可扩展一个现存的类型服从一个协议

Swift 的 Array 类型已经提供了一个 append(_:) 方法,一个 count 属性和一个下标操作。其能力匹配 Container 协议。这意味着你可扩展 Array 服从 Container 协议通过简单声明 Array 适配该协议

extension Array: Container {}

对关联协议添加限制

你可添加类型限制到一个协议里的关联协议来请求确认类型满足这些限制。例如,以下代码定义了一个 Container 版本请求容器里的条目是可相等的

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

协议中关联类型的限制

协议可作为它自己需求的一部分出现。例如,下面是一个 Container 协议,有一个 suffix(_:) 方法。该方法返回给定数目的容器末尾的条目,存储在 Suffix 类型实例中

protocol SuffixableContainer: Container {
    assosicatedtype Suffix: SuffixableContainer where Suffix.Item == Item
    func suffix(_ size: Int) -> Suffix
}

Suffix 有两个限制:它必须服从 SuffixableContainer 协议,它的 Item 类型必须跟 Container 的 Item 类型相同

这里是 Stack 类型的扩展,使其支持 SuffixableContainer 协议

extension Stack: SuffixableContainer {
    func suffix(_ size: Int) -> Stack {
        var result = Stack()
        for index in (count - size)..<count {
            result.append(self[index])
        }

        return result
    }
}

var stackOfInts = Stack<Int>()
stackOfInts.append(10)
stackOfInts.append(20)
stackOfInts.append(30)
let suffix = stackOfInts.suffix(2)
// suffix contains 20 and 30

范型的 Where 语法

类型限制,使得你定义需求在类型参数上关联一个范型函数、下标或类型

对关联类型的需求定义很有用。一个范型 where 语法使你请求一个关联类型必须服从某个协议,或某个类型参数和关联类型必须相同。以下是一个范型函数 allItemsMatch 的例子

func allItemsMatch<C1: Container, C2: Container>(_ someContainer: C1, _ anotherContainer C2) -> Bool
where C1.Item == C2.Item, C1.Item: Equatable {
    // Check that both containers contain the same number of items
    if someContainer.count != anotherContainer.count {
        return false;
    }

    // Check each pair of items to see if they're equavalent
    for i in 0..<someContainer.count {
        if someContainer[i] != anotherContainer[i] {
            return false
        }
    }

    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 上下文

你可以写一个范型 where 语法作为没有它自已的范型类型的声明的一部分,你实际上有上下文的范型类型。例如,你可写一个范型其一个方法的范型类型下标扩展到一个范型类型。Container 结构是泛化的,其中的 where 语法指定类型限制必须满足来使得容器上这些新方法有效

extension Container {
    func average() -> Double where Item == Int {
        var sum = 0.0
        for index in 0..<count {
            sum += Double(self[index])
        }
        return sum / Double(count)
    }

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

let numbers = [1260, 1200, 98, 37]
print(numbers.average())
print(numbers.endsWith(37))

这个例子当 Container 条目为整数时添加一个 average() 方法,当条目适配 Equatable 协议时添加一个 endsWith(_:) 方法。两个方法都包含一个 where 范型限制到原有 Container 声明的 Item 类型参数上

如果你想要写这个代码不带 where 上下文语法,你写两个扩展,代码如下:

extension Container where Item == Int {
    func average() -> Double {
        var sum = 0.0
        for index in 0..<count {
            sum += Double(self[index])
        }
        return sum / Double(count)
    }
}

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

带一个 where 语法的关联类型

你可在关联类型上包含一个 where 限制。例如,假设你想要 Container 包含一个迭代器,代码如下:

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 语法需要迭代器必须遍历条目,其类型为容器的条目类型

对一个协议继承另一个协议,你添加一个限制到继承的关联类型,代码如下

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
    }
}

Tips

Opaque and Boxed Types

Swift 提供两种方法来隐藏一个值类型的细节:不透明类型和封装协议类型。隐藏类型信息在一个模块和调用该模块的代码的边界上是很有用的,因为返回值的类型可以仍然是私有的

一个函数或方法返回一个不透明类型隐藏它的返回值的类型信息。不提供一个固定类型作为函数返回类型,返回值被描述成它支持的协议。不透明类型保留类型 - 编译器可以访问类型信息,但客户端模块不行

一个封装协议类型可保存服从给定协议的任意类型实例。封装协议类型不保存类型 - 值的特殊类型直到运行时之前是不知道的,且它可以随时间改变作为一个不同的值存储

不透明类型解决的问题

例如,假设你写一个模块绘制 ASCII 艺术形状。一个 ASCII 艺术形状的基本特征是一个 draw() 函数返回字符串代表该形状,你可使用 Shape 协议

protocol Shape {
    func draw() -> String
}

struct Triangle: Shape {
    var size: Int
    func draw() -> String {
        var result: [String] = []
        for length in 1...size {
            result.append(String(repeating: "*", count: length))
        }
        return result.joined(separator: "\n")
    }
}
let smallTriangle = Triangle(size: 3)
print(smallTriangle.draw())

你可以使用泛化来实现操作如垂直翻转一个形状,代码如下。然而,这个处理有一个重大的限制:翻转结果导出我们用来创建的确切的范型类型

struct FlippedShape<T: Shape>: Shape {
    var shape: T
    func draw() -> String {
        let lines = shape.draw().split(separator: "\n")
        return lines.reversed().joined(separator: "\n")
    }
}
let flippedTriangle = FlippedShape(shape: smallTriangle)
print(flippedTriangle.draw())

这个处理定义一个 JoinedShape<T: Shape, U: Shape> 结构垂直连接两个形状,代码如下

struct JoinedShape<T: Shape, U: Shape>: Shape {
    var top: T
    var bottom: U
    func draw() -> String {
        return top.draw() + "\n" + bottom.draw()
    }
}
let joinedTriangle = JoinedShape(top: smallTriangle, bottom: flippedTriangle)
print(joinedTriangle.draw())

导出形状创建的详细信息允许不是 ASCII 艺术模块公共接口的类型泄漏因为需要表达完整的返回类型。模块内部的代码可已各种方式构建相同的形状,且使用形状的模块外部的其他代码不应该说明转换列表的实现细节。封装类型如 JoinedShape 和 FlippedShape 对模块用户来说并不关心,它们不应该可见。模块公开接口包含操作如合并和翻转形状,和这些操作返回另一个形状值

返回一个不透明类型

你可认为一个不透明为反转的范型类型。范型类型让代码调用一个函数把类型作为函数的参数且返回对函数实现来说是抽象的值。例如,如下代码函数返回一个类型依赖于调用者

func max<T>(_ x: T, _ y: T) -> T where T: Comparable { ... }

代码选择 x 和 y 值调用 max(_: _:) 函数,且这些值的类型决定 T 的确切类型。调用代码可使用服从 Comparable 协议的任意类型。函数里的代码以范型方式写这样它可以处理调用者提供的任意类型。max( _: _:) 函数实现只使用所有 Comparable 类型共享的功能

这些角色对函数保留,返回的是一个不透明类型。一个不透明类型让函数实现把类型作为值以一种抽象的方式返回。例如下面例子返回一个不规则四边形而不暴露该形状的类型

struct Square: Shape {
    var size: Int
    func draw() -> String {
        let line = String(repeating: "*", count: size)
        let result = Array<String>(repeating: line, count:size)
        return resuilt.joined(separator: "\n")
    }
}

func makeTrapezoid() -> some Shape {
    let top = Triangle(size: 2)
    let middle = Square(size: 2)
    let bottom = FlippedShape(shape: top)
    let trapezoid = JoinedShape(top: top, bottom: JoinedShape:(top: middle, bottom: bottom))

    return trapezoid
}

let trapezoid = makeTrapezoid()
print(trapezoid.draw())

你也可以用范型组合不透明类型。下面代码的函数都返回服从 Shape 协议的某个类型

func flip<T: Shape>(_ shape: T) -> some Shape {
    return FlippedShape(shape: shape)
}

func join<T: Shape, U: Shape>(_ top: T, _ bottom: U) -> some Shape {
    JoinedShape(top: top, bottom: bottom)
}

let opaqueJoinedTriangles = join(smallTriangle, flip(smallTriangle))
print(opaqueJoinedTriangles.draw())

如果一个函数从多个地方返回一个不透明类型,所有可能的返回值必须是相同的类型。对一个范型函数,返回值可使用函数的范型类型参数,但它必须为一个单一类型。例如,下面代码是一个无效版本包含一个正方形的特殊情况

func invalidFlip<T: Shape>(_ shape: T) -> some Shape {
    if shape is Square {
        return shape            // Error: return types don't match
    }

    reutrn FlippedShape(shape: shape) // Error: return types don't match
}

如果你用正方形调用这个函数,它返回一个正方形;否则,它返回一个翻转的形状。这个违反了返回值只有一种类型的要求使得该函数是无效代码。一种修复方式是把正方形的特例放入 FlippedShape 的实现中,使得函数总是返回一个翻转形状

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

总是返回单一类型的要求不防止你在一个不透明类型中使用范型返回。以下是一个函数示例兼容它的类型参数为底层的类型值返回

func `repeat`<T: Shape>(shape: T, count: Int) -> some Collection {
    return Array<T>(repeating: shape, count: count)
}

在这个例子中,返回值的底层类型依赖 T,它总是返回相同底层类型的 T 数组,这样它满足函数返回不透明类型为单一类型的要求

封装协议类型

一个封装协议类型有时候也称为一个存在主义类型,来自于语义存在一个类型 T 使得 T 服从协议。为制作一个封装协议类型,在 protocol 关键字前写 any 关键字。下面是一个例子

struct VerticalShapes: Shape {
    var shapes: [any Shape]
    func draw() -> String {
        return shapes.map { $0.draw() }.joined(separator: "\n\n")
    }
}

let largeTriangle = Triangle(size: 5)
let largeSquare = Square(size: 5)
let vertical = VerticalShapes(shapes: [largeTriangle, largeSquare])
print(vertical.draw())

上面例子 VerticalShapes 声明 shapes 的类型为 [any Shape] - 一个封装 Shape 元素的数组。数组中每个元素可为不同的类型,且每个必须服从 Shape 协议。为支持这个运行时灵活性,Swift 添加了一层间接层当需要时 = 这个间接层被称为一个盒子,且它有一个性能成本

在 VerticalShapes 类型中,代码可使用 Shape 协议需的方法,属性和下标。例如,draw() 方法调用了数组中每个元素的 draw() 方法。这个方法是有效的因为 Shape 有一个 draw() 方法。相反,尝试访问 triangle 的 size 属性,或任意不为 Shape 要求的属性或方法,会产生一个错误

对照在 shapes 上你可以使用的三种类型:

  • 使用范型,struct VerticalShapes<S: Shape> 和 var shapes: [S],使得数组中元素为相同的某种形状类型,且该形状类型与任意和该数组交互的代码可见
  • 使用一个不透明类型,写 var shapes: [some Shape],使得一个数组可存储不同类型的元素,且数组确定类型被隐藏
  • 使用一个封装协议类型,写 var shapes: [any Shape],使得一个数组可存储不同类型的元素,且这些类型的确切类型被隐藏

在这个例子中,一个封装协议类型是唯一的方式让 VerticalShapes 的调用者混合不同类型的形状在一起

你可使用 as 关键字转换你知道的封装值下的底层类型,例如

if let downcastTriangle = vertical.shapes[0] as? Triangle {
    print(downcastTriangle.size)
}

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

返回一个不透明类型跟使用一个封装协议类型做为函数返回值看起来非常相似,但这两种类型在是否保持类型确定性上不同。一个不透明类型引用一个特定的类型,虽然调用者不能看到具体类型;一个封装协议类型可引用服从协议的任意类型。一般来说,封装协议类型对它们底层存储的值类型给你更多的灵活性,且不透明类型让你对这些底层类型有更强的保证

例如,下面版本的 flip(_:) 使用一个封装协议类型作为返回值类型而不是返回一个不透明类型

func protoFlip<T: Shape>(_ shape: T) -> Shape {
    return FlippedShape(shape: shape)
}

与 flip(_:) 不同,protoFlip( _:) 值返回不需要总是相同的类型 - 它只需要服从 Shape 协议就可以。即 protoFlip( _: ) 是一个比 flip( _: ) 更松散的 API,它保留返回多个类型值的灵活性

func protoFlip<T: Shape>(_ shape: T) -> Shape {
    if shape is Square {
        return shape
    }

    return FlippedShape(shape: shape)
}

这个修改版本会返回不同类型,这个版本返回值对依赖于类型信息的许多操作来说会变得无效。例如,不能用 == 号比较这个函数的返回结果

let protoFlippedTriangle = protoFlip(smallTriangle)
let someThing = protpFlip(smallTriangle)
protoFlippedTriangle == sameThing // Error

例子最后一行的错误发生有几个原因。Shape 协议不包含一个 = 操作符作为协议需求。如果你尝试添加一个,则 = 操作符需要知道左边和右边参数的类型。这类操作符通常用 Self 作为参数,匹配适应协议的具体类型,但添加一个 Self 到协议当你使用协议作为类型时不允许类型擦除

使用一个封装协议类型作为函数返回类型给你返回任意服从协议类型的灵活度。然而,灵活度的成本是一些操作不能在返回值上。上面例子显示了 == 操作符是如何无效的 - 它依赖特殊类型信息但使用封装协议类型后没有保留

这个处理的另一个问题是 Shape 转换不能嵌套。翻转一个三角形的结果是一个 Shape 类型的值,protoFlip( _: ) 函数是带一个服从 Shape 协议的某种类型的参数。然而,一个封装协议类型的值不能服从协议;protoFlip( _: ) 返回的值不能服从 Shape 协议。这意味着像 protoFlip(protoFlip(smallTriangle)) 这样的代码应用多次转换是无效的因为翻转的形状对 protoFlip( _:) 不是一个有效的参数

相反,不透明类型防止底层类型的唯一性。Swift 可引用关联类型,其让你使用一个不透明返回值作为一个封装协议类型不能作为返回值的替代。例如,下面这个范型版本的 Container 协议

protocol Container {
    associatedtype Item
    var count: Int { get }
    subscript(i: Int) -> Item { get }
}
extension Array: Container {}

你不能使用 Container 作为一个函数的返回值类型因为协议有一个关联类型。你也不能使用它因为范型返回类型的限制导致没有足够信息在函数体外来引用当范型类型需要时

// Error: Protocol with associated types can't be used as a return type.
func makeProtocolContainer<T>(item: T) -> Container {
    return [item]
}

// Error: Not enough information to infer C.
func makeProtocolContainer<T, C: Container>(item: T) -> C {
    return [item]
}

使用不透明类型 some Container 作为返回值类型表达想要的 API 联系 - 函数返回一个容器,但倾向于指明容器的类型

func makeOpaqueContainer<T>(item: T) -> some Container {
    return [item]
}

let opaqueContainer = makeOpaqueContainer(item: 12)
let twelve = opaqueContainer[0]
print(type(of: twelve))

twelve 的类型是 Int,表明类型引用在不透明类型中起作用。在 makeOpaqueContainer(item:) 的实现中,不透明容器的底层类型是 [T]。在这个例子中,T 是 Int,这样返回值是一个整形数组,意味着 twelve 也是一个 Int

Share

阅读苹果官方文档

苹果官方文档还是对新手很友好的,基本上会循循渐进地把知识点重点都会系统的讲到。当问题是某些地方稍显啰嗦,阅读地时候如果快速跳过又有时候会漏掉一些重要的知识细节

完全从头开始看效率太低,一般有人推荐的比较好,根据需要指定看哪些重点章节,其他则暂时跳过。这样兼顾效率

由于文档讲得较细,有些地方看地时候会容易快速跳过而漏过一些知识点,则当某些章节是推荐内容时,则对这些内容要重点细看,轻易不要跳,效果会好很多。因为苹果提到的细节一般都是很关键或容易混淆出错的,跳过或漏看对知识细节的把握就欠妥,后续遇到相关问题还是会吃苦头的。因此对推荐关键章节阅读要有耐心才行