泛型解决的问题
下面是一个标准的非泛型函数 swapTwoInts(_:_:)
,用来交换两个 Int
值:
funcswapTwoInts(_a: inoutInt, _b: inoutInt) { 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(_:_:)
函数:
funcswapTwoStrings(_a: inoutString, _b: inoutString) { let temporaryA = a a = b b = temporaryA } funcswapTwoDoubles(_a: inoutDouble, _b: inoutDouble) { let temporaryA = a a = b b = temporaryA }
你可能注意到了,swapTwoInts(_:_:)
、swapTwoStrings(_:_:)
和 swapTwoDoubles(_:_:)
函数体是一样的,唯一的区别是它们接受的参数类型(Int
、String
和 Double
)。
在实际应用中,通常需要一个更实用更灵活的函数来交换两个任意类型的值,幸运的是,泛型代码帮你解决了这种问题。(这些函数的泛型版本已经在下面定义好了。)
注意
在上面三个函数中,
a
和b
类型必须相同。如果a
和b
类型不同,那它们俩就不能互换值。Swift 是类型安全的语言,所以它不允许一个String
类型的变量和一个Double
类型的变量互换值。试图这样做将导致编译错误。
泛型函数
泛型函数可适用于任意类型,下面是函数 swapTwoInts(_:_:)
的泛型版本,命名为 swapTwoValues(_:_:)
:
funcswapTwoValues<T>(_a: inout T, _b: inout T) { let temporaryA = a a = b b = temporaryA }
swapTwoValues(_:_:)
和 swapTwoInts(_:_:)
函数体内容相同,它们只在第一行不同,如下所示:
funcswapTwoInts(_a: inoutInt, _b: inoutInt) funcswapTwoValues<T>(_a: inout T, _b: inout T)
泛型版本的函数使用占位符
类型名(这里叫做 T
),而不是 实际类型名(例如 Int
、String
或 Double
),占位符
类型名并不关心 T
具体的类型,但它要求 a
和 b
必须是相同的类型,T
的实际类型由每次调用 swapTwoValues(_:_:)
来决定。
泛型函数和非泛型函数的另外一个不同之处在于这个泛型函数名(swapTwoValues(_:_:)
)后面跟着占位类型名(T
),并用尖括号括起来(<T>
)。这个尖括号告诉 Swift 那个 T
是 swapTwoValues(_:_:)
函数定义内的一个占位类型名,因此 Swift 不会去查找名为 T
的实际类型。
swapTwoValues(_:_:)
函数现在可以像 swapTwoInts(_:_:)
那样调用,不同的是它能接受两个任意类型的值,条件是这两个值有着相同的类型。swapTwoValues(_:_:)
函数被调用时,T
所代表的类型都会由传入的值的类型推断出来。
在下面的两个例子中,T
分别代表 Int
和 String
:
var someInt =3 var anotherInt =107 swapTwoValues(&someInt, &anotherInt) // someInt 现在是 107,anotherInt 现在是 3 var someString ="hello" var anotherString ="world" swapTwoValues(&someString, &anotherString) // someString 现在是“world”,anotherString 现在是“hello”
注意
上面定义的
swapTwoValues(_:_:)
函数是受swap(_:_:)
函数启发而实现的。后者存在于 Swift 标准库,你可以在你的应用程序中使用它。如果你在代码中需要类似swapTwoValues(_:_:)
函数的功能,你可以使用已存在的swap(_:_:)
函数。
类型参数
上面 swapTwoValues(_:_:)
例子中,占位类型 T
是一个类型参数的例子,类型参数指定并命名一个占位类型,并且紧随在函数名后面,使用一对尖括号括起来(例如 <T>
)。
一旦一个类型参数被指定,你可以用它来定义一个函数的参数类型(例如 swapTwoValues(_:_:)
函数中的参数 a
和 b
),或者作为函数的返回类型,还可以用作函数主体中的注释类型。在这些情况下,类型参数会在函数调用时被实际类型所替换。(在上面的 swapTwoValues(_:_:)
例子中,当函数第一次被调用时,T
被 Int
替换,第二次调用时,被 String
替换。)
你可提供多个类型参数,将它们都写在尖括号中,用逗号分开。
命名类型参数
大多情况下,类型参数具有描述下的名称,例如字典 Dictionary<Key, Value>
中的 Key
和 Value
及数组 Array<Element>
中的 Element
,这能告诉阅读代码的人这些参数类型与泛型类型或函数之间的关系。然而,当它们之间没有有意义的关系时,通常使用单个字符来表示,例如 T
、U
、V
,例如上面演示函数 swapTwoValues(_:_:)
中的 T
。
注意
请始终使用大写字母开头的驼峰命名法(例如
T
和MyTypeParameter
)来为类型参数命名,以表明它们是占位类型,而不是一个值。
泛型类型
除了泛型函数,Swift 还允许自定义 泛型类型 。这些自定义类、结构体和枚举可以适用于 任意类型 ,类似于 Array
和 Dictionary
。
本节将向你展示如何编写一个名为 Stack
(栈)的泛型集合类型。栈是值的有序集合,和数组类似,但比数组有更严格的操作限制。数组允许在其中任意位置插入或是删除元素。而栈只允许在集合的末端添加新的元素(称之为入栈)。类似的,栈也只能从末端移除元素(称之为出栈)。
注意
栈的概念已被
UINavigationController
类用来构造视图控制器的导航结构。你通过调用UINavigationController
的pushViewController(_:animated:)
方法来添加新的视图控制器到导航栈,通过popViewControllerAnimated(_:)
方法来从导航栈中移除视图控制器。每当你需要一个严格的“后进先出”方式来管理集合,栈都是最实用的模型。
- 现在有三个值在栈中。
- 第四个值被压入到栈的顶部。
- 现在栈中有四个值,最近入栈的那个值在顶部。
- 栈中最顶部的那个值被移除出栈。
- 一个值移除出栈后,现在栈又只有三个值了。
下面展示如何编写一个非泛型版本的栈,以 Int
型的栈为例:
structIntStack { var items: [Int] = [] mutatingfuncpush(_item: Int) { items.append(item) } mutatingfuncpop() ->Int { return items.removeLast() } }
这个结构体在栈中使用一个名为 items
的数组属性来存储值。栈提供了两个方法:push(_:)
和 pop()
,用来向栈中压入值以及从栈中移除值。这些方法被标记为 mutating
,因为它们需要修改结构体的 items
数组。
上面的 IntStack
结构体只能用于 Int
类型。不过,可以定义一个泛型 Stack
结构体,从而能够处理任意类型的值。
下面是相同代码的泛型版本:
structStack<Element> { var items: [Element] = [] mutatingfuncpush(_item: Element) { items.append(item) } mutatingfuncpop() ->Element { return items.removeLast() } }
注意,Stack
基本上和 IntStack
相同,只是用占位类型参数 Element
代替了实际的 Int
类型。这个类型参数包裹在紧随结构体名的一对尖括号里(<Element
>)。
Element
为待提供的类型定义了一个占位名。这种待提供的类型可以在结构体的定义中通过 Element
来引用。在这个例子中,Element
在如下三个地方被用作占位符:
- 创建
items
属性,使用Element
类型的空数组对其进行初始化。 - 指定
push(_:)
方法的唯一参数item
的类型必须是Element
类型。 - 指定
pop()
方法的返回值类型必须是Element
类型。
由于 Stack
是泛型类型,因此可以用来创建适用于 Swift 中任意有效类型的栈,就像 Array
和 Dictionary
那样。
你可以通过在尖括号中写出栈中需要存储的数据类型来创建并初始化一个 Stack
实例。例如,要创建一个 String
类型的栈,可以写成 Stack<String>()
:
var stackOfStrings = Stack<String>() stackOfStrings.push("uno") stackOfStrings.push("dos") stackOfStrings.push("tres") stackOfStrings.push("cuatro") // 栈中现在有 4 个字符串
移除并返回栈顶部的值“cuatro”,即出栈:
let fromTheTop = stackOfStrings.pop() // fromTheTop 的值为“cuatro”,现在栈中还有 3 个字符串
泛型扩展
当对泛型类型进行扩展时,你并不需要提供类型参数列表作为定义的一部分。原始类型定义中声明的类型参数列表在扩展中可以直接使用,并且这些来自原始类型中的参数名称会被用作原始定义中类型参数的引用。
下面的例子扩展了泛型类型 Stack
,为其添加了一个名为 topItem
的只读计算型属性,它将会返回当前栈顶元素且不会将其从栈中移除:
extensionStack { var topItem: Element? { return items.isEmpty?nil: items[items.count-1] } }
topItem
属性会返回 Element
类型的可选值。当栈为空的时候,topItem
会返回 nil
;当栈不为空的时候,topItem
会返回 items
数组中的最后一个元素。
注意:这个扩展并没有定义类型参数列表。相反的,Stack
类型已有的类型参数名称 Element
,被用在扩展中来表示计算型属性 topItem
的可选类型。
计算型属性 topItem
现在可以用来访问任意 Stack
实例的顶端元素且不移除它:
iflet topItem = stackOfStrings.topItem { print("The top item on the stack is \(topItem).") } // 打印“The top item on the stack is tres.”
泛型类型的扩展,还可以包括类型扩展需要额外满足的条件,从而对类型添加新功能,这一部分将在 具有泛型 Where 子句的扩展 中进行讨论。