Swift编程思想(三) - 面向泛型编程

理解泛型

泛型的定义
在 Swift 语言中,泛型是一种编程技术,它允许你编写灵活、可重用的函数和类型,可以工作于任何类型。泛型的主要好处是它可以帮助你避免重复代码,并用一种清晰和抽象的方式来表达代码的意图。
泛型的占位类型,可以是 T,也可以是 U,完全由您决定,使用一个由含义的占位符,更能表达含义。例如:系统对数组的定义 struct Array<Element>

在某场技术活动中,需要管理会场中的观众,每人必须观看两个小时才可以离场(先进先出,无法提前离场)。

会场分为三个会场,分别有以下要求:

  1. 主会场 A:有入场号可以进
  2. 分会场 B:使用姓名就可以进
  3. 女性会场 C: 有号码的女性可以进

在主会场 A 中,由于准备充分,每个入场的观众发放参会证,使用参会证上的号码入场。

使用 Stack 管理观众的入场和离场。Stack 通过 push 方法记录入场的用户号码,通过 pop 方法移除离场的用户。

 struct Stack {
    var items = [Int]()
    mutating func push(_ item: Int) {
        items.append(item)
    }
    mutating func pop() {
        return items.removeFirst()
    }
 }

泛型函数

活动火爆,观众来的很多,主办方又开展了分会场 B。由于时间有限,没有制作入场证,每个入场的观众的凭身份证(姓名)入场。为了能让 Stack 不仅可以管理会场 A,同时也可以管理会场 B。我们进行了如下修改:

 struct Stack {
    var items = [Any]()
    mutating func push<T>(_ item: T) {
        items.append(item)
    }
    mutating func pop() {
        return items.removeFirst()
    }
 }

泛型函数可适用于任意类型。泛型函数使用 占位符 类型名(这里叫做 T ),而不是实际类型名(例如 Int、String 或 Double),占位符 类型名并不关心 T 具体的类型,只有在使用的时候才确定类型。

泛型类型

一个球被踢入了会场,一个气球被吹入了会场,Stack 要不要记录?

stack 可以 push 任意类型,开发人员头大,难以管理。

stack.push(1)
stack.push("1123")
stack.push(UIView())

技术大佬说: 使用泛型类型。会场对应的 Stack 明确类型。

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

主会场 A 中要求: 只能进入有号码的观众。

 var a = Stack<Int>()
 a.push(1)
 a.push(2)

分会场 B 中要求: 只能进入使用名称的观众。

 var a = Stack<Sting>()
 a.push("小明")
 a.push("大黄")

完美的解决了问题。

泛型扩展

领导来视察,问最后一个进入的是谁?

开发人员慌了,开发的时候没有设计,怎么办?

技术大佬说: 别慌,使用泛型扩展。

 extension Stack {
     var count: Int {
         return items.count
     }
 }

当对泛型类型进行扩展时,原始类型定义中声明的类型参数列表在扩展中可以直接使用,并且这些来自原始类型中的参数名称会被用作原始定义中类型参数的引用。

泛型约束

主办方又开了一个会场 C,这个会场主题是:关爱女性(只允许女性进入)。

开发人员慌了,怎么办?怎么办?

技术大佬淡定的说: 使用泛型约束,只允许女性(实现了Womenable协议的 Element)进来就好了。

 // 女性的协议
 protocol Womenable { }
 ​
 struct Stack<Element: Womenable> {
    var items = [Element]()
    mutating func push(_ item: Element) {
        items.append(item)
    }
    mutating func pop() {
        return items.removeFirst()
    }
 }

在一个类型参数名后面放置一个类名或者协议名,并用冒号进行分隔,来定义类型约束。下面将展示泛型函数约束的基本语法(与泛型类型的语法相同)。下面这个函数有两个类型参数,第一个类型参数 T必须是 SomeClass 子类;第二个类型参数 U必须符合 SomeProtocol 协议。

 func someFunction<TSomeClassUSomeProtocol>(someTTsomeUU) {
     // 这里是泛型函数的函数体部分
 }

泛型与协议

A 和 B 会场中的观众意见很大: 我要上厕所,不要限制我!!!
主办方不得不在正常离开通道旁边,开通了临时通道:该通道可以不到时间就可以离场。但是女性会场有厕所,为了保证参会效果,暂时不允许通行。
开发人员又挠头了,一个方法怎么支持两种实现?
会场 A 和会场 B 中,临时通道可以提前离场。

 mutating func pop(_ item: Element) {
    items.removeAll { $0 == item }
 }

会场 C 中,不支持提前离场,有人要离开,给予提示信息。

 mutating func pop(_ item: Element) {
    print("场内有厕所,暂不支持提前离场")
 }

通过协议定义能力

技术大佬也陷入了思考……

只见他先声明了 Container 协议,边写边说:将以前的实现抽离出来:

  • items: 定义为会场中的人
  • last:最后一个进入会场的人
  • push:记录进入会场的人的方法
  • pop:记录离开会场的人的方法
 protocol Container {
    associatedtype Element
    var items: [Element] { get set }
    var last: Element? { get }
    mutating func push(_ item: Element)
    mutating func pop()
    mutating func pop(with item: Element)
 }

//在这三个会场中同样的逻辑进行统一实现:

 extension Container {
    mutating func push(_ item: Element) {
        items.append(item)
    }
     
    mutating func pop() {
        items.removeFirst()
    }
     
    var last: Element? {
        return items.isEmpty ? nil : items[items.count - 1]
    }
 }

泛型结合协议使用

三个会场的差异性的实现:

 struct StackA: Container {
    var items: [Int] = []
    mutating func pop(with item: Int) {
        items.removeAll { $0 == item }
    }
 }
 ​
 struct StackB: Container {
    var items: [String] = []
    mutating func pop(with item: String) {
        items.removeAll { $0 == item }
    }
 }
 ​
 struct StackC<T: Womenable>: Container {
     
    var items: [T] = []
    mutating func pop(with item: T) {
        print("场内有厕所,暂不支持提前离场")
    }
 }

给关联类型添加约束

场馆 C 中的部分观众带了孩子: 期望可以提前离场,照顾孩子的情绪。

开发人员心想: 这还不简单? 参考大佬的实现,改一下就行了。

 struct StackC<T: Womenable>: Container {
    var items: [T] = []
    mutating func pop(with item: T) {
        items.removeAll { $0 == item }
    }
 }

❌ 报错了: Referencing operator function '==' on 'Equatable' requires that 'T' conform to 'Equatable'

没事,这个我知道: T 类型需要遵守 Equatable,才可以进行比较。

protocol Womenable: Equatable { }

让 Womenable 也遵循 Equatable 协议就好了。

此时的 T 类型:不仅要是 Womenable,还要是 Equatable,才可以提前离场。

但是如何限制只允许带孩子的提前离场呢?

具有泛型 Where 子句的扩展

技术大佬说: 使用带 Where 子句的扩展,做个限制。

 protocol Childable { }
 extension StackC where T: Childable {
    mutating func pop(with item: T) {
        items.removeAll { $0 == item }
    }
 }

泛型的运用

对于 iOS 开发而言,绕不过MVC的设计模式,臃肿的 C 层令人头疼。
按照苹果的设计理念:
UIViewController对应MVCCUIView对应MVCVXXXModel对应MVCM.
臃肿的原因是 C 层承担了 V 层的职责。 可以通过泛型解决该问题,使 Controller 与 View 的分离更加优雅。

创建控制器基类

一个控制器只管理一个 Container,其他的子 View 都放到 Container 中。

class BaseViewController<Container: UIView>: UIViewController {
    var container: Container { view as! Container }

    override func loadView() {
        super.loadView()
        if view is Container {
            return
        }
        view = Container()
    }
}

实现首页控制器

class HomeController: BaseViewController<HomeView> {
// 简单的Model
    private let model = HomeModel()

    override func viewDidLoad() {
        super.viewDidLoad()
        container.set(title: model.name)
    }
}

View 层

其他的子 View 都会加入到该层。

class HomeView: UIView {
    private lazy var titleLabel = UILabel()

    override init(frame: CGRect) {
        super.init(frame: frame)

        titleLabel.textColor = .black
      titleLabel.frame = .init(x: 000, y: 200, width: 100, height: 40)
        titleLabel.font = .systemFont(ofSize: 13, weight: .semibold)
        addSubview(titleLabel)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    func set(title: String) {
        titleLabel.text = title
    }
}

总结

泛型的使用使 Controller 与 View 的分离更加优雅。
头部的声明可以直观的看到 Controller 的 View 类型, 可读性强.
因明确了类型 调用更加顺畅自然, 省去了多余的类型转换代码,使 Controller 可以更专注于 Model 与 View 的协调和调用, 职责更明确。

面向泛型编程

通过上面的学习,我们对泛型有了一定的了解,那么使用泛型进行编程相信也可以胜任了。

面向泛型编程是一种编程范式,强调代码的通用性和复用性。

  1. 通用性:通过泛型编写的代码可以适用于多种数据类型。
  2. 类型安全:泛型保持类型安全,减少类型转换和错误。
  3. 可复用性:泛型提高代码复用性,减少重复代码。
  4. 抽象编程:通过定义泛型和协议,可以更抽象地处理问题,更专注于逻辑而非类型特定的细节。

Swift 的泛型和面向泛型编程思想,为 Swift 编程提供了高效、安全和易于维护的代码写法。

1. 通用性

泛型让你能够写出适用于任何类型的代码。这减少了对特定类型的依赖,从而增加了代码的使用范围。

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

这个函数可以交换任何类型的两个值,无论是整数、字符串还是自定义类型。

2. 类型安全

在泛型编程中,类型是在编译时确定的,这保证了类型的正确性和安全性。

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

在这个 Stack 示例中,你可以为栈指定存储的元素类型(如 IntString),并确保只有正确的类型能被添加到栈中。

3. 可复用性

通过泛型,你可以写出高度复用的代码,减少重复和冗余。

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

这个函数可用于查找任何遵循 Equatable 协议类型的数组中的元素,无论是数字、字符串还是其他自定义可比较类型。

4. 抽象编程

泛型编程鼓励从具体实现中抽象出来,关注逻辑而非具体类型。

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

这个 Container 协议定义了一个容器应有的基本行为,而不关心容器中具体存储的元素类型。任何遵循此协议的类型都需要实现这些基本行为,提供了一种高层次的抽象。
这些例子展示了如何通过泛型来实现面向泛型编程的四个主要特点,体现了 Swift 泛型编程的灵活性和强大功能。