从数组起步,梳理Swift中一个重要的角色:泛型。 以及延伸一些函数式编程。
数组 Array
数组是Swift提供的一种集合类型,它用来存储有序的数据。
数组的完整写法:
Array<Element>
简略写法:
[Element]
创建一个空数组:
var someInts = [Int]()
Ok 暂时知道这些就够了。
泛型 Generics
泛型参数
假定我们想要写一个方法,交换两个Int类型的值。这很简单:
func swaptwoInts (a: inout Int, b: inout Int) {
var temp = a
a = b
b = temp
}
// inout 注解 : 函数参数默认是常量。试图在函数体中更改参数值将会导致编译错误。这意味着你不能错误地更改参数值。如果你想要一个函数可以修改参数的值,并且想要在这些修改在函数调用结束后仍然存在,那么就应该把这个参数定义为输入输出参数(In-Out Parameters)。
定义一个输入输出参数时,在参数定义前加 inout 关键字。
但如果我们需要交换两个String类型的值,你需要写一个新的方法:
func swaptwoString (a: inout String, b: inout String) {
var temp = a
a = b
b = temp
}
这显然很不优雅。这时候我们可以使用泛型。
func swapTwoValues<T> (a: inout T, b: inout T) {
var temp = a
a = b
b = temp
}
我们看一下两种写法的比较:
func swaptwoString (a: inout String, b: inout String)
func swapTwoValues<T> (a: inout T, b: inout T)
我们在函数后面增加了一对尖括号表示这是一个泛型函数,当我们对泛型类型没有明确的表意时,我们可以写一个T或者U,V 等占位符。占位符T不关心你传进来的参数具体是什么类型,只需要a 和 b的类型必须一致。
当你调用这个函数时,如果传进来两个Int 值, T 会被Int 类型替代,传进来两个String类型值,T 被String替代。
尖括号中的T 被称为类型参数, 你在使用泛型时,可以提供多个类型参数,并用逗号分开写在尖括号内。
这时候我们回看一下数组:
Array<Element>
我们发现数组就是基于泛型的定义,由系统直接提供给我们的一个可以存储数据的集合类型。 尖括号内Element就是一个占位符,它不关心数组内装的到底是什么类型的数据。不同类型的数据被塞进数组内时,Element会被指定的类型所代替。
泛型类型
除了泛型参数,Swift还允许我们自定义泛型类型。
我们来实现一个泛型类型的栈。 我们都知道栈的特点是: 先进后出,后进先出。
我们先来写一个非泛型版本的Int型栈。
struct Stack {
var items = [Int]()
mutating func push(item: Int) {
items.append(item)
}
mutating func pop(item: Int) -> Int {
return items.removeLast()
}
}
// 关于 mutating: 在Swift中,struct和enum是值类型,class是引用类型。
默认情况下,在结构体中,实例方法中是不可以修改值类型的属性,使用mutating后可修改属性的值
下面是泛型版本:
struct Stack<Element> {
var items = [Element]()
mutating func push(item: Element){
items.append(item)
}
mutating func pop(item: Element) -> Element {
return items.removeLast()
}
}
由于 Stack 是泛型类型,因此可以用来创建适用于 Swift 中任意有效类型的栈,就像 Array 和 Dictionary 那样。 你可以通过在尖括号中写出栈中需要存储的数据类型来创建并初始化一个 Stack 实例。例如,要创建一个 String 类型的栈,可以写成
Stack<String>():
var stackOfStrings = Stack<String>()
stackOfStrings.push("zhu")
stackOfStrings.push("ni")
stackOfStrings.push("kai")
stackOfStrings.push("xin")
// 栈中现在有 4 个字符串
我们这时候再回看一下数组:
Array<Element>
我们知道了,数组本身也是Swift为我们封装好的一种泛型类型。我们在实例化数组的时候(譬如Srting类型)都是这么写的:
var demoArray = Array<String>()
everything makes sense right? 有兴趣的同学可以去研究一下Array的标准库源码。
泛型扩展
当对泛型类型进行扩展时,你不需要提供类型参数作为定义的一部分,原始类型定义中声明的类型参数列表可以在扩展中直接使用。并且这些来自原始类型中的参数名称会被用作原始定义中类型参数的引用。
上代码:
extension Stack {
var topItem: Element? {
return items.isEmpty ? nil : items[items.count - 1]
}
}
我们对自定义的泛型类型Stack进行扩展的时候,为其添加了一个叫topItem的只读计算属性,它返回栈顶元素,并不会将其移除。
这个扩展并没有定义类型参数列表。相反的,Stack 类型已有的类型参数名称 Element,被用在扩展中来表示计算型属性 topItem 的可选类型。
类型约束
什么是类型约束?
swapTwoValues(::) 函数和 Stack 适用于任意类型。不过,如果能对泛型函数或泛型类型中添加特定的类型约束,这将在某些情况下非常有用。类型约束指定类型参数必须继承自指定类、遵循特定的协议或协议组合。
例如,Swift 的 Dictionary 类型对字典的键的类型做了些限制。在 字典的描述 中,字典键的类型必须是可哈希(hashable)的。也就是说,必须有一种方法能够唯一地表示它。字典键之所以要是可哈希的,是为了便于检查字典中是否已经包含某个特定键的值。若没有这个要求,字典将无法判断是否可以插入或替换某个指定键的值,也不能查找到已经存储在字典中的指定键的值。
这个要求通过 Dictionary 键类型上的类型约束实现,它指明了键必须遵循 Swift 标准库中定义的 Hashable 协议。所有 Swift 的基本类型(例如 String、Int、Double 和 Bool)默认都是可哈希的。如何让自定义类型遵循 Hashable 协议,可以查看文档 遵循 Hashable 协议。
当自定义泛型类型时,你可以定义你自己的类型约束,这些约束将提供更为强大的泛型编程能力。像 可哈希(hashable) 这种抽象概念根据它们的概念特征来描述类型,而不是它们的具体类型。
类型约束语法
在一个类型参数名后面放置一个类名或者协议名,并用冒号进行分隔,来定义类型约束。下面将展示泛型函数约束的基本语法(与泛型类型的语法相同):
func someFunction<T: SomeClass, U: SomeProtocol>(someT: T, someU: U) {
// 这里是泛型函数的函数体部分
}
老规矩,先写点代码看看:
func findIndex(ofString valueToFind: String , in array: [String]) -> Int? {
for (index, value) in array.enumerated() {
if value == valueToFind {
return index
}
}
return nil
}
我们先来说一说array.enumerated()
如果我们想要遍历数组并且拿到下标和元素,我们可以用元组来进行遍历。
let demoArray = ["a", "b", "c", "d"]
// 利用元组来遍历
for (index , value) in demoArray.enumerated() {
print (index , value)
}
// 输出结果
0 a
1 b
2 c
3 d
//如果想要反向对数组遍历,可以使用reversed()
for (index,value) in demoArray.enumerated().reversed() {
print(index,value)
}
// 打印结果
3 d
2 c
1 b
0 a
回到我们的findIndex 函数, 我们传进来了一个String值和一个数组,遍历数组,如果在数组中找到和传进来的String值相同的Sting,返回它的下标值,如果没有找到,返回nil。
这时候我们还没有写任何的泛型和泛型的类型约束。
我们升级到泛型版本函数:
func findIndex<T>(ofString valueToFind: T , in array: [T]) -> Int? {
for (index, value) in array.enumerated() {
if value == valueToFind {
return index
}
}
return nil
}
但是这时候xcode报错了,问题出在if value == valueToFind上:不是所有的Swift类型都可以用 == 进行比较,我们在这个函数中的ofString valueToFind本意是传进来一个String,但是对系统来说,如果你传进来一个复杂的数据模型,它无法确定== 的有效性。
这时候你很尴尬,用泛型吧,无法确定类型导致报错,不用泛型吧,代码将会写的很冗余。
别慌, 终于到了我们的类型约束发挥作用的时刻了,上才艺,展示!
Swift 标准库中定义了一个 Equatable 协议,该协议要求任何遵循该协议的类型必须实现等式符(==)及不等符(!=),从而能对该类型的任意两个值进行比较。所有的 Swift 标准类型自动支持 Equatable 协议。
我们来改造泛型函数:
func findIndex<T: Equatable>(ofString valueToFind: T , in array: [T]) -> Int? {
for (index, value) in array.enumerated() {
if value == valueToFind {
return index
}
}
return nil
}
其实很简单,就是给泛型参数T指定了一个类型。
我们来实例化调用一下看看:
let doubleIndex = findIndex(ofString: 9.3, in: [3.14159, 0.1, 0.25])
print(doubleIndex)
// doubleIndex 类型为 Int?,其值为 nil,因为 9.3 不在数组中
let stringIndex = findIndex(ofString: "Andrea", in: ["Mike", "Malcolm", "Andrea"])
print(stringIndex!)
// stringIndex 类型为 Int?,其值为 2
关联类型
定义一个协议时, 在协议中声明一个或者多个关联类型作为协议的一部分会很有用。因为关联类型可以为协议中的某一个类型提供占位符名称,而关联类型代表的具体类型在遵循协议的实际类型中才会被指定。关联类型可以通过 associatedtype 指定。
protocol Container {
associatedtype Item
mutating func append(item: Item)
var count: Int { get }
subscript(i: Int) -> Item { get }
}
这里我们定义了一个Container 协议, 如果我们有某个类型遵循了这个协议,我们这个类型必须提供的功能如下:
必须可以通过 append(_:) 方法添加一个新元素到容器里。
必须可以通过 count 属性获取容器中元素的数量,并返回一个 Int 值。
必须可以通过索引值类型为 Int 的下标检索到容器中的每一个元素。
但是,我们并没有在这个协议中指定我们要添加什么类型的元素,它可以是字符串,也可以说是整型。但是我们要保证,你用append方法添加到容器中的类型,和subscript能检索到的下标返回的类型是一致的。
所以我们指定了一个关联类型“Item”, 它的实际类型在遵循该协议的具体类型中被定义。
struct IntStack: Container {
// IntStack 的原始实现部分
var items = [Int]()
mutating func push(_ item: Int) {
items.append(item)
}
mutating func pop() -> Int {
return items.removeLast()
}
// IntStack 的协议实现部分
typealias Item = Int // 指定 Item 为 Int类型
mutating func append(_ item: Int) {
self.push(item)
}
var count: Int {
return items.count
}
subscript(i: Int) -> Int {
return items[i]
}
}
我们也可以让泛型版本的栈遵循这个协议:
struct Stack<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]
}
}
你可以看到,我们并没有给关联类型Item指定它的具体类型,如在Int类型栈中的 “typealias Item = Int ”,哪怕是泛型类型。这是因为,占位类型参数 Element 被用作 append(_:) 方法的 item 参数和下标的返回类型 , Swift强大的推断功能,帮我们推断出了Item的类型为Element.
给关联类型添加约束
当然,我们也可以给关联类型添加约束。比如我们可以要求Container中的Item 必须遵循 Equatable协议。
protocol Container {
associatedtype Item: Equatable
mutating func append(_ item: Item)
var count: Int { get }
subscript(i: Int) -> Item { get }
}
这样, 要遵守 Container 协议,Item 类型也必须遵守 Equatable 协议。
在关联类型约束里使用协议
协议可以作为它自身的要求实现(姬无命:是我!杀了我!) 比如, 我们有个协议细化了Container协议,添加了一个suffix方法,suffix(_:) 方法返回容器中从后往前给定数量的元素,并把它们存储在一个 Suffix 类型的实例里。
protocol SuffixableContainer: Container {
associatedtype Suffix: SuffixableContainer where Suffix.Item == Item
func suffix(_ size: Int) -> Suffix
}
我们来好好看一下这个 “ associatedtype Suffix: SuffixableContainer where Suffix.Item == Item ”
首先,在这个SuffixableContainer协议中, Suffix 是一个关联类型, 它就像上面Container中的Item一样。 但是Suffix拥有两个约束:1, 它必须遵循当前定义的SuffixableContainer协议(有点怪怪的对吧) 2, 它的 Item 类型必须是和Container里的 Item 类型相同。
我们可以写一个对Stack的扩展看一下
extension Stack: SuffixableContainer {
func suffix(size: Int) -> Stack {
var result = Stack()
for index in (count-size)..<count {
result.append(self[index])
}
return result
}
}
从上面这个扩展中,我们可以看到,我们的Stack 遵循了 SuffixableContainer这个协议,所以SuffixableContainer协议中定义的Suffix就是Stack的关联类型,这个没毛病吧。所以Suffix也是一种Stack,对吧。所以 Stack 的后缀运算返回另一个 Stack 。另外,遵循 SuffixableContainer 的类型可以拥有一个与它自己不同的 Suffix 类型——也就是说后缀运算可以返回不同的类型。比如说,这里有一个非泛型 IntStack 类型的扩展,它遵循了 SuffixableContainer 协议,使用 Stack 作为它的后缀类型而不是 IntStack:
extension IntStack: SuffixableContainer {
func suffix(size: Int) -> Stack<Int> {
var result = Stack<Int>()
for index in (count-size)..<count {
result.append(self[index])
}
return result
}
// 推断 suffix 结果是 Stack<Int>
}
函数式编程中的几个高阶函数 Map Filter Reduce
Map
如果我们要写一个函数,它接收给定的整形数组,通过计算得到一个各项为原数组加一的新数组。
我们可能会这样写:
func increment (array: [Int]) -> [Int] {
var item = [Int]()
for x in array {
item.append(x+1)
}
return item
}
如果我们这个函数接收整形的数组,我们通过计算得到一个原数组各项二倍的新数组。
我们会这样写:
func double (array: [Int]) -> [Int] {
var item = [Int]()
for x in array {
item.append(x * 2)
}
return item
}
大家可能会觉得,哎这个和一开始介绍泛型的时候交换两个值得函数有点像,想用泛型来改写函数。但仔细一看,两个函数的接收值类型和返回值类型却都相同,这好像没办法像我们写交换值泛型函数时那样,通过添加泛型参数以及给输入输出值泛型类型的方法来实现。
的确,现在我们还不需要使用泛型。但我们可以将没有区别的内容抽象出来:
func compute (array: [Int] , transform: (Int) -> Int) -> [Int] {
var item = [Int]()
for x in array {
item.append(transform(x))
}
return item
}
上面这个compute函数,我们新增加了一个 transform参数,这个参数接收一个函数,该函数的输入值是Int,输出值是Int。
因为我们item.append要接收一个Int类型的值,当需要对原数组的各项值进行操作时,transform这个函数正好符合我们的要求,它的返回值是Int型。
这样,当我们再实现数组乘2的操作时,只需要这样:
func double (array: [Int]) -> [Int] {
return compute(array: array){ $0 * 2 }
}
我们知道,compute函数接收一个整形数组和一个函数,最后返回一个整形数组。而我们的double函数,接收一个传进来的整形数组,返回一个整形数组。我们可以直接return compute函数的返回值,把double函数接收的整形数组传给compute, 然后给 compute函数传一个函数进去。
因为transform是compute函数的最后一个参数,我们直接使用了尾随闭包的形式。
我们继续来丰富compute函数。
func compute (array: [Int] , transform: (Int) -> Int) -> [Int]
我们知道,由于在一开始定义compute函数的时候,把transform函数的输入输出类型都指定为了Int,这限制了我们函数的发挥,如果我们想要得到一个布尔类型的新数组呢?
func isEven (array: [Int]) -> [Bool] {
return compute(array: array) { $0 % 2 == 0 }
}
这是不可行的。compute 函数接受一个 (Int) -> Int 类型的参数,也就是说,该参数是一个返回整型值的函数。而在 isEven 函数的定义中,我们传递了一个 (Int) -> Bool 类型的参数,于是导致了类型错误。
我们怎么解决这个问题呢,显然更改compute函数中的transform类型为(Int)-> Bool 并不是一个好办法。这又限制了compute函数的功能。
这时候该我们的泛型出场了。
func compute<T> (array: [Int] , transform: (Int) -> T) -> [T] {
var items: [T] = []
for x in array {
items.append(transform(x))
}
return items
}
现在变得更清晰了吧,我们不关心transform最后返回的是什么类型,也不关心compute最后返回的数组是什么类型,只需要这两个类型保持一致即可。
我们还可以继续优化它,因为我们的array参数还在指定着[Int] 类型。
func compute<Element, T> (array: [Element] , transform: (Element) -> T) -> [T] {
var items: [T] = []
for x in array {
items.append(transform(x))
}
return items
}
这样,我们的compute函数更通用了,你可以传进来任意类型的数组,可以输出为任意类型的数组。
其实,这就是map函数:
func map<Element, T> (array: [Element] , transform: (Element) -> T) -> [T] {
var items: [T] = []
for x in array {
items.append(transform(x))
}
return items
}
我们再次再次回到Array, 看一下它的定义:
Array<Element>
其实更好的方法是定义map为Array的扩展:
extension Array {
func map<T> (transform: (Element) -> T) -> [T] {
var result: [T] = []
for x in self {
result.append(transform(x))
}
return result
}
}
我们直接对数组进行扩展,由于系统已经为我们的数组定义好了Element泛型,我们不需要再给map函数传递泛型类型的array参数,在我们进行for循环的时候,用self代替了之前的array参数。
你可以自己来定义map的更多功能,但是系统在标准库中已经为我们封装好了map函数。它是Array的扩展。
这时候你应该知道了map的作用: 当我们拥有一个数组对象时,我们可以使用map函数对其进行操作。
let demoArray = [0, 3, 4, 6, 7, 8]
print(demoArray.map{ $0 % 2 == 0 })
//输出结果:[true, false, true, true, false, true]
Filter
假如我们有一个String类型数组,代表文件夹内的内容。
let files = ["README.md", "helloWorld.swift", "demo.swift"]
我们想要得到所有文件类型为.swift的文件,这可以通过循环得到:
func getSwiftFiles (array: [String]) -> [String] {
var result: [String] = []
for file in array {
if files.hasSuffix(".swift") {
result.append(file)
}
}
return result
}
这显然不是一个优雅的可扩展方法,因为如果我们要查找.md类型文件,或者文件名中含有某个字符的文件,我们需要重新构建方法,这些方法中的重复部分很多。
我们可以像map函数一样,给Array一个filter扩展。
filter的函数类型是(Element) -> Bool,代表对数组中的元素,使用filter会判定它是否应该被包含在结果中。
extension Array {
func filter(includeElement: (Element) -> Bool) -> [Element] {
var result: [Element] = []
for x in self where includeElement(x) {
result.append(x)
}
return result
}
}
当然,filter也已经是被封装好的扩展。我们可以直接用:
let demoArray = [0, 3, 4, 6, 7, 8]
print(demoArray.filter{ $0 % 2 == 0 })
// [0, 4, 6, 8]
Reduce
我们还是先来写一点简单函数。
我们定义一个函数,用来计算所有数组中的和。
func sum(array: [Int]) -> Int {
var result: Int = 0
for x in array {
result += x
}
return result
}
如果我们想计算数组所有值得积:
func product(integers: [Int]) -> Int {
var result: Int = 1
for x in integers {
result = x * result
}
return result
}
如果我们想连结数组中所有的字符串:
func concatenate (strings: [String]) -> String {
var result = ""
for string in strings {
result += string
}
return result
}
我们发现这些函数都有一个共同点: 就是都将result初始化为了某个值,然后通过对数组的遍历,以某种方式更新了最后的结果。所以,我们需要抽象两个东西: 赋给result的初始值,和在循环中更新result的函数。
extension Array {
func reduce<T>( intital: T, combine: (T, Element) -> T) -> T {
var result = intital
for x in self {
result = combine(result, x)
}
return result
}
}
我们用一个实例来感受一下Map , Filter 和 Reduce
// 城市
struct City {
let name: String
let population: Int
}
//我们筛选出人口大于100万的城市,先写一个换算人口方法。
extension City{
func scalingPopulation() -> City {
return City(name: name, population: population * 1000)
}
}
let beijing = City(name: "BeiJing", population: 2241)
let shanghai = City(name: "ShangHai", population: 3165)
let tianjin = City(name: "TianJin", population: 827)
let xianggang = City(name: "HongKong", population: 3562)
let cities = [beijing, shanghai, tianjin, xianggang]
let demo = cities.filter { $0.population > 1000 }
.map { $0.scalingPopulation() }
.reduce("City: Population"){
result, city in
return result + "\n" + "\(city.name): \(city.population)"
}
print(demo)
// 输出结果:
City: Population
BeiJing: 2241000
ShangHai: 3165000
HongKong: 3562000