Swift 让编译器自动生成类型 | 七日打卡

1,726 阅读6分钟

点赞评论,感觉有用的朋友可以关注笔者公众号 iOS 成长指北,持续更新

本文是 《Swift 100 Days》系列的的第 14 天, Swift 100 Days 是笔者记录自己 Swift 学习的记录,欢迎各位指正。

在我们过往的讨论中,我们多次提及了泛型的概念。泛型是 Swift 编程语言最强大的功能之一。

作为一种类型安全的语言,泛型是 Swift 的核心特性——包括它的标准库,也大量使用泛型。在学习之前,你应该对Swift的类型、类和协议有基本的了解。 Swift 中的复杂数据类型 一文中提及的 ArrayDictionarySet,都是使用了泛型。

为什么要使用泛型

自动类型生成是泛型解决的问题!

当我们编写可以应用于许多不同类型的代码时,泛型特别有用。

协议与拓展 一节中,我们利用协议来处理商场中不同类型产品的售卖操作。这其实就是一种泛型思想的落实。

protocol Purchaseable {
    // 商品名称
    var name: String { get set }
    // 折扣
    var discount: Double { get }
}

struct Customer {
    var shoppingList = Array<Purchaseable>()
  // 购买
    mutating func buy(_ product: Purchaseable) {
        self.shoppingList.append(product)
    }
}

如果我们不遵循 Purchaseable 协议,我们需要为用户添加

struct Customer {
  var shoppingBookList = Array<Book>()
  var shoppingClothesList = Array<Clothes>()
  ...
  // 购买书籍
  mutating func buyBook(_ book: Book) {
    self.shoppingBookList.append(book)
  }
  // 购买服装
  mutating func buyclothes(_ clothes: Clothes) {
    self.shoppingBookList.append(clothes)
  }
  ...
}

在实际应用中,我们应该遵循一种被称作 Don’t Repeat Yourself (DRY) 的软件开发原则,用来减少代码和应用程序中的重复的代码块。

我们应该尽可能的减少重复的代码块。

让我们开始在我们的代码中使用泛型吧!

编写含有泛型的结构体

让我们定义一个包含任意类型和日期对象的结构体,Container

struct Container<Value> {
    var value: Value
    var date: Date
}

let stringContainer = Container(value: "iOS成长指北", date: Date())
let intContainer = Container(value: 2000, date: Date())
let dateContainer = Container(value: Date(), date: Date())
print("stringContainer.value = ", stringContainer.value)
print("intContainer.value = ", intContainer.value)
print("dateContainer.value = ", dateContainer.value)
//stringContainer.value =  iOS成长指北
//intContainer.value =  2000
//dateContainer.value =  2021-01-07 09:49:34 +0000

Container 是一种泛型类型,其泛型自变量子句中具有类型自变量 Value。另一种说法是,ContainerValue 类型上的泛型。例如,Container<String> Queue<Int> 在运行时将成为它们自己的具体类型。

Value 被称为占位符类型。这告诉 Swift,Value不是实际类型,而是 Container 中的占位符

编写含有泛型的函数

在商品的例子中,我们为消费者创建了购物的方法,我们将这个方法改成支持泛型的

struct Customer<Element> {
    var shoppingList = Array<Element>()
  // 购买
    mutating func buy(_ product: Element) {
        self.shoppingList.append(product)
    }
}

此时,我们的类型为任意类型,但是我们需要让我们的类型遵循 Purchaseable 协议,我们可以

使用泛型类型约束

如果能对泛型函数或泛型类型中添加特定的类型约束,这将在某些情况下非常有用。类型约束指定类型参数必须继承自指定类、遵循特定的协议或协议组合。

struct Customer<Element: Purchaseable> {
    var shoppingList = Array<Element>()
  // 购买
    mutating func buy(_ product: Element) {
        self.shoppingList.append(product)
        print("product.name", product.name)
    }
}

当自定义泛型类型时,你可以定义你自己的类型约束,这些约束将提供更为强大的泛型编程能力。

如果我们在方法中使用类型约束的话,我们可以这么做

struct Customer {
  // 购买
    mutating func buy<Element: Purchaseable>(_ product: Element) {
        print("product.name", product.name)
    }
}

除了自定义的协议,Swift提供了以下一些基本协议:

  • Equatable 对于那些可以相等或不相等的值
  • Comparable 或可以比较的值,例如a> b
  • Hashable 用于可哈希的值,该值是该值的唯一整数表示形式(通常用于字典键)
  • CustomStringConvertible 用于可以表示为字符串的值,这是一种有助于快速将自定义对象转换为可打印字符串的有用协议
  • NumericSignedNumeric 指数字值,例如423.1415
  • Strideable 可以偏移和测量的值

关联类型

Swift 中的关联类型与通常与协议密切相关。

你可以从字面上把它们看作是协议的一种关联类型:从你把它们放在一起的那一刻起,它们就是一体的。

关联的类型可以看作是协议定义中特定类型的替代。换句话说:这是在采用协议并指定确切类型之前要使用的类型的占位符名称。

关联类型通过 associatedtype 关键字来指定。

下面例子定义了一个 Identifiable 协议,该协议定义了一个关联类型 ID

protocol Identifiable {
    associatedtype ID: Equatable & CustomStringConvertible

    var id: ID { get }
}

然后在我们遵循 Identifiable 协议的对象中给他加上不同类型的 id

struct Book: Identifiable {
    let id: String
}

struct Clothes: Identifiable {
    let id: Int
}

当我们加入到购物车时,我们可以根据 id 是否相同来判断我们加入购物车的商品的数量。并且各个类型的id 都可以有他们自己的类型,甚至声明出来。

通过简化针对多个场景的通用接口,它们可以防止编写重复的代码。这样,可以将同一逻辑用于多种不同类型,从而只允许编写和测试一次逻辑。

这与泛型的优势很像是不是?

结合泛型、协议和关联类型的使用

定义一个集合协议Collection

protocol Collection {
    associatedtype Item: Equatable

    var count: Int { get }
    subscript(index: Int) -> Item { get }
    mutating func append(_ item: Item)
}

然后为集合协议添加一个新的 CollectionSlice 协议用于获取集合的前 n 个,并保证类型相等。

protocol CollectionSlice: Collection {
    associatedtype Slice: CollectionSlice where Slice.Item == Item
    func prefix(_ maxLength: Int) -> Slice
}

然后我们定义一个遵循 Collection 协议的结构体 UppercaseStringsCollection

struct UppercaseStringsCollection: Collection {
    
    var container: [String] = []
  
    var count: Int { container.count }
  
    mutating func append(_ item: String) {
        guard !container.contains(item) else { return }
        container.append(item.uppercased())
    }
    
    subscript(index: Int) -> String {
        return container[index]
    }
}

然后我们为UppercaseStringsCollection 新增一个遵循 CollectionSlice 协议的拓展

extension UppercaseStringsCollection: CollectionSlice {
    func prefix(_ maxLength: Int) -> UppercaseStringsCollection {
        var collection = UppercaseStringsCollection()
        for index in 0..<min(maxLength, count) {
            collection.append(self[index])
        }
        return collection
    }
}

结合泛型、协议和关联类型的使用,在 Swift 中很常见。

总结

总的来说,泛型是一种强大功能,它使我们能够编写更易于重用的代码,同时还支持本地专门化。

算法、数据结构和实用程序通常是泛型的最佳候选者,因为它们通常只需要它们所使用的类型来满足特定的一组需求,而不是绑定到特定的具体类型。

不透明类型

Swift 5.1之后,我们可以使用另一种方法来处理泛型:不透明类型。some 关键字允许隐藏属性或函数的具体返回类型。返回的具体类型可以由实现本身决定,而不是由调用代码决定。这就是为什么不透明类型有时被称为反向泛型

不透明类型和泛型是相关的。

  • 使用泛型的占位符,常见的 T,其类型是由函数的调用者确定占位符 T的具体类型。
  • 对于不透明类型,函数实现将确定具体类型。

我们依旧按照我们之前商品的例子来说明不透明类型的作用。

我们为商品协议定义一个买家

protocol Purchaseable {
    associatedtype Buyer
    var buyer: Buyer { get }
    // 商品名称
    var name: String { get set }
}

然后我们定义书籍和衣服两种产品

struct Book: Purchaseable {
    var buyer: String = "iOS 成长指北"
    var name: String = "我是一本书"
}

struct Clothes: Purchaseable {
    var buyer: Int = 89757
    var name: String = "我是一件衣服"
}

定义一个快递员 Courier,快递员将商品送给买家,但是他不需要知道买家买的具体是什么,他可以是符合商品协议的任何一个。

//快递员
struct Courier {
    func delivery() -> some Purchaseable {
        return Book()
    }
}

此时就是不透明类型的使用场景了。

泛型类型占位符允许函数的调用者具体化泛型函数中使用的类型。协议类型允许我们从函数中返回任何类型,只要它符合协议。

但是这不适用于关联的类型,因为缺少具体的类型信息。因此,我们使用 some 关键字创建不透明类型,反向泛型,并让函数的实现确定返回值的具体类型以及任何关联类型的具体类型。

很绕口是不是?

总结一下几点

  • 首先,不透明类型可以将带有关联类型的协议用作返回类型。
  • 其次,与协议类型不同是,不透明类型会保留类型身份。
  • 第三,最后但同样重要的是,不透明类型对于SwiftUI至关重要——我们会在学习 Swift UI 时进行讲解。

消化一下今天的内容。我们下次再说。

感谢你阅读本文! 🚀