泛型
泛型代码可以让你编写适用自定义需求以及任意类型的灵活可重用的函数和类型。它的可以让你避免重复的代码,用一种清晰和抽象的方式来表达代码的意图。
泛型
是Swift的强大特性之一,许多Swift标准库
是通过泛型代码构建的。事实上,泛型的使用贯穿了整本语言手册,只是你可能没有发现而已。例如,Swift的Array
和Dictionary
都是泛型集合。你可以创建一个Int
数组,也可创建一个String
数组,甚至可以是任意其他Swift
类型的数组。同样的,你也可以创建存储任意指定类型的字典。
泛型所解决的问题
func swapTwoInts(inout a: Int,inout _ b: Int) {
let temp = a
a = b
b = temp
}
var x = 10
var y = 20
swapTwoInts(&x,&y)
print(x)
print(y)
func swapTwoDoubles(inout a: Double,inout _ b: Double) {
let temp = a
a = b
b = temp
}
var m = 1.2
var n = 2.3
swapTwoDoubles(&m,&n)
print(m)
print(n)
func swapTwoStrings(inout a: String,inout _ b: String) {
let temp = a
a = b
b = temp
}
var p = "aaaa"
var q = "bbbb"
swapTwoStrings(&p,&q)
print(p)
print(q)
在上面三个函数中,a
和b
类型相同。如果a
和b
类型不同,那它们俩就不能互换值。Swift是类型安全的语言,所以它不允许一个String
类型的变量和一个Double
类型的变量互换值。试图这样做将导致编译错误。
泛型函数
泛型函数可以适用于任何类型。
func swapTwoValues<T>(inout a: T,inout _ b: T) {
let tempA = a
a = b
b = tempA
}
var x = 1
var y = 2
swapTwoValues(&x,&y)
print(x)
print(y)
var m = "Hello"
var n = "World"
swapTwoValues(&m,&n)
print(m)
print(n)
swap(&m,&n) // 调用Swift标准库中的泛型函数
print(m)
print(n)
这个函数的泛型版本使用了占位类型名
(在这里用字母T
来表示)来代替实际类型名(例如Int
、String
或Double
)。占位类型名没有指明T
必须是什么类型,但是它指明了a
和b
必须是同一类型T
,而无论T
代表什么类型。只有swapTwoValues(_:_:)
函数在调用时,才能根据所传入的实际类型决定T
所代表的类型。
另外一个不同之处在于这个泛型函数名后面跟着占位类型名(T
),而且是用尖括号括起来的(<T>
)。这个尖括号告诉Swift
那个T
是swapTwoValues(_:_:)
函数定义的一个占位类型名,因此Swift
不会去查找名为`T“的实际类型。
上面定义的swapTwoValues(_:_:)
函数是受swap(_:_:)
函数启发而实现的。后者存在于Swift标准库
,你可以在你的应用程序中使用它。如果你在代码中需要类似swapTwoValues(_:_:)
函数的功能,你可以使用已存在的swap(_:_:)
函数。
类型参数
一旦一个类型参数被指定,你可以用它来定义一个函数的参数类型(例如swapTwoValues(_:_:)
函数中的参数a
和b
),或者作为函数的返回类型,还可以用作函数主体中的注释类型。在这些情况下,类型参数会在函数调用时被实际类型所替换。可提供多个类型参数,将它们都写在尖括号中,用逗号分开。
命名类型参数
在大多数情况下,类型参数具有一个描述性名字,例如Dictionary<Key,Value>
中的Key
和Value
,以及Array<Element>
中的Element
,这可以告诉阅读代码的人这些类型参数和泛型函数之间的关系。然而,当它们之间的关系没有意义时,通常使用单一的字母来命名,例如T
、U
、V
。
注意,请始终使用大写字母开头的驼峰式命名法(例如T
和MyTypeParameter
)来为类型参数命名,以表明它们是占位类型,而不是一个值。
泛型类型
除了泛型函数,Swift还允许你定义泛型类型
。这些自定义类、结构体和枚举可以适用于任何类型,如同Array
和Dictionary
的用法。
栈是一系列值的有序集合,和Array
类似,但它相比Swift的Array
类型有更多的操作限制。数组允许对其中任意位置的元素执行插入或删除操作。而栈,只允许在集合的末端添加新的元素(称之为入栈
)。同样的,栈也只能从末端移除元素(称之为出栈
)。
注意,栈的概念已被UINavigationController
类用来模拟视图控制器的导航结构。你通过调用UINavigationController
的pushViewController(_:animated:)
方法来添加新的视图控制器到导航栈,通过popViewControllerAnimated(_:)
方法来从导航栈中移除某个视图控制器。每当你需要一个严格的“后进先出”方式来管理集合,栈都是最实用的模型。
struct IntStack {
var items = [Int]()
mutating func push(item: Int) {
items.append(item)
}
mutating func pop() -> Int {
return items.removeLast()
}
}
struct Stack<Element> {
var items = [Element]()
mutating func push(item: Element) {
items.append(item)
}
mutating func pop() -> Element {
return items.removeLast()
}
}
var s = Stack<String>()
s.push("abc")
s.push("def")
s.push("ghi")
s.push("jkl")
let a = s.pop()
print(a)
扩展一个泛型类型
当你扩展一个泛型类型的时候,你并不需要在扩展的定义中提供类型参数列表。更加方便的是,原始类型定义中声明的类型参数列表在扩展中可以直接使用,并且这些来自原始类型中的参数名称会被用作原始定义中类型参数的引用。
struct Stack<Element> {
var items = [Element]()
mutating func push(item: Element) {
items.append(item)
}
mutating func pop() -> Element {
return items.removeLast()
}
}
/** * 这个扩展并没有定义一个类型参数列表。相反的,Stack类型已有的类型参数名称Element,被用在扩展中来表示计算型属性topItem的可选类型。 */
extension Stack {
var topItem: Element? {
return items.isEmpty ? nil : items[items.count - 1]
}
}
var s = Stack<String>()
s.push("abc")
s.push("def")
s.push("ghi")
s.push("jkl")
if let item = s.topItem { // 可选绑定
print(item)
}
类型约束
有的时候如果能将使用在泛型函数
和泛型类型
中的类型,强制约束为某种特定类型,将会是非常有用的。类型约束可以指定一个类型参数必须继承自指定类,或者符合一个特定的协议或协议组合。
例如,Swift的Dictionary
类型对字典的键的类型做了些限制。在字典的描述中,字典的键的类型必须是可哈希的。也就是说,必须有一种方法能作为其唯一的表示。Dictionary
之所以需要其键是可哈希的,是为了便于检查字典是否已经包含某个特定键的值。如无此要求,Dictionary
将无法判断是否可以插入或者替换某个指定键的值,也不能查找到已经存储在字典中的指定键的值。
这个要求强制加上了一个类型约束作用于Dictionary的键类型上,其键类型必须符合
Hashable协议,这是Swift标准库中定义的一个特定协议。***所有的Swift基本类型(例如
String、
Int、
Double和
Bool`)默认都是可哈希的*。
当你创建自定义泛型类型时,你可以定义你自己的类型约束,这些约束将提供更为强大的泛型编程能力。抽象概念,例如可哈希的,描述的是类型在概念上的特征,而不是它们的显式类型。
类型约束语法
可以在一个类型参数名后面放置一个类名或者协议名,通过冒号分隔,从而定义类型约束,它们将作为类型参数列表的一部分。这种基本的类型约束作用于泛型函数时的语法如下所示(作用于泛型类型时的语法与之相同)。
func someFunction<T: SomeClass,U: SomeProtocol>(someT: T,someU: U) {
// 这里是泛型函数的函数体部分
}
类型约束实践
func findStringIndex(array: [String],_ valueToFind: String) -> Int? {
for (index,value) in array.enumerate() {
if value == valueToFind {
return index
}
}
return nil
}
let s = ["a","b","c"]
if let i = findStringIndex(s,"b") {
print("index = \(i)")
} else {
print("not found")
}
func findIndex<T: Equatable>(array: [T],_ valueToFind: T) -> Int? {
for (index,value) in array.enumerate() {
if value == valueToFind { // 不加: Equatable会报错:error: binary operator '==' cannot be applied to two 'T' operands
return index
}
}
return nil
}
print(findIndex([1.1,2.2,3.3],4.4)) // nil
print(findIndex(["11","22","333"],"333")!) // 2
在上面的代码中,不加: Equatable
会报错:error: binary operator '==' cannot be applied to two 'T' operands
不是所有的Swift类型都可以用等式符(==
)进行比较。例如,如果你创建一个你自己的类或结构体来表示一个复杂的数据模型,那么Swift无法猜到对于这个类或结构体而言“相等”意味着什么。正因如此,这部分代码无法保证适用于每个可能的类型T
,当你试图编译这部分代码时会出现相应的错误。
不过,所有的这些并不会让我们无从下手。Swift标准库中定义了一个Equatable
协议,该协议要求任何符合该协议的类型必须实现等式符(==
),从而能对符合该协议的类型的任意两个值进行比较。所有的Swift标准类型自动支持Equatable
协议。
加了: Equatable
之后,任何Equatable
类型都可以安全地使用在findIndex(_:_:)
函数中,因为其保证支持等式操作符。
关联类型
定义一个协议时,有的时候声明一个或多个关联类型作为协议定义的一部分将会非常有用。关联类型作为协议的一部分,为某个类型提供了一个占位名(或者说别名),其代表的实际类型在协议被采纳时才会被指定。你可以通过typealias
关键字来指定关联类型。
关联类型实践
protocol Container {
typealias ItemType // Container协议需要在不知道容器中元素的具体类型的情况下引用这种类型,因此声明了一个关联类型ItemType,这个协议无法定义ItemType是什么类型的别名,这个信息将留给采纳协议的类型来提供。
mutating func append(item: ItemType)
var count: Int { get }
subscript(i: Int) -> ItemType { get }
}
struct IntStack2: Container {
var items = [Int]()
mutating func push(item: Int) {
items.append(item)
}
mutating func pop() -> Int {
return items.removeLast()
}
// 遵循的Container协议的实现部分
typealias ItemType = Int // IntStack指定ItemType为Int类型,将Container协议中抽象的ItemType类型转换为具体的Int类型。
mutating func append(item: Int) {
self.push(item)
}
var count: Int {
return items.count
}
subscript(i: Int) -> Int {
return items[i]
}
}
/** * 由于Swift的类型推断,你实际上不用在IntStack的定义中声明ItemType为Int。因为IntStack符合Container协议的所有要求,Swift 只需通过append(_:)方法的item参数类型和下标返回值的类型,就可以推断出ItemType的具体类型。事实上,如果你在上面的代码中删除了typealias ItemType = Int这一行,这一切仍旧可以正常工作,因为Swift清楚地知道ItemType应该是何种类型。 */
struct IntStack: Container {
var items = [Int]()
mutating func push(item: Int) {
items.append(item)
}
mutating func pop() -> Int {
return items.removeLast()
}
// 遵循的Container协议的实现部分
// typealias ItemType = Int // IntStack指定ItemType为Int类型,将Container协议中抽象的ItemType类型转换为具体的Int类型。
mutating func append(item: Int) {
self.push(item)
}
var count: Int {
return items.count
}
subscript(i: Int) -> Int {
return items[i]
}
}
/** * 泛型版本的遵循了Container协议的Stack。 * 占位类型参数Element被用作append(_:)方法的item参数和下标的返回类型。Swift可以据此推断出Element的类型即是ItemType的类型。 */
struct Stack<Element>: Container {
var items = [Element]()
mutating func push(item: Element) {
items.append(item)
}
mutating func pop() -> Element {
return items.removeLast()
}
mutating func append(item: Element) {
self.push(item)
}
var count: Int {
return items.count
}
subscript(i:Int) -> Element {
return items[i]
}
}
由于Swift的类型推断,你实际上不用在IntStack
的定义中声明ItemType
为Int
。因为IntStack
符合Container
协议的所有要求,Swift只需通过append(_:)
方法的item
参数类型和下标返回值的类型,就可以推断出ItemType
的具体类型。事实上,如果你在上面的代码中删除了typealias ItemType = Int
这一行,这一切仍旧可以正常工作,因为Swift清楚地知道ItemType
应该是何种类型。
而在泛型类型Stack
中,占位类型参数Element
被用作append(_:)
方法的item
参数和下标的返回类型。Swift可以据此推断出Element
的类型即是ItemType
的类型。
通过扩展一个存在的类型来指定关联类型
extension Array: Container {}
Swift的Array
已经提供append(_:)
方法,一个count
属性,以及一个接受Int
型索引值的可用来检索数组元素的下标。这三个功能都符合Container
协议的要求,也就意味着你可以扩展Array
去符合Container
协议,只需简单地声明Array
采纳该协议即可。你可以通过一个空扩展来实现这点。
Array
的append(_:)
方法和下标确保了Swift 可以推断出ItemType
的类型。定义了这个扩展后,你可以将任意Array
当作Container
来使用。
where子句
为关联类型定义约束也是非常有用的。你可以在参数列表中通过where
子句为关联类型定义约束。一个where
子句能够使一个关联类型符合某个特定的协议,以及某个特定的类型参数和关联类型必须类型相同。你可以通过将where
关键字紧跟在类型参数列表后面来定义where
子句,where
子句后跟一个或者多个针对关联类型的约束,以及一个或多个类型参数和关联类型间的相等关系。
protocol Container {
typealias ItemType
mutating func append(item: ItemType)
var count: Int { get }
subscript(i: Int) -> ItemType { get }
}
struct Stack<Element>: Container {
var items = [Element]()
mutating func push(item: Element) {
items.append(item)
}
mutating func pop() -> Element {
return items.removeLast()
}
mutating func append(item: Element) {
self.push(item)
}
var count: Int {
return items.count
}
subscript(i:Int) -> Element {
return items[i]
}
}
extension Array: Container {}
/**
* 被检查的两个Container可以不是相同类型的容器(虽然它们可以相同),但它们必须拥有相同类型的元素。这个要求通过一个类型约束以及一个where子句来表示
*/
func allItemsMatch<C1: Container,C2: Container where C1.ItemType == C2.ItemType,C1.ItemType: Equatable>(aContainer: C1,_ bContainer: C2) -> Bool {
if aContainer.count != bContainer.count {
return false
}
for i in 0..<aContainer.count {
if aContainer[i] != bContainer[i] {
return false
}
}
return true
}
let arr = ["abc","def","ghi"]
var s = Stack<String>()
s.append("abc")
s.append("def")
s.append("ghi")
print(allItemsMatch(arr,s)) // true
即使栈和数组是不同的类型,但它们都符合Container
协议,而且它们都包含相同类型的值。因此你可以用这两个容器作为参数来调用allItemsMatch(_:_:)
函数。