swift 学习- 26 -- 泛型

// 泛型 代码能够让你根据自定义的需求,编写出适用于任意类型, 灵活可重用的函数以及类型, 它能让你避免代码的重复, 用一种清晰和抽象的方式来表达代码的意图

// 泛型是 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 的值,

var someInt = 3

var anotherInt = 107

swap(&someInt, &anotherInt)

print("someInt is now \(someInt), and anotherInt is now \(anotherInt)")

// 诚然,swapTwoInts(_:_:) 函数挺有用, 但是他只能交换 Int 值, 如果你想要交换两个 String 值 或者 Double 值, 就不得不写更多的函数

// 在实际应用中, 通常需要一个更实用更灵活的函数来交换两个任意类型的值, 泛型代码帮你解决了这两种问题,

// 泛型函数

// 泛型函数可以适用于任何类型, 下面的 swapTwoValue(_:_:) 函数是上面的泛型版本

func swapTwoValues<T>(_ a: inout T, _ b: inout T){

let temporaryA = a

a = b

b = temporaryA

}

// 这个函数的泛型版本使用了占位类型名 (在这里用字母 T 表示) 来代替实际类型名 (例如 Int, Sreing , Double) 占位类型名没有指明 T 必须是什么类型, 单是他指明了 a 和 b 必须是同一类型 T ,无论 T 代表什么类型, 只有 swapTwoValues(_:_:) 函数在调用时,才能根据传入的实际类型决定 T 所代表的类型

// 另外一个不同之处在于这个泛型函数名 (swapTwoValues(_:_:)) 后面跟着的占位类型名 (T), 并用尖括号括起来 ( <T> ), 这个尖括号告诉 Swift 你那个 (T) 是 swapTwoValues(_:_:) 函数定义内的一个占位类型名, 因此 Swift 不会去检查名为 (T) 的实际类型

// swapTwoValues(_:_:) 函数现在可以像 swapTwoInts(_:_:) 那样调用, 不同的是它能接受两个任意类型的值, 条件是这两个值有相同的类型, swapTwoValues(_:_:) 函数被调用时, T 所代表的类型都会由传入的值的类型推断出来

// 注意: 上面定义的 swapTwoValues(_:_:) 函数 是受 swap(_:_:) 函数启发而发现的, 后者存在于 Swift 标准库,你可以在你的应用程序中使用它

// 类型参数

// 在上面的 swapTwoValues(_:_:) 例子中,占位类型 T 是 类型参数的一个例子

// 类型参数 指定并命名一个占位类型, 并且紧随在函数名后面, 使用一对尖括号括起来 (例如 <T> )

// 一旦一个类型参数被指定, 你可以用它来定义一个函数的参数类型, (例如 swapTwoValues(_:_:) 函数中的参数 a 和 b ), 或者作为函数的返回类型,还可以用作函数主体中的注释类型, 在这些情况下, 类型参数会在函数调用时被实际类型所替代

// 你可以提供多个类型参数, 将它们都写在尖括号中, 用逗号隔开

// 命名类型参数

// 在大多数情况下,类型参数具有一个描述性的名字 例如 Dictionary<key,value> 中的 key 和 value, 以及 Array<Element> 中的 Element, 这可以告诉阅读代码的人这些类型参数 和 泛型函数之间的关系, 然而, 当它们之间没有有意义的关系时, 通常使用单个字母命名, 例如 T, U ,V 正如上面演示的 swapTwpValues(_:_:) 函数中的 T 一样

// 注意: 请始终使用大写字母开头的驼峰命名法, 来为类型参数命名, 以表明它们是占位类型, 而不是一个值

// 泛型类型

// 除了泛型函数, Swift 还允许你定义泛型类型, 这些自定义类, 结构体, 和枚举可以适用于任何类型, 类似于 Array 和 Dictionary

struct Stack<Element>{

var items = [Element]()

mutating func push(_ item: Element){

items.append(item)

}

mutating func pop() -> Element{

return items.removeLast()

}

}

// 扩展一个泛型类型

// 当你扩展一个泛型类型的时候, 你并需要在扩展的定义中提供类型的参数列表, 原始类型定义中声明的类型参数列表在扩展中可以直接使用, 并且这些来自原始类型中的参数名称会被用作原始定义中类型参数的引用

// 下面的例子扩展了泛型类型的 Stack

extension Stack{

var topItem: Element?{

return items.isEmpty ? nil: items[items.count - 1]

}

}

// 类型约束

// swapTwoValues(_:_:) 函数 和 Stack 类型可以作用域任何类型, 不过, 有时候如果能将 使用在泛型函数 和 泛型类型中的类型 添加一个特定的类型约束, 将会是非常有用的, 类型约束可以指定一个类型参数必须继承自指定类型, 或者符合一个特定的协议或协议组合

// 当你创建自定义泛型类型时, 你可以定义你自己的类型约束, 这些约束将提供更为强大的泛型编程能力, 抽象概念,

// 类型约束语法

// 你可以在一个类型参数名后面放置一个类名或者协议名, 并用冒号分隔, 类定义类型约束, 他们将成为类型参数列表的一部分, 对泛型函数添加类型约束的基本语法如下:

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

//

// }

// 上面这个函数有两个类型参数, 第一个类型参数 T, 有一个要求 T 必须是 SomeClass 子类的类型约束, 第二个类型参数 U, 有一个要求 U 必须缝合 SomeProtocol 协议的类型约束

// 类型约束实践

func findIndex(ofString valueToFind: String, in array: [String]) -> Int?{

for (index, value) in array.enumerated() {

if value == valueToFind {

return index

}

}

return nil

}

// findIndex(ofString:in:) 函数可以用于查找字符串数组中的某个字符串

let strings = ["cat","dog","llama","parakeet","terrapin"]

if let foundIndex = findIndex(ofString: "llama", in: strings) {

print("The index of llama is \(foundIndex)")

}

// 下面展示了 findIndex(ofString:in:) 函数的泛型版本, 不过, 你可以用占位类型 T 替换 string 类型来写出具有相同吧功能的泛型函数 findIndex(_:_:).

// 请注意这个函数返回值的类型仍然是 Int?, 这是因为函数返回的是一个 可选的 索引数, 而不是从数组中得到的一个可选值, 需要提醒的是,这个函数无法通过编译

//func findIndexF<T>(of valueToFind: T, in array:[T]) -> Int?{

// for (index,value) in array.enumerated() {

// if value == valueToFind {

// return index

// }

// }

// return nil

//}

// 上面所写的函数无法通过编译, 问题出在相等性检查上, 即 if value == valueToFind, 不是所有的 Swift 了新鲜感都可以用等式符合 (==) 进行比较, 比如说, 如果你创建一个自定义的类 或结构体来表示一个复杂的数据模型, 那么 Swift 无法猜到对于这个类或结构体而言 '相等' 意味着什么, 正因为如此, 这部分代码无法保证适用于每个可能的类型 T , 所以会出现错误

// 不过这些并不会让我们无法下手, Swift 标准库中定义了一个 Equatable 协议, 该协议要求任何遵循该协议的类型必须实现 等式符号(==) 及不等式符号 (!=). 从而 能对该类型的任意两个值进行比较, 所有的 Swift 标准类型自动支持 Equatable 协议

func findIndexF<T: Equatable>(of valueToFind: T, in array:[T]) -> Int?{

for (index, value) in array.enumerated() {

if value == valueToFind {

return index

}

}

return nil

}

// 关联类型

// 定义一个协议时, 有的时候声明一个 或 多个关联类型作为协议定义 的一部分将会非常有用, 关联类型 为协议何中的某个类型提供了一个占位名(或者说别名), 其代表的实际类型在协议被采纳时才会被指定, 你可以通过 associatedtype 关键字来指定 关联类型

// 关联类型实践

// 下面的例子定义了一个 Container 协议, 该协议定义了一个关联类型 ItemType:

protocol Container{

associatedtype ItemType

mutating func append(_ item: ItemType)

var count: Int { get }

subscript(i: Int) -> ItemType { get }

}

// 为了达到这个目的,Container 协议声明了一个关联类型 ItemType,写作 associatedtype ItemType。这个协议无法定义 ItemType 是什么类型的别名,这个信息将留给遵从协议的类型来提供。尽管如此,ItemType 别名提供了一种方式来引用 Container 中元素的类型,并将之用于 append(_:) 方法和下标,从而保证任何 Container 的行为都能够正如预期地被执行。

struct IntStack: Container {

// IntStack 的原始实现部分

var items = [Int]()

mutating func push(_ item: Int) {

items.append(item)

}

mutating func pop() -> Int {

return items.removeLast()

}

// Container 协议的实现部分

typealias ItemType = Int

mutating func append(_ item: Int) {

self.push(item)

}

var count: Int {

return items.count

}

subscript(i: Int) -> Int {

return items[i]

}

}

// IntStack 结构体实现了 Container 协议的三个要求,其原有功能也不会和这些要求相冲突。

// 此外,IntStack 在实现 Container 的要求时,指定 ItemType 为 Int 类型,即 typealias ItemType = Int,从而将 Container 协议中抽象的 ItemType 类型转换为具体的 Int 类型。

// 你也可以让泛型 Stack 结构体遵从 Container 协议:

struct Stack2<Element>: Container {

// Stack<Element> 的原始实现部分

var items = [Element]()

mutating func push(_ item: Element) {

items.append(item)

}

mutating func pop() -> Element {

return items.removeLast()

}

// Container 协议的实现部分

mutating func append(_ item: Element) {

self.push(item)

}

var count: Int {

return items.count

}

subscript(i: Int) -> Element {

return items[i]

}

}

// 这一次,占位类型参数 Element 被用作 append(_:) 方法的 item 参数和下标的返回类型。Swift 可以据此推断出 Element 的类型即是 ItemType 的类型。

// 泛型 Where 语句

// 类型约束 让你能够为泛型函数 或泛型类型的类型参数定义一些强制要求