Swift语法总结2.23(泛型)

Swift语法总结2.23(泛型)_第1张图片
c.c..jpg

本文参考自苹果官方文档 Generics

泛型(Generics)

泛型代码允许你定义适用于任何类型的,符合你设置的要求的,灵活且可重用的 函数类型
泛型代码让你避免重复的代码,并且用一种清晰和抽象的方式表达其意图

泛型是 Swift 强大特征中的其中一个
Swift标准库中的很多内容都是通过泛型代码构建出来的。
事实上,泛型的使用贯穿了整本语言手册(The Swift Programming Language (Swift 3)),只是你没有发现而已。
例如,Swift 的数组和字典类型都是泛型集合
你可以创建一个Int数组,也可创建一个String数组,或者甚至于可以是任何其他Swift的类型数据数组。
同样的,你也可以创建元素是任何指定类型的字典(dictionary).

泛型所解决的问题

这里是一个标准的,非泛型函数swapTwoInts,用来交换两个Int值:



func swapTwoInts(inout a: Int, inout _ b: Int) { let temporaryA = a a = b b = temporaryA }

这个函数使用输入输出参数(inout)来交换a和b的值,请参考输入输出参数
swapTwoInts函数可以交换b的原始值到a,也可以交换a的原始值到b,你可以调用这个函数交换两个Int变量值:



var someInt = 3 var anotherInt = 107 swapTwoInts(&someInt, &anotherInt) print("someInt is now \(someInt), and anotherInt is now \(anotherInt)") //someInt is now 107, and anotherInt is now 3

swapTwoInts函数是非常有用的,但是它只能交换Int值,如果想要交换两个String或者Double,就不得不写更多的函数,如swapTwoStrings和swapTwoDoubles,如下所示:



func swapTwoStrings(inout a: String, inout _ b: String) { let temporaryA = a a = b b = temporaryA } func swapTwoDoubles(inout a: Double, inout _ b: Double) { let temporaryA = a a = b b = temporaryA }

你可能注意到swapTwoInts,swapTwoStrings和swapTwoDoubles函数功能都是相同的,唯一不同之处就是参数类型不同,分别是Int,String和Double。

此时就需要某个更有用更灵活的单个函数用来交换两个任何类型的值的,很幸运的是,泛型代码帮你解决了这种问题

泛型函数

泛型函数可以应用于任何类型
这里是一个上面swapTwoInts函数的泛型版本



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

swapTwoValues和swapTwoInts函数的主体是一样的,只在第一行有一点不同:



func swapTwoInts(inout a: Int, inout _ b: Int) func swapTwoValues(inout a: T, inout _ b: T)

这个函数的泛型版本使用了占位类型(通常用字母T来表示)来代替实际类型名称(如Int,String或Double)。
占位类型没有表明T必须是什么类型,但是它表明了a和b必须是同一类型T,而不管T表示什么类型

T所代表的实际类型只有swapTwoValues函数在每次被调用时才能决定

另外一个不同之处在于这个泛型函数名后的占位类型T是用尖括号<>括起来的。
这个<>表示T是swapTwoValues函数中的一个占位类型
因为T是一个占位类型,Swift不会去查找命名为T的实际类型。

swapTwoValues函数也可以被调用。
每次swapTwoValues被调用,T所代表的类型都会传给函数。

在下面的两个例子中,T分别代表Int和String:



`
var someInt = 3
var anotherInt = 107
swapTwoValues(&someInt, &anotherInt)
print(someInt,anotherInt)

var someString = "hello"
var anotherString = "world"
swapTwoValues(&someString, &anotherString)
print(someString,anotherString)

//107 3
//world hello
`

注意
上面定义的函数swapTwoValues是受swap函数的启发。
swap函数存在于Swift标准库.如果你在自己代码中需要类似swapTwoValues函数的功能,你不用自己来实现因为可以使用已存在的swap函数

类型参数

在上面的swapTwoValues例子中,占位类型T是类型参数的一个示例。
类型参数:
指定并命名了一个占位类型
在函数名之后
在一对<>中,例如

一旦类型参数被指定(指的是:)
你可以使用该类型参数:
1.作为函数的参数类型
2.作为函数返回类型
3.作为函数主体中的类型标注
以swapTwoValues函数为例来体会:

func swapTwoValues //指定类型参数为占位类型T (inout a: T, inout _ b: T)//定义函数的参数类型为类型参数T { let temporaryA = a a = b b = temporaryA }

类型参数在函数被调用时都会被实际类型所替换
在之前的swapTwoValues例子中,当函数第一次被调用时,T被Int替换,第二次调用时,T被String替换。(再写一遍之前的例子)



`
var someInt = 3
var anotherInt = 107
swapTwoValues(&someInt, &anotherInt)
print(someInt,anotherInt)

var someString = "hello"
var anotherString = "world"
swapTwoValues(&someString, &anotherString)
print(someString,anotherString)

//107 3
//world hello
`

你可以提供多个类型参数通过编写多个类型参数并在尖括号中用逗号分开的方式
eg.

命名类型参数

在大多数的情况下,类型阐述具有描述性的名称,例如Dictionary中Key,Value和Array中的Element,来告诉读者泛型类型/泛型函数与其中的类型参数的关系.
然而,当用当二者之间的关系没有具体意义时,可以用传统的方法来命名类型参数,使用单个字母如T,U,V.

泛型函数泛型类型 需要指定一个占位类型(如上面的泛型函数,或一个存储单一类型的泛型集,如数组)
通常用一单个字母T来命名类型参数

注意
最好用大写字母开头的驼峰式命名法(例如T和MyTypeParameter)来给类型参数命名,以表明它们是类型的占位符, 而非类型的值

泛型类型

除了定义泛型函数,Swift允许你自定义泛型类型.
这些自定义的类,结构体和枚举 如同Array和Dictionary ,可以与任何类型一起工作.

这部分向你展示如何写一个泛型集合类型Stack。
Stack是一系列有序的值的集合,和Array类似,但比Array有更多限制:
数组可以允许其里面任何位置的插入/删除操作
栈只允许在集合的末端添加新的项(如同push一个新值进栈)。同样的栈也只能从末端移除项(如同pop一个值出栈)。

注意
栈的概念已经被UINavigationController类使用来模拟视图控制器的导航结构.
通过调用UINavigationController的pushViewController方法来为导航栈添加新的视图控制器;通过popViewController方法来从导航栈中移除试图控制器.
栈是一种非常有用的集合模型当你需要严格的后进先出的方式来管理集合时.

下图展示了一个栈的压栈(push)/出栈(pop)的行为:

Swift语法总结2.23(泛型)_第2张图片
栈的压栈(push):出栈(pop).png
  1. 现在有三个值在栈中
  2. 第四个值“pushed”到栈的顶部
  3. 现在有四个值在栈中,最近压入的那个在顶部
  4. 栈中最顶部的那个项被移除,或称之为“popped”
  5. 移除掉一个值后,现在栈又重新只有三个值。

The Swift Programming Language 中文版

这里展示了如何写一个非泛型版本的栈,Int值型的栈



`
struct IntStack
{
var items = Int

mutating func push(item: Int)
{
items.append(item)
}
mutating func pop() -> Int
{
    return items.removeLast()
}

}
`

这个结构体在栈中使用一个Array类型的items变量来存储值。
Stack提供两个方法:push和pop,向栈中压进一个值和从栈中移除一个值。
这些方法标记为mutating(可变的),因为它们需要修改(或转换)结构体IntStack中的属性items。
上面所展现的IntStack类型只能用于Int值,如果定义一个泛型的Stack(可以处理任何类型值的栈)将会是是非常有用的。

这里是一个IntStack的泛型版本:


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

泛型版本的Stack和非泛型版本的IntStack基本相同
只是泛型版本Stack的类型参数Element代替了实际Int类型.
类型参数Element包含在一对尖括号里,紧随在结构体名字后面

Element为"某种类型Element"定义了一个占位符名称以便在之后能被使用.
这种之后会被用到的类型在结构体的定义的任何地方来表示Element类型

在此示例中, Element在如下三个地方被用作占位符:
1.创建一个名为items的属性,使用空的类型为[Element]的数组对其进行初始化
var items = [Element]()
2.定义参数名为item的push(_:) 方法,参数item必须是Element类型
mutating func push(item: Element) { items.append(item) }
3.定义pop方法,该犯法返回值是一个Element类型的值。
mutating func pop() -> Element { return items.removeLast() }
由于Stack是泛型类型,所以在Swift中其可以用来创建存储任何有效的类型的值的栈,这种方式如同Array和Dictionary可以存储任何类型的元素.

可以通过泛型类型名 + <实际类型>的方式来创建并初始化一个泛型实例。
eg:

var stackOfStrings = Stack() //在尖括号里写出栈中需要存储的数据类型

此时栈stackOfStrings是空的

向stackOfStrings压入4个字符串

stackOfStrings.push("uno") stackOfStrings.push("dos") stackOfStrings.push("tres") stackOfStrings.push("cuatro")

Swift语法总结2.23(泛型)_第3张图片
push 四个值进栈.png

从stackOfStrings中移除"cuatro"

let fromTheTop = stackOfStrings.pop() //从栈中移除"cuatro",并将方法的返回值"cuatro"存储在fromTheTop中

Swift语法总结2.23(泛型)_第4张图片
pop一个值出栈.png

扩展一个泛型类型

当你扩展一个泛型类型的时候,你不用在扩展的定义中提供类型参数的列表。
因为原始类型定义中声明的类型参数的列表在扩展体里是可以使用的

下面的例子扩展了泛型Stack类型,为其添加了一个名为topItem的只读计算属性,它将会返回当前栈顶端的元素而不会将其从栈中移除。



extension Stack { var topItem: Element? { return items.isEmpty ? nil : items[items.count - 1] } }

topItem属性会返回一个Element类型的可选值。
当栈为空的时候,topItem将会返回nil
当栈不为空的时候,topItem会返回items数组中的最后一个元素

注意
这里的扩展并没有定义一个类型参数列表
而Stack类型已有的类型参数名称(Element)被用在扩展中topItem计算属性的可选类型。

topItem计算属性现在可以被用来返回任意 Stack 实例的顶端元素而无需移除它:

if let topItem = stackOfStrings.topItem { print("The top item on the stack is \(topItem).") } //The top item on the stack is tres.

类型约束

泛型函数swapTwoValues和泛型类型Stack可以与类型一起工作
不过,有的时候 对 与泛型函数和泛型类型 一起工作的 类型 进行 类型约束 很有用.

类型约束 指定了 类型参数 必须 继承自一个指定的类 或者 遵循一个特定的协议或一个合成的协议

例如,Swift的Dictionary类型对类型参数Key的类型做了些限制。
在Dictionary的描述中, Dictionary的类型参数Key必须是可哈希的,也就是说,必须有一种方法可以使其被唯一的表示
Dictionary之所以其类型参数Key是可哈希是为了以便于其检查否已经包含某个特定键的值。
如无此需求,Dictionary既无法表述是否插入或者替换了某个特定Key对应的Value,也无法查找到已经存储在Dictionary里面的给定Key对应的Value。
这个需求通过在Dictionary的Key上强制加上一个类型约束来实现,指定Dictionary的Key的类型必须遵循Hashable协议(Swift标准库中定义的一个特定协议)
所有的Swift基本类型(如String,Int,Double和Bool)默认都是可哈希的

当你自定义泛型类型时,你可以自定义类型约束,并且这些约束要提供泛型编程的大部分的能力。

类型约束语法

你可以设置类型约束,通过 将一个类或协议放在类型参数之后 ,通过 冒号 分隔,来 作为类型参数列表的一部分

这种作用于泛型函数/泛型类型类型约束的基础语法如下所示(以泛型类型为例):



`
//注:
////类型约束
//(someT: T, someU: U)//泛型函数的参数的类型分别是T,U
func someFunction
(someT: T, someU: U)
{
// 这里是函数主体
}

`

上面示例有两个类型参数。
第一个类型参数T,有一个要求T是SomeClass子类的类型约束
第二个类型参数U,有一个要求U遵循SomeProtocol协议的类型约束。

类型约束实例

这里有个名为findStringIndex的非泛型函数,该函数功能是查找一给定String在数组中的索引值。若查找到匹配的字符串, findStringIndex函数返回该字符串在数组中的索引值,反之则返回nil

func findStringIndex(array: [String], _ valueToFind: String) -> Int? { for (index, value) in array.enumerate() { if value == valueToFind { return index } } return nil }

findStringIndex函数可以作用于查找一字符串数组中的某个字符串:

let strings = ["cat", "dog", "llama", "parakeet", "terrapin"] var foundIndex = findStringIndex(strings, "llama") print("The index of llama is \(foundIndex)") foundIndex = findStringIndex(strings, "不存在") print("The index of llama is \(foundIndex)") //The index of llama is Optional(2) //The index of llama is nil

如果只是能对字符串查找在数组中的的索引,用处不是很大
不过,你可以写出相同功能的泛型函数findIndex,用类型T替换String类型。
这里展示了findStringIndex的泛型版本findIndex。

注意:这个函数返回值类型仍为Int?因为函数返回的是一个可选的索引数,而不是从数组中得到的一个可选值。
注意:这个函数被不会编译,原因在例子后面会说明:

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

上面所写的函数不会被编译。问题出现在等式的检查上, “if value == valueToFind” 。
不是所有的Swift中的类型都可以用等式符(==)进行比较
如果你自定义了一个类或结构体来表示一个复杂的数据模型,Swift是猜测不出 "=="对于你自定义的类或结构体所表示的含义 .
尝试编译这个函数时Xcode会报错:
//Binary operator '==' cannot be applied to two 'T' operands
//二进制运算符'=='不能应用于两个'T'类型的操作数

但是,Swift标准库中定义了一个Equatable协议
Equatable协议要求任何遵循Equatable协议的类型需要实现(==)和(!=)操作符来对任意两个该类型的实例进行比较

Swift所有的标准的类型都自动支持Equatable协议

在下面的例子中,任何遵守Equatable协议的类型都可以在findIndex函数中安全使用,因为其保证支持(==)和(!=)操作符。
为了说明这个事实,当你定义一个函数时,你可以将类型约束Equatable作为类型参数定义的一部分:

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

函数findIndex的类型参数设置为T: Equatable ,也就意味着“类型T遵循Equatable协议”。
findIndex函数现在则可以成功的编译过,并且作用于任何遵循Equatable的类型,如Double或String :

let doubleIndex = findIndex([3.14159, 0.1, 0.25], 9.3) print(doubleIndex) let stringIndex = findIndex(["Mike", "Malcolm", "Andrea"], "Andrea") print(stringIndex) //nil //Optional(2)

关联类型(Associated Types)

有时在协议的定义中
声明一或多个关联类型作为协议定义的一部分非常有用
关联类型将一个类型命名为某占位类型,使该类型能够作为协议的一部分
关联类型代表的实际类型在协议被实现前是不需要指定的。
使用 associatedtype + 占位类型名指定一个关联类型

(个人理解:关联类型就是占位类型,但是关联类型通常和协议一起使用)

关联类型实例

这里是一个Container协议的例子,定义了一个ItemType关联类型:

`
protocol Container
{
//使用associatedtype声明关联类型ItemType
associatedtype ItemType

//功能1:向容器添加新元素
mutating func append(item: ItemType)

//功能2:容器中元素个数的属性
var count: Int { get }

//功能3:下标
subscript(i: Int) -> ItemType { get }

}
`

Container协议定义了任何容器必须支持的3个功能:
1.必须可以通过append方法添加新元素到容器里
2.必须可以通过使用count属性获取容器里元素的数量,并返回一个Int值
3.必须可以通过容器的Int类型的下标能够检测到每一个元素

这个协议没有指定容器里的元素是如何存储的,没有指定元素属于何种类型
这个协议只指定了3个任何遵循Container协议的类型所必须支持的功能。
一个遵循Container协议的类型在满足这三个条件的情况下*也可以提供其他额外的功能

任何遵循Container协议的类型
1.必须指定存储在其里面的值的类型
mutating func append(item: ItemType)
2必须保证只有正确类型的元素可以加进容器里
var count: Int { get }
3.必须明确可以通过其下标返回元素类型。
subscript(i: Int) -> ItemType { get }

为了定义上述3个规定,Container协议需要 通过某种方式(也就是声明一个关联类型) 表示容器里存储的元素的类型,而不需要知道特定的容器的类型
Container协议 需要表明 append方法的参数的类型/下标返回值的类型/容器中存储的元素的类型 是相同的

因此 Container协议 声明了 一个 名为ItemType的关联类型,写作associatedtype ItemType

虽然 Container协议 不会定义ItemType是什么类型,因为 ItemType代表的实际类型由遵循Container协议的类型来提供

但是,ItemType这个别名提供了一种方法来:
表示Container协议中的元素的类型
定义一种类型用于append方法和subscript
以确保任意一个Container的预期行为(Container协议中的3个规定)能够执行.

这里是之前的非泛型版本的IntStack类型的定义,现在遵循Container协议:

`
struct IntStack: Container//声明遵循Container协议
{
//IntStack的原始实现
var items = Int
mutating func push(item: Int)
{
items.append(item)
}
mutating func pop() -> Int
{
return items.removeLast()
}

//实现Container协议的规定
//表示ItemType是Int的别名
//由于Swift类型推断功能的存在,这一行不写也可以推断出ItemType的实际类型是Int
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这个定义把抽象的ItemType类型转换为具体的Int类型。
由于Swift类型推断功能,你不用在IntStack的定义中将占位类型ItemType声明为实际的Int类型。
由于IntStack类型实现了Container协议的所有要求,Swift可以推断出占位类型ItemType的实际类型来使用

事实上,如果上面的代码中你删除了typealias ItemType = Int这一行,编译时仍然可以成功,因为ItemType是哪一种类型一目了然。
eg.

`
var intStack = IntStack()
intStack.push(111)
intStack.push(222)
intStack.push(333)
intStack.push(444)
print("intStack.count->(intStack.count)")

let poped = intStack.pop()
print("poped->(poped)")

for index in 0.. {
print("(index)->(intStack[index])")
}

//intStack.count->4
//poped->444
//0->111
//1->222
//2->333
`

你也可以定义遵循Container协议的泛型类型Stack:

struct Stack: Container { //Stack的原始实现 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被用作Container协议中规定的append方法中item参数的类型和下标的返回类型。
Swift因此可以推断出Element类型就是这个特定容器的ItemType类型

在扩展中使已存在的类型遵循协议

在扩展中使已存在的类型遵循协议 中有描述通过扩展使一个存在的类型遵循一个协议(该协议中可能有关联类型)。
Swift的Array已经提供append方法,count属性下标来查找元素
这三个功能都实现了Container协议的要求。
也就意味着你可以通过扩展Array使其遵循Container协议,只要通过简单声明Array遵循该协议。
通过扩展补充声明遵循协议中有描述可以通过一个空扩展来实现此目的:
eg.

extension Array: Container {}

如同上面的泛型类型Stack一样,Array的append方法和下标保证Swift可以推断出Container协议中的ItemType类型所指代的实际类型。
定义了这个扩展后,你可以将任何Array当作Container来使用。

Where语句

类型约束 让你能够为 与泛型函数/泛型类型相关联的类型参数 设置要求

类型约束为关联类型定义要求 方面 也非常有用。
你可以通过定义where语句 作为类型参数列表的一部分来实现.
where语句 使你能够要求一个关联类型必须遵循一个特定的协议,或类型参数和关联类型必须是相同的

你可以这样写where语句:
在类型参数列表后面放置 where ,然后放置 针对关联类型的约束类型参数和关联类型间的等价关系

下面的例子定义了一个名为allItemsMatch的泛型函数,用来检查两个Container实例是否包含相同顺序的相同元素。如果所有的元素能够匹配,那么返回一个为true的Boolean值,反之则为false.
被检查的两个Container可以不是相同类型的容器(虽然它们可以是),但它们确实拥有相同类型的元素。这个需求通过一个类型约束和where语句结合来表示:

`
func allItemsMatch
//where语句作为类型参数列表的一部分
(someContainer: C1, anotherContainer: C2)//方法的参数
-> Bool//方法的返回值
{
// 检查两个Container的元素个数是否相同
if someContainer.count != anotherContainer.count
{
return false
}

// 检查两个Container相应位置的元素彼此是否相等
for i in 0..

}
`

这个函数用了两个参数someContainer和anotherContainer
someContainer参数是类型C1,anotherContainer参数是类型C2
C1和C2是两个占位类型代表的具体的Container类型在函数被调用是才会被决定.
该函数的类型参数的要求:
1.C1必须遵循Container协议(写作C1: Container)。
2.C2必须遵循Container协议(写作C2: Container)。
3.C1的ItemType同样是C2的ItemType(写作C1.ItemType == C2.ItemType)。
4.C1的ItemType必须遵循Equatable协议(写作C1.ItemType: Equatable)。

第3个和第4个要求被定义为where语句的一部分,写在关键字where后面,作为函数类型参数列表的一部分。

这些要求意思是:
someContainer是一个C1类型的容器。
anotherContainer是一个C2类型的容器。
someContainer和anotherContainer包含相同的元素类型。
someContainer中的元素可以通过不等于操作( != )来检查它们是否彼此不同。
第3个和第4个要求结合起来的意思是anotherContainer中的元素也可以通过!=操作来检查,因为它们在someContainer中元素确实是相同的类型。
这些要求能够使allItemsMatch函数比较两个容器,即便它们是不同的Container类型。
allItemsMatch首先检查两个容器是否拥有同样数目的items,如果它们的元素数目不同,便没有办法进行匹配,函数就会返回false 。
检查完之后,函数通过for-in循环和半闭区间操作( ..< )来迭代 someContainer中的所有元素。对于每个元素,函数检查是否 someContainer中的元素不等于对应的anotherContainer中的元素,如果这两个元素不等,则这两个容器不匹配,返回false 。
如果循环体结束后未发现没有任何的不匹配,那表明两个容器匹配,函数返回 true 。

allItemsMatch函数示例:

var stackOfStrings = Stack() stackOfStrings.push("uno") stackOfStrings.push("dos") stackOfStrings.push("tres") let arrayOfStrings = ["uno", "dos", "tres"] if allItemsMatch(stackOfStrings, anotherContainer: arrayOfStrings) { print("All items match.") } else { print("Not all items match.") } // All items match.

上面的例子创建一个Stack实例来存储字符串,然后压入三个字符串进栈。这个例子也创建了一个Array实例,并初始化包含三个同栈里一样的字符串。即便栈和数组是不同的类型,但它们都遵循Container协议,而且它们都包含同样的类型值。因此你可以调用allItemsMatch函数,用这两个容器作为它的参数。在上面的例子中, allItemsMatch函数正确的显示了这两个容器的所有元素都是相互匹配的。

你可能感兴趣的:(Swift语法总结2.23(泛型))