泛型在开发中的使用场景主要在以下几方面
- A. 泛型函数
- B. 泛型类型
- 扩展泛型类型
- C. 泛型约束
- 协议约束
- 函数的协议约束的语法
- 类或结构体的协议约束的语法
- 继承约束
- 条件约束
- D. 泛型下标
A. 泛型函数
泛型函数指的是:函数的
参数
或返回值
类型使用泛型
,而不是具体的类型
泛型函数的格式:
func 函数名<泛型1, 泛型2, …>(形参1, 形参2, ...) -> 返回类型 {
函数体
}
需求:实现Swift高阶函数
reduce功能的一个函数
func myReduce<T, U>(arr: [T], initialValue: U, partialResult: (U, T) -> U) -> U {
var result = initialValue
arr.forEach {
result = partialResult(result, $0)
}
return result
}
myReduce这个函数有两个占位类型,T
和U
:
- T 作为形参arr的数组元素占位类型和形参partialResult闭包的第二个入参占位类型
- U 不仅作为了形参initialValue和形参partialResult闭包的第一个参数占位类型,也作为闭包partialResult的返回类型和整个函数的返回类型
泛型函数真的可以支持所有的类型吗?看疗效:
let array = ["1", "2", "3", "4", "5"]
let r1 = myReduce(arr: array, initialValue: 0) { $0 + (Int($1) ?? 0) }
// 打印结果:转为Int后的求和结果:15
print("转为Int后的求和结果:\(r1)")
let nums = [1, 2, 3, 4, 5]
let r2 = myReduce(arr: nums, initialValue: []) { $1 % 2 == 0 ? $0 + ["\($1)"] : $0 }
// 打印结果:偶数转为字符串数组:["2", "4"]
print("偶数转为字符串数组:\(r2)")
这个泛型函数正常工作了:
- 第一个例子,T代表String,U代表Int,代码意思是将数组中的每个String元素转换成Int然后累计求和,将String和Int分别代入myReduce函数的T和U,就能明白了
- 第二个例子,T代表Int,U代表[String],代码意思是将数组中每个Int元素的偶数取出来转成一个新的String数组,将Int和[String]本别代入myReduce函数的T和U,分析一下,就明白了
B. 泛型类型
除了泛型函数
,Swift还允许定义泛型类型
。这些自定义类
、结构体
和枚举
可以适用于任何类型,类似于Array和Dictionary。
// Array 的定义
public struct Array<Element> {
// ...
}
// Dictionary 的定义
public struct Dictionary<Key, Value> where Key : Hashable {
// ...
}
- Array后面尖括号中的Element就是Array的定义的泛型类型
- Dictionary尖括号中的Key、Value就是Dictionary定义的泛型类型
// Array 的使用
let a1: Array<String> = ["a", "b"]
let a2: Array<Int> = [1, 2]
// Dictionary 的使用
let d1: Dictionary<String, String> = ["a": "b"]
let d2: Dictionary<String, Int> = ["a": 1]
- a1的String和a2的Int就是Array定义的泛型Element的具体类型
- d1的String、String和d2的String、Int就是Dictionary的泛型Key、Value的具体类型
不过一般写代码的时候,我们都是使用如下的方式:
let a: [String] = ["a", "b"]
let d: [String: String] = ["a": "b"]
所以对泛型的感知并不强
。
泛型类型的使用和泛型函数差不多,就是在类型名后面加上<泛型1, 泛型2, …>
,然后在类型里面直接使用泛型。
举个例子,实现一个简易版的迭代器:
struct CustomIterator<Element> {
var elements: [Element] = []
var num = 0
mutating func next() -> Element? {
if num == elements.count {
num = 0
return nil
}
num += 1
return elements[num - 1]
}
init(elements: [Element], num: Int = 0) {
self.elements = elements
self.num = num
}
}
CustomIterator
结构体名字后面的Element就是泛型类型,对CustomIterator来说,它实现的是一个简易迭代
的功能,与类型无关
,只要能装进Array的类型都可以使用这个功能。
Element
为待提供的类型定义了一个占位名
。这种待提供的类型可以在结构体的定义中通过Element来引用。在这个例子中Element在如下三个地方被用作占位符:
- 指定init方法的第一个参数必须是Element的数组
- 创建elements属性,使用Element类型的空数组对其进行初始化
- 指定next方法的返回值,指定是可选的Element
与泛型函数类似,都是在调用
的时候会确定泛型的具体类型
。
var cusIterStr = CustomIterator<String>(elements: ["2", "3", "6"])
while let ele = cusIterStr.next() {
print(ele)
}
/*
打印结果:
2
3
6
*/
var cusItemrDouble = CustomIterator<Double>(elements: [22.13, 45.67, 98.12])
while let ele = cusItemrDouble.next() {
print(ele)
}
/*
打印结果:
22.13
45.67
98.12
*/
第一个例子Element代表的是String,第二个例子Element代表的是Double,它们分别是在CustomIterator和CustomIterator初始化的时候确定了泛型的具体类型。
扩展泛型类型
当你扩展一个泛型类型的时候,你并不需要在扩展的定义中提供类型参数列表。原始类型定义中声明的类型参数列表在扩展中可以直接使用,并且这些来自原始类型中的参数名称会被用作原始定义中类型参数的引用。
下面我们扩展泛型类型CustomIterator,为其添加了一个名为firstElement的只读计算型属性,返回一个CustomIterator中数组的第一个元素:
extension CustomIterator {
var firstElement: Element? {
return elements.first
}
}
firstElement会返回一个Element类型的可选值。当elements为空的时候,firstElement会返回nil;当elements不为空的时候,firstElement会返回elements中的第一个元素。
注意,这个扩展并没有定义一个类型参数列表。相反的,CustomIterator类型已有的类型参数名称Element,被用在扩展中来表示计算型属性firstElement的可选类型。
计算型属性firstElement现在可以用来访问任意CustomIterator实例的第一个元素:
var cusIter = CustomIterator<String>(elements: ["hello", "你好", "hi"])
// 打印结果:hello
print(cusIter.firstElement ?? "")
C. 泛型约束
myReduce和CustomIterator都可以作用任何类型。不过,有的时候如果能将使用在泛型函数
和泛型类型
中的类型添加一个特定的类型约束
,将会是非常有用的。类型约束可以指定一个类型参数必须继承自指定类,或者符合一个特定的协议或协议组合,或者符合一些什么条件。
所以,泛型约束大致分为以下几种:
-
继承约束
,泛型类型必须是某个类的子类类型 -
协议约束
,泛型类型必须遵循某些协议 -
条件约束
,泛型类型必须满足某种条件
接下来我们来逐个说明:
协议约束
协议约束,工作中高频率使用的Dictionary的定义的泛型Key和Value中的Key就是有协议约束的:
// Dictionary 的定义
public struct Dictionary<Key, Value> where Key : Hashable {
// ...
}
泛型Key必须遵循Hashable协议
,所以字典的键的类型必须是可哈希(hashable)的。也就是说,必须有一种方法能够唯一地表示它。Dictionary 的键之所以要是可哈希的,是为了便于检查字典是否已经包含某个特定键的值。若没有这个要求,Dictionary将无法判断是否可以插入或者替换某个指定键的值,也不能查找到已经存储在字典中的指定键的值。
当你创建自定义泛型类型时,你可以定义你自己的类型约束,这些约束将提供更为强大的泛型编程能力。抽象概念,例如可哈希的,可比较的等等,而不是它们的显式类型。
函数的协议约束的语法
// 函数的协议约束
func 函数名<泛型: 协议, ...>(形参: 泛型, ...) -> 返回值 {
// 函数体
}
func 函数名<泛型, ...>(形参: 泛型, ...) -> 返回值 where 泛型: 协议, ... {
// 函数体
}
// 函数的协议约束简单例子
func f1<T: Equatable, U: Hashable>(p1: T, p2: U) -> U {
return p2
}
func f2<T, U>(p1: T, p2: U) -> U where T: Equatable, U: Hashable {
return p2
}
以上两种写法都是可以的,f1和f2表达的意思一样,T需要遵循Equatable协议,U需要遵循Hashable协议。
到这就可以解释初体验的例子中T: Equatable
是什么意思了,这是对泛型T的一个协议约束
,必须要遵循Equatable协议
,因为函数体内调用了contains方法,而contains要求数组中的元素必须遵循Equatable协议。
类或结构体的协议约束的语法
// 类或结构体的泛型约束
class / struct 类名/结构体名: 泛型:协议, ... {
// ...
}
class / struct 类名/结构体名: 泛型, ... where 泛型:协议, ... {
// ...
}
class A1<T: Equatable, U: Hashable> {
// ...
}
class B1<T, U> where T: Equatable, U: Hashable {
// ...
}
struct S1<T: Equatable, U: Hashable> {
// ...
}
class S2<T, U> where T: Equatable, U: Hashable {
// ...
}
我们来为泛型类型中的例子CustomIterator添加一个extension,然后在extension中添加一个方法,判断某个元素是否存在在CustomIterator的elements中:
extension CustomIterator {
func isExist(element: Element) -> Bool {
// 错误:Argument type 'Element' does not conform to expected type 'Equatable'
return elements.contains(element)
}
}
按正常思路,判断一个元素是否存在数组中,自然会想到调用contains函数即可,但很可惜,编译器是报错的,因为这里使用的是泛型,也就是任意类型,而contains函数要求数组内的元素必须遵循Equatable协议,而泛型的任意类型自然不能保证都遵循了Equatable协议。
所以正确的写法如下:
// 写法1.1
struct CustomIterator<Element: Equatable> {
// ...
}
// 写法1.2
struct CustomIterator<Element> where Element: Equatable {
// ...
}
// 写法2
extension CustomIterator where Element: Equatable {
func isExist(element: Element) -> Bool {
return elements.contains(element)
}
}
写法1.1和1.2的效果是一样的,我们可以对结构体或类中的泛型进行协议约束,但写法1和写法2是区别的:
- 写法1:要使用CustomIterator这个结构体中的
任意功能
,Element就必须要遵循Equatable协议 - 写法2:如果要使用CustomIterator
扩展中的isExist函数时
,Element才必须遵循Equatable协议
也就是说,将协议约束加在不同的地方,约束的范围是不一样的
- 加在类名或结构体名之后,要使用这个类或者协议,就必须遵循这个协议
- 加在类或结构体的extension之后,只有要使用这个extension中的函数时,才需要遵循这个协议,使用本体或其他没有加约束的extension中的函数或属性时,并不需要遵循这个协议
约束的范围就看具体的需求了~
继承约束
顾名思义,就是说泛型必须是某个类的子类。
语法与协议约束一样,只是把协议换成具体的父类的类名。
就拿说方言(中国方言)来当例子,来自中国不同地方的人会说当地特有的方言,用程序来体现就是这样的:
// 中国人
class Chinese {
func speak() {
print("中国人说普通话~")
}
}
// 湖南人 继承自 中国人
class HuNan: Chinese {
override func speak() {
print("湖南人说湖南话~")
}
}
// 广东人 继承自 中国人
class GuangDong: Chinese {
override func speak() {
print("广东人说广东话~")
}
}
// 定义泛型函数(中国人说话),接受一个泛型参数,自然要求该泛型类型必须继承Chinese
func chineseSpeak<T: Chinese>(_ chinese: T) {
chinese.speak()
}
// 打印结果:湖南人说湖南话~
// 广东人说广东话~
chineseSpeak(HuNan())
chineseSpeak(GuangDong())
通过代码可以看到,继承约束与协议约束非常类似,只是泛型的限制条件一个是具体的父类,一个是协议。
条件约束
在说条件约束
之前,我们得先说说另一个概念,那就是关联类型(associatedtype)
:
定义一个协议时,有的时候声明一个或多个关联类型作为协议定义的一部分将会非常有用。关联类型为协议中的某个类型提供了一个占位名(或者说别名),其代表的实际类型在协议被采纳时才会被指定。
这样的协议称之为泛型协议,可以通过associatedtype
关键字来指定关联类型
。
下面例子定义了一个MyProtocol协议,该协议定义了一个关联类型MyType:
protocol MyProtocol {
associatedtype MyType
mutating func append(_ item: MyType)
}
MyProtocol定义了一个append方法,添加一个新元素。MyProtocol协议需要指定任何通过append方法添加到容器中的元素和容器中的元素是相同类型。
为了达到这个目的,MyProtocol协议声明了一个关联类型MyType
,写作 associatedtype MyType
。这个协议无法定义MyType是什么类型
的别名,这个信息将留给遵循协议的具体类型来提供
。尽管如此,MyType别名提供了一种方式来引用 MyProtocol中元素的类型,并将之用于append方法,从而保证append方法能够正如预期地被执行。
下面是上文的CustomIterator简易版迭代器)遵循MyProtocol协议:
struct CustomIterator<Element>: MyProtocol {
var elements: [Element] = []
var num = 0
mutating func next() -> Element? {
if num == elements.count {
num = 0
return nil
}
num += 1
return elements[num - 1]
}
init(elements: [Element], num: Int = 0) {
self.elements = elements
self.num = num
}
// MARK: - MyProtocol
mutating func append(_ item: Element) {
elements.append(item)
}
}
由于Swift的类型推断,并不需要指定MyProtocol中MyType的具体类型
,CustomIterator只需通过append方法的item参数类型
,就可以推断出 MyType的具体类型
。
上述代码中append的实现,占位类型参数Element被用作append方法的item 参数。Swift可以据此推断出Element的类型即是MyType的类型。
说完了关联类型,接下来说条件约束。
通过where
子句要求一个关联类型遵从某个特定的协议
,以及某个特定的类型参数和关联类型必须类型相同。可以通过将where
关键字紧跟在函数体或者类型的大括号后面来定义where
子句,where
子句后跟一个或者多个针对关联类型的约束,以及一个或多个类型参数和关联类型间的相等关系。
下面的例子定义了一个名为isAllItemsMatch的泛型函数,用来检查两个自定义迭代器实例是否包含相同顺序的相同元素。如果所有的元素能够匹配,那么返回true,否则返回false。
func isAllItemsMatch<A1: MyProtocol, A2: MyProtocol>(_ ite1: A1, _ ite2: A2) -> Bool where A1.MyType == A2.MyType, A1.MyType: Equatable {
if ite1.count != ite2.count { return false }
for i in 0..<arr1.count {
if ite1[i] != ite2[i] {
return false
}
}
return true
}
// 以下为配套代码
protocol MyProtocol {
associatedtype MyType
mutating func append(_ item: MyType)
var count: Int { get }
subscript(i: Int) -> MyType { get }
}
struct CustomIterator<Element>: MyProtocol {
...
subscript(i: Int) -> Element {
return elements[i]
}
var count: Int {
return elements.count
}
}
这个函数接受ite1和ite2两个参数。参数ite1的类型为A1,参数ite2的类型为A2。A1和A2是容器的两个占位类型参数,函数被调用时才能确定它们的具体类型。
这个函数的类型参数列表还定义了对两个类型参数的要求:
- A1必须符合MyProtocol协议(写作A1: MyProtocol)。
- A2必须符合MyProtocol协议(写作A2: MyProtocol)。
- A1的MyType必须和A2的MyType类型相同(写作A1.MyType == A2.MyType)。
- A1的MyType必须符合Equatable协议(写作A1.MyType: Equatable),同时也就意味着A2.MyType也必须遵循Equatable协议。
下面演示了isAllItemsMatch函数的使用:
let cusIte1 = CustomIterator(elements: [1, 2, 3, 4])
let cusIte2 = CustomIterator(elements: [1, 2, 3, 4])
let cusIte3 = CustomIterator(elements: [1, 2, 3])
let cusIte4 = CustomIterator(elements: [1.1, 2.2, 3.3])
let cusIte5 = CustomIterator(elements: ["a", "b", "c"])
// true
print(isAllItemsMatch(cusIte1, cusIte2))
// false
print(isAllItemsMatch(cusIte1, cusIte3))
// 报错,类型不符
//print(isAllItemsMatch(cusIte1, cusIte4))
// 报错,类型不符
//print(isAllItemsMatch(cusIte1, cusIte5))
// 报错,类型不符
//print(isAllItemsMatch(cusIte4, cusIte5))
还有一种用法,就是在扩展泛型协议的时候,对关联类型加上约束:
// 两个测试协议
protocol TestProtoclol1 {}
protocol TestProtoclol2 {}
// 遵循两个测试协议的 测试类
class TestClass1: TestProtoclol1 {}
class TestClass2: TestProtoclol2 {}
// 泛型协议
protocol TestContainer {
associatedtype ItemType
}
// 扩展泛型协议,并使泛型 ItemType 遵循 TestProtoclol1 协议
extension TestContainer where ItemType: TestProtoclol1 {
var id: Int {
return 0
}
}
// 扩展泛型协议,并使泛型 ItemType 遵循 TestProtoclol2 协议
extension TestContainer where ItemType: TestProtoclol2 {
var id: Int {
return 1
}
}
// 遵循泛型协议的测试类1
class TestTest1: TestContainer {
typealias ItemType = TestClass1
// 换为 TestClass2 ,测试一下,看看结果
// typealias ItemType = TestClass2
}
// 遵循泛型协议的测试类2
class TestTest2: TestContainer {
typealias ItemType = Int
}
let test1 = TestTest1()
// 打印:0
print(test1.id)
let test2 = TestTest2()
// 报错,调用不到,因为 Int 并没有遵循 TestProtoclol1 或 TestProtoclol2 协议
//print(test2.id)
上述例子,简单地解释就是,泛型协议扩展中的属性或函数,如果扩展对泛型做了其他的约束,想要调用到扩展中的属性或函数,泛型就必须满足这个约束。
如例子中,test1能获取到id属性,是因为指定了ItemType为TestClass1,TestClass1是遵循了TestProtoclol1
协议的,所以获取到的id的值就是TestContainer第一个扩展中的id值0,test2无法获取id值,是因为指定了ItemType为Int类型,而Int并未遵循TestProtoclol1
或TestProtoclol2
协议。
看明白了这个例子,就可以思考一下,我们使用RxSwift时,调用rx的方法,例如:button.rx.tap…,总会有.rx这种签名的标识,RxSwift是怎么做到呢,原理就是关联类型和对泛型的扩展。
我们来尝试实现一下,我想调用自己对String和URL的扩展方法带上.shanzhu的标识:
//定义泛型结构体,里面初始化注入一个泛型抽象类型
public struct Shanzhu<Base> {
public let base: Base
public init(_ base: Base) {
self.base = base
}
}
public protocol ShanzhuCompatible {
// 关联类型
associatedtype CompatibleType
// 实例 使用关联类型,替换泛型
var shanzhu: Shanzhu<CompatibleType> { get set }
// 静态
static var shanzhu: Shanzhu<CompatibleType>.Type { get set }
}
public extension ShanzhuCompatible {
// Self是实现此协议的对象 实例变量对应调用实例方法
var shanzhu: Shanzhu<Self> {
get {
return Shanzhu(self)
}
set {}
}
static var shanzhu: Shanzhu<Self>.Type {
get {
return Shanzhu<Self>.self
}
set {}
}
}
extension String: ShanzhuCompatible {}
extension URL: ShanzhuCompatible {}
// String 的扩展 对泛型Base进行约束限定
extension Shanzhu where Base == String {
func exTest() {
print("String exTest suc~~ value:\(self)")
}
}
// URL 的扩展
extension Shanzhu where Base == URL {
func exTest() {
print("URL exTest suc~~ value:\(self)")
}
}
// 打印结果:String exTest suc~~ value:Shanzhu<String>(base: "a")
"a".shanzhu.exTest()
// 打印结果:URL exTest suc~~ value:Shanzhu<URL>(base: http://www.baidu.com)
URL(string: "http://www.baidu.com")?.shanzhu.exTest()
// 不带 .shanzhu 的标识,调用不到,为什么呢?
//"a".exTest()
//URL(string: "a")?.exTest()
这段代码的意思:
- 定义了一个名为Shanzhu的结构体,且定义了泛型Base,结构体有一个属性base为Base类型,并提供一个初始化方法init;
- 定义了一个泛型协议ShanzhuCompatible,关联类型为CompatibleType,且有都名为shanzhu的一个实例变量和一个静态变量,均为可读可写,类型都是Shanzhu结构体;
- 扩展了ShanzhuCompatible泛型协议,并实现了名为shanzhu的一个实例变量和一个静态变量,get方法返回对应的实例变量和静态变量;
String和URL遵循ShanzhuCompatible泛型协议; - 扩展Shanzhu结构体,分别约束泛型Base为String和URL,都添加一个exTest方法。
以String为例,“a”.shanzhu.exTest()能调用成功:
- "a"是String的实例,由于String遵循了ShanzhuCompatible协议,所以就有一个shanzhu的实例属性;
- 当"a".shanzhu时,ShanzhuCompatible的关联类型CompatibleType就会推导出是String类型,同时也就会推导出shanzhu实例属性是Shanzhu类型;
- 由于我们扩展了Shanzhu结构体,且约束了Base为String类型,其中有一个exTest方法,那么Shanzhu类型恰好是满足条件的,所以能调用到exTest方法。
URL也是一个道理,就不详细说明了。
分析完了,有没有一种代理的感觉,没错,就是代理,我们并不是直接对String或URL做的扩展吧,而是对Shanzhu结构体进行扩展并约束泛型Base为String类型,为了能调用到扩展中的方法或属性,我们又让String遵循了ShanzhuCompatible协议,这个协议中有Shanzhu结构体的实例变量和静态变量,而协议的关联类型就是Shanzhu结构体的泛型类型,当.shanzhu时,就能调用到Shanzhu结构体的扩展中与当前具体类型一致的方法或属性。
说到这,不带.shanzhu的标识,调用不到的原因就很清晰了。
D. 泛型下标
下标能够是泛型的,他们能够包含泛型where子句。你可以把占位符类型的名称写在 subscript后面的尖括号里,在下标代码体开始的标志的花括号之前写下泛型where子句。
extension CustomIterator {
subscript<Indices: Sequence>(indices: Indices) -> [Element]
where Indices.Iterator.Element == Int {
var result = [Element]()
for index in indices {
result.append(self[index])
}
return result
}
}
这个CustomIterator的扩展添加了一个下标方法,接收一个索引的集合,返回每一个索引所在的值的数组。这个泛型下标的约束如下:
这个CustomIterator的扩展添加了一个下标:下标是一个序列的索引,返回的则是索引所在的项目的值所构成的数组。这个泛型下标的约束如下:
- 在尖括号中的泛型参数Indices,必须是符合标准库中的Sequence协议的类型。
- 下标使用的单一的参数,indices,必须是Indices的实例。
- 泛型where子句要求Sequence(Indices)的迭代器,其所有的元素都是 Int类型。这样就能确保在序列(Sequence)中的索引和容器(Container)里面的索引类型是一致的。
综合一下,这些约束意味着,传入到indices下标,是一个整型的序列。
我们来看一下使用效果:
let subIte = CustomIterator(elements: [1, 2, 3, 4])
// 打印结果:[1, 3]
print(subIte[[0, 2]])
使用泛型下标后,使用下标访问数组元素时,就不仅限于访问某一个,而是可以访问一组了。
到这,本篇文章要说的就是这些了,泛型远远不止这些内容,本篇文章算是一个抛砖引玉吧,如有说的不准确的地方,欢迎指正~