这是Swift编程思想的系列文章:
[Swift 编程思想(四)面向响应式编程(待完成)]
理解泛型
泛型的定义
在Swift语言中,泛型是一种编程技术,它允许你编写灵活、可重用的函数和类型,可以工作于任何类型。泛型的主要好处是它可以帮助你避免重复代码,并用一种清晰和抽象的方式来表达代码的意图。
泛型 的占位类型,可以是 T ,也可以是U ,完全由您决定,使用一个由含义的占位符,更能表达含义。例如:系统对数组的定义
struct Array<Element>
。
在某场技术活动中,需要管理会场中的观众,每人必须观看两个小时才可以离场(先进先出,无法提前离场)。
会场分为三个会场,分别有以下要求:
在主会场A中,由于准备充分,每个入场的观众发放参会证,使用参会证上的号码入场。
使用 Stack 管理观众的入场和离场。Stack通过push方法记录入场的用户号码,通过pop方法移除离场的用户。
swift
struct Stack {
var items = [Int]()
mutating func push(_ item: Int) {
items.append(item)
}
mutating func pop() {
return items.removeFirst()
}
}
泛型函数
活动火爆,观众来的很多,主办方又开展了分会场B。由于时间有限,没有制作入场证,每个入场的观众的凭身份证(姓名)入场。为了能让Stack不仅可以管理会场A,同时也可以管理会场B。我们进行了如下修改:
swift
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要不要记录?
stock可以push任意类型,开发人员头大,难以管理。
scss
stock.push(1)
stock.push("1123")
stock.push(UIView())
技术大佬说: 使用泛型类型。会场对应的Stack明确类型。
swift
struct Stack<Element> {
var items = [Element]()
mutating func push(_ item: Element) {
items.append(item)
}
mutating func pop() {
return items.removeFirst()
}
}
主会场A中要求: 只能进入有号码的观众。
css
var a = Stack<Int>()
a.push(1)
a.push(2)
分会场B中要求: 只能进入使用名称的观众。
less
var a = Stack<Sting>()
a.push("小明")
a.push("大黄")
完美的结局了问题。
泛型扩展
领导来视察,问最后一个进入的是谁?
开发人员慌了,开发的时候没有设计,怎么办?
技术大佬说: 别慌,使用泛型扩展。
kotlin
extension Stack {
var count: Int {
return items.count
}
}
当对泛型类型进行扩展时,原始类型定义中声明的类型参数列表在扩展中可以直接使用,并且这些来自原始类型中的参数名称会被用作原始定义中类型参数的引用。
泛型约束
主办方又开了一个会场C,这个会场主题是:关爱女性(只允许女性进入)。
开发人员慌了,怎么办?怎么办?
技术大佬淡定的说: 使用泛型约束,只允许女性(实现了Womenable协议的Element)进来就好了。
swift
// 女性的协议
protocol Womenalbe { }
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
协议。
swift
func someFunction<T: SomeClass, U: SomeProtocol>(someT: T, someU: U) {
// 这里是泛型函数的函数体部分
}
泛型与协议
A和B会场中的观众意见很大: 我要上厕所,不要限制我!!!
主办方在正常离开通道旁边,开通了临时通道:可以不到时间就可以离场。
但是女性会场有厕所,为了保证参会效果,暂时不允许通行,有人通过给予提示:场内有厕所,暂不支持提前离场。
开发人员又挠头了,一个方法怎么支持两种实现?
会场A和会场B中,依然采用先进先出的管理措施。
swift
mutating func pop() -> Element {
return items.removeFirst()
}
会场C中,支持提前离场。
swift
mutating func pop(_ item: Element) {
print("场内有厕所,暂不支持提前离场")
}
通过协议定义能力
技术大佬也陷入了思考......
只见先实现了Container协议,他说将以前的实现抽离出来:
- items: 定义为会场中的人
- last:最后一个进入会场的人
- push:记录进入会场的人的方法
- pop:记录离开会场的人的方法
swift
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)
}
在这三个会场中同样的实现的统一实现:
swift
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]
}
}
泛型结合协议使用
三个会场的差异性的实现:
swift
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: Womenalbe>: Container {
var items: [T] = []
mutating func pop(with item: T) {
print("场内有厕所,暂不支持提前离场")
}
}
给关联类型添加约束
场馆C中的观众非常不满: 为什么要区别对待? 我们为什么不能离场?
开发人员心想: 这还不简单? 参考大佬的实现,改一下就行了。
swift
struct StackC<T: Womenalbe>: 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,才可以进行比较。
css
protocol Womenalbe: Equatable { }
让 Womenable 也遵循 Equatable 协议就好了。
此时的 T 类型:不仅要是Womenalbe,还要是Equatable,才可以提前离场。
具有泛型 Where 子句的扩展
主办方发现C场馆中,有一些带孩子的母亲有提前离场的需要。要求:在C场馆中带孩子的女性可以提前离场。
swift
protocol Childable { }
extension StackC where T: Childable {
mutating func pop(with item: T) {
items.removeAll { $0 == item }
}
}
面向泛型编程
通过上面的学习,我们对泛型有了一定的了解,那么使用泛型进行编程相信也可以胜任了。
面向泛型编程是一种编程范式,强调代码的通用性和复用性。
- 通用性:通过泛型编写的代码可以适用于多种数据类型。
- 类型安全:泛型保持类型安全,减少类型转换和错误。
- 可复用性:泛型提高代码复用性,减少重复代码。
- 抽象编程:通过定义泛型和协议,可以更抽象地处理问题,更专注于逻辑而非类型特定的细节。
总结:Swift 的泛型和面向泛型编程思想,为 Swift 编程提供了高效、安全和易于维护的代码写法。
1. 通用性
泛型让你能够写出适用于任何类型的代码。这减少了对特定类型的依赖,从而增加了代码的灃用范围。
例子:
swift
func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
let temporaryA = a
a = b
b = temporaryA
}
这个函数可以交换任何类型的两个值,无论是整数、字符串还是自定义类型。
2. 类型安全
在泛型编程中,类型是在编译时确定的,这保证了类型的正确性和安全性。
例子:
swift
struct Stack<Element> {
var items = [Element]()
mutating func push(_ item: Element) {
items.append(item)
}
mutating func pop() -> Element {
return items.removeLast()
}
}
在这个 Stack
示例中,你可以为栈指定存储的元素类型(如 Int
或 String
),并确保只有正确的类型能被添加到栈中。
3. 可复用性
通过泛型,你可以写出高度复用的代码,减少重复和冗余。
例子:
swift
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. 抽象编程
泛型编程鼓励从具体实现中抽象出来,关注逻辑而非具体类型。
例子:
swift
protocol Container {
associatedtype Item
mutating func append(_ item: Item)
var count: Int { get }
subscript(i: Int) -> Item { get }
}
这个 Container
协议定义了一个容器应有的基本行为,而不关心容器中具体存储的元素类型。任何遵循此协议的类型都需要实现这些基本行为,提供了一种高层次的抽象。
这些例子展示了如何通过泛型来实现面向泛型编程的四个主要特点,体现了 Swift 泛型编程的灵活性和强大功能。